Java8新特性(一)
Lambda表达式
//匿名内部类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("测试");
}
};
r1.run();
System.out.println("******************");
//Lambda表达式
Runnable r2 = () -> System.out.println("测试2");
r2.run();
//匿名内部类
Comparator<Integer> c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};
int compare = c1.compare(12, 23);
System.out.println(compare);
System.out.println("*********");
//Lambda表达式
Comparator<Integer> c2 = (o1,o2) -> Integer.compare(o1,o2);
int compare1 = c2.compare(12, 10);
System.out.println(compare1);
System.out.println("*********");
//方法引用
Comparator<Integer> c3 = Integer::compare;
int compare2 = c3.compare(23, 23);
System.out.println(compare2);
//匿名内部类
Consumer<String> c1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
c1.accept("测试");
System.out.println("*****************");
//Lambda表达式
Consumer<String> c2 = s -> System.out.println(s);
c2.accept("hello");
//简化Lambda表达式
Comparator<Integer> c1 = (o1, o2) -> Integer.compare(o1,o2);
int compare = c1.compare(10, 20);
System.out.println(compare);
Functional Interfaces(函数接口)
ambda 表达式如何适应 Java 的类型系统?每个 lambda 对应一个由接口指定的类型。一个所谓的函数接口必须包含一个抽象方法声明。该类型的每个 lambda 表达式都将与此抽象方法匹配。由于默认方法不是抽象的,所以你可以自由地添加默认方法到你的函数接口。
只要保证接口仅包含一个抽象方法,就可以使用任意的接口作为 lambda 表达式。为确保您的接口符合要求,您应该添加 @FunctionalInterface
注解。编译器注意到这个注解后,一旦您尝试在接口中添加第二个抽象方法声明,编译器就会抛出编译器错误。
四种常用的函数式接口:
四大核心函数式接口
消费型Consumer<T> void accept(T t);
供给型 Supplier<T> T get();
函数型 Function<T,R> R apply(T);
断言型 Predicate(T) boolean test(T);
Consumer<String> c1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
System.out.println("****************");
Consumer<String> c2 = s -> System.out.println(s);
c2.accept("hello");
Supplier<Integer> supplier = () -> 10;
System.out.println(supplier.get());
Function<Integer,String> function = (o1) -> "ces"+o1*2;
System.out.println(function.apply(20));
Predicate<Integer> predicate = o1 -> o1 > 10;
System.out.println(predicate.test(20));
System.out.println(predicate.test(8));
Method and Constructor References(方法和构造器引用)
上面的示例代码可以通过使用静态方法引用进一步简化:
Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123
Java 8 允许您通过 ::
关键字传递方法或构造函数的引用。上面的例子展示了如何引用一个静态方法。但是我们也可以引用对象方法:
class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
我们来观察一下 ::
关键字是如何作用于构造器的。首先,我们定义一个有多个构造器的示例类。
class Person {
String firstName;
String lastName;
Person() {}
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
接着,我们指定一个用于创建 Person 对象的 PersonFactory 接口。
interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
我们不是手动实现工厂,而是通过构造引用将所有东西粘合在一起:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
我们通过 Person::new
来创建一个 Person 构造器的引用。Java 编译器会根据PersonFactory.create
的签名自动匹配正确的构造器。
Lambda Scopes(Lambda 作用域)
从 lambda 表达式访问外部作用域变量与匿名对象非常相似。您可以访问本地外部作用域的常量以及实例的成员变量和静态变量。
Accessing local variables(访问本地变量)
我们可以访问 lambda 表达式作用域外部的常量:
final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
不同于匿名对象的是:这个变量 num
不是一定要被 final
修饰。下面的代码一样合法:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
但是,num
必须是隐式常量的。下面的代码不能编译通过:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;
此外,在 lambda 表达式中对 num
做写操作也是被禁止的。
Accessing fields and static variables(访问成员变量和静态变量)
与局部变量相比,我们既可以在 lambda 表达式中读写实例的成员变量,也可以读写实例的静态变量。这种行为在匿名对象中是众所周知的。
class Lambda4 {
static int outerStaticNum;
int outerNum;
void testScopes() {
Converter<Integer, String> stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};
Converter<Integer, String> stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
}
}
Accessing Default Interface Methods(访问默认的接口方法)
还记得第一节的 formula 例子吗? Formula
接口定义了一个默认方法 sqrt
,它可以被每个 formula 实例(包括匿名对象)访问。这个特性不适用于 lambda 表达式。
默认方法不能被 lambda 表达式访问。下面的代码不能编译通过:
Formula formula = (a) -> sqrt(a * 100);
Optionals
Optional
不是功能性接口,而是防止 NullPointerException
的好工具。这是下一节的一个重要概念,所以让我们快速看看 Optional
是如何工作的。
Optional
是一个简单的容器,其值可以是 null 或非 null。想想一个可能返回一个非空结果的方法,但有时候什么都不返回。不是返回 null,而是返回 Java 8 中的 Optional
。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Streams
java.util.Stream
表示可以在其上执行一个或多个操作的元素序列。流操作是中间或终端。当终端操作返回一个特定类型的结果时,中间操作返回流本身,所以你可以链接多个方法调用。流在源上创建,例如一个 java.util.Collection
像列表或集合(不支持映射)。流操作既可以按顺序执行,也可以并行执行。
一、Stream 流是如何工作的?
流表示包含着一系列元素的集合,我们可以对其做不同类型的操作,用来对这些元素执行计算。听上去可能有点拗口,让我们用代码说话:
List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream() // 创建流
.filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
.map(String::toUpperCase) // 转换成大写
.sorted() // 排序
.forEach(System.out::println); // for 循环打印
我们可以对流进行中间操作或者终端操作。小伙伴们可能会疑问?什么是中间操作?什么又是终端操作?
Stream中间操作,终端操作
- ①:中间操作会再次返回一个流,所以,我们可以链接多个中间操作,注意这里是不用加分号的。上图中的
filter
过滤,map
对象转换,sorted
排序,就属于中间操作。 - ②:终端操作是对流操作的一个结束动作,一般返回
void
或者一个非流的结果。上图中的forEach
循环 就是一个终止操作。
看完上面的操作,感觉是不是很像一个流水线式操作呢。
实际上,大部分流操作都支持 lambda 表达式作为参数,正确理解,应该说是接受一个函数式接口的实现作为参数。
二、不同类型的 Stream 流
我们可以从各种数据源中创建 Stream 流,其中以 Collection 集合最为常见。如 List
和 Set
均支持 stream()
方法来创建顺序流或者是并行流。
并行流是通过多线程的方式来执行的,它能够充分发挥多核 CPU 的优势来提升性能。本文在最后再来介绍并行流,我们先讨论顺序流:
List<String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList.stream() // 创建流
.filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
.map(String::toUpperCase) // 转换成大写
.sorted() // 排序
.forEach(System.out::println); // for 循环打印
在集合上调用stream()
方法会返回一个普通的 Stream 流。但是, 您大可不必刻意地创建一个集合,再通过集合来获取 Stream 流,您还可以通过如下这种方式:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1
例如上面这样,我们可以通过 Stream.of()
从一堆对象中创建 Stream 流。
除了常规对象流之外,Java 8还附带了一些特殊类型的流,用于处理原始数据类型int
,long
以及double
。说道这里,你可能已经猜到了它们就是IntStream
,LongStream
还有DoubleStream
。
其中,IntStreams.range()
方法还可以被用来取代常规的 for
循环
IntStream.range(1,4)
.mapToObj(i -> "a"+i)
.forEach(System.out::println);
上面这些原始类型流的工作方式与常规对象流基本是一样的,但还是略微存在一些区别:
- 原始类型流使用其独有的函数式接口,例如
IntFunction
代替Function
,IntPredicate
代替Predicate
。 - 原始类型流支持额外的终端聚合操作,
sum()
以及average()
,如下所示:
Arrays.stream(new int[]{1,2,3,4,5,6,7,8,9,10})
.map(i -> i*2+1).average()
.ifPresent(System.out::println);
但是,偶尔我们也有这种需求,需要将常规对象流转换为原始类型流,这个时候,中间操作 mapToInt()
,mapToLong()
以及mapToDouble
就派上用场了:
//将对象流转换成原始类型流
Stream.of("s1","s2","s3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println);
如果说,您需要将原始类型流装换成对象流,您可以使用 mapToObj()
来达到目的:
IntStream.range(1,4)
.mapToObj(i -> "a"+i)
.forEach(System.out::println);
下面是一个组合示例,我们将双精度流首先转换成 int
类型流,然后再将其装换成对象流:
Stream.of(1.0,2.0,3.0)
.mapToInt(Double::intValue)//将double类型转换成int
.mapToObj(i -> "a" + i)//将int类型转换成Object
.forEach(System.out::println);//依次输出
三、Stream 流的处理顺序
在讨论处理顺序之前,您需要明确一点,那就是中间操作的有个重要特性 —— 延迟性。观察下面这个没有终端操作的示例代码:
//延迟执行
Stream.of("a1","a2","a3","a4").filter(s -> {
System.out.println(s);
return true;
});
原因是:当且仅当存在终端操作时,中间操作操作才会被执行。
Stream.of("a1","a2","a3","a4").filter(s -> {
System.out.println("filter:" + s);
return true;
}).forEach(s -> System.out.println("foreach:" + s));
四、中间操作顺序这么重要?
下面的例子由两个中间操作map
和filter
,以及一个终端操作forEach
组成。让我们再来看看这些操作是如何执行的:
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A"); // 过滤出以 A 为前缀的元素
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
如果我们改变中间操作的顺序,将filter
移动到链头的最开始,就可以大大减少实际的执行次数:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s)
return s.startsWith("a"); // 过滤出以 a 为前缀的元素
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase(); // 转大写
})
.forEach(s -> System.out.println("forEach: " + s)); // for 循环输出
五、数据流复用问题
Java8 Stream 流是不能被复用的,一旦你调用任何终端操作,流就会关闭:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true); // ok
stream.noneMatch(s -> true); // exception
为了克服这个限制,我们必须为我们想要执行的每个终端操作创建一个新的流链,例如,我们可以通过 Supplier
来包装一下流,通过 get()
方法来构建一个新的 Stream
流,如下所示:
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok