距离 java 8 发布,都快四年了。现在回过头来,系统地整理一下零零散散的知识点。
本章是对Java 8 中新特性的一些总结。原文链接
接口改进
现在我们可以在接口中定义静态方法了。比如,在 java.util.Comparator 中就定义了静态方法 naturalOrder:
public static <T extends Comparable<? super T>>
Comparator<T> naturalOrder() {
return (Comparator<T>)
Comparators.NaturalOrderComparator.INSTANCE;
}
另外,现在在接口中可以定义 default 方法。例如, java.lang.Iterable 中的 forEach 方法:
public default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
在过去,这基本上是不可能的。
在Java 8 中,大量的 default 方法被添加进核心的 JDK 接口中,后面我会进一步讨论他们。
为什么不同使用 default 方法重写 equals 、hashCode、toString 方法?
接口中并不能通过默认实现(default implementation)的方式重写Object类中的任何方法。这就意味着不能通过这种方式重写equals 、 hashCode 以及 toString方法。
一开始看起来这有点奇怪,Brian Goetz在 Lambda项目中的邮件列出了四个冗长的理由,这里我只讲一个,因为这一个理由足够说服我:
所有接口实例本身都是Object,这使得它原本就有关于equals/hashCode/toString 等方法的 non-default 实现,因此,再提供一个default版本的实现就显得毫无用户。
函数接口( Functional interfaces)
在 Java 8 中引入的一个核心概念就是函数接口。一个接口是函数接口,就意味着,它有且仅有一个抽象方法。
正如 java.lang.Runnable 就是一个函数接口。唯一的抽象方法就是 run 方法。
接口中的 default 方法并不是抽象的,所有函数接口中可以定义任意多的 default 方法。
一个新的注解 @FunctionalInterface 也被引入了,用于标注函数接口。类似于@Override 注解,它声明这个接口是一个函数接口,如果不是,它会拒绝编译。
lamdbas
关于函数接口,一个非常有价值的性质在于,它可以被通过 lambdas表达式的方式进行实例化。下面将给出一些例子:
在左边使用逗号分隔的具有指定类型的输入列表,在右边是带有返回值的代码块:
(int x, int y) -> { return x + y; }
左边是可以推断出类型的以逗号分隔的输入列表,右边是返回值:
(x, y) -> x + y
左边是可以推断出类型的单个参数,右边是返回值
x-> x*x
没有输入,但是有返回值:
()->x
左边是可以推断出类型的单个参数,右边是代码块,但是没有返回值:
x->{System.out.println(x);}
静态方法引用:
String::valueOf
非静态方法引用:
Object::toString
捕获方法引用(capturing method reference):
x::toString
构造器引用:
ArrayList::new
事实上,你可以把方法应用看做是lamdba的速记形式。
方法引用和lambda表达式是等价的。
String::valueOf 等价于 x->String.valueOf(x);
Object::toString 等价于 x->x.toString();
x::toString 等价于 ()->x.toString();
ArrayList::new 等价于 ()-> new ArrayList();
当然,方法是可以被重载的。对于同一个方法名,类中可能有多个不同参数的方法。一个 ArrayList::new 可以指向它三个构造器中的任意一个,具体的指向,取决于到底使用哪个函数接口。
lambda表达式和函数接口在“形状”是匹配的。也就是说,它们的输入列表、输出列表以及声明的检查期异常都是一样的。
比如说
Comparator<String> c = (a, b) -> Integer.compare(a.length(),
b.length());
Comparator接口中的compare方法有两个 String 类型的输入,返回一个 int 值,这和 lambda 表达式的左右输入、返回是一致的。
在比如说:
Runnable r = () -> { System.out.println("Running!"); }
Runnable 中的 run 方法,没有输入,没做返回这和它的 lambda表达式也应该是一致的。
捕获式和非捕获式 lambda
所谓捕获,是指 lambda表达式中访问了在 lambda 函数体非静态的变量或者对象。举个栗子,下面 lambda 捕获了变量x:
int x = 5;
return y -> x + y;
为了使这种 lambda 声明变得合理,它所捕获的变量必须是“有效的 final”类型。即,要么被标记为 final,或者在之后不可以被赋值。
非捕获的 lambda 通常会比捕获 lambda 效率高,虽然并没有任何证据指明这一点(据我所知)。(译注:前者通常比后者的依赖少)。比如,下面的 isFunActivity 依赖于 this,因为 isFun 是对象方法。
class FunDetector {
private boolean isFun(Activity activity) { ... }
public Predicate<Activity> isFunAcitivity() {
return activity -> isFun(activity);
}
}
lambda 不能做什么
有些特性,是 lambda 并没有提供。
非 final 变量的捕获 如果外部变量会赋新值,那就不应该被用在 lambda 中。当然,final 修饰不是必须的,但是肯定不能再 lambda 之后修改,否则代码不会被编译。
int count = 0;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
count++; // 错误:不能修改 count
});
异常透明度 如果一个编译期异常被 lambda 抛出来了,那么,它所对应的函数接口必须声明了这个异常可以被抛出来,否则,异常是不会向上传播的。下面这段代码不会被编译:
void appendAll(Iterable<String> values, Appendable out)
throws IOException { // doesn't help with the error
values.forEach(s -> {
out.append(s); //错误:无法抛出IO异常,因为它所对应的Consumer.accept 没有允许,所以,forEach是不会收到抛出的异常的
});
}
控制流(break,return) 在上面的 forEach 栗子中,如果想在 lambda 中返回true,并停止循环,这样是不行的
final String secret = "foo";
boolean containsSecret(Iterable<String> values) {
values.forEach(s -> {
if (secret.equals(s)) {
??? // want to end the loop and return true, but can't
}
});
}
为什么抽象方法不能通过 lambda 实例化
在一个抽象类中,哪怕只有一个抽象方法,都不能使用 lambda 进行实例化。
两个原因,其一是,这么做,隐藏了构造方法的执行;另外一个原因,是 lambda 未来的优化方向。以后, lambda 可能不需要被计算进一个实例化对象中。
java.util.function
这里有一些通用的函数接口被广泛使用,这些接口都是新增的。这里列举一些:
- Function<T,R> - T是输入,R作为输出
- Predicate<T> - T作为输入,输出boolean
- Consumer<T> - T作为输入,执行一些动作,但是没有输出
- Supplier<T> - 没有输入,返回 T
- BinaryOperator<T> - 由两个T作为输入,返回一个T。该接口对于“reduce”操作很有用。
出于性能考虑,在使用基础类型作为输入输出时,为了避免装包解包,会提供专用的函数接口,比如: - intConsumer - 使用一个int类型作为输入,执行一些操作,而不做返回。
java.util.stream
新的 java.util.stream 包提供了工具集以支持面向函数的编程。通常,我们会从一个 collection 集合中获取 stream:
Stream<T> stream = collection.stream();
stream 就像一个 iterator.类比水流,一个 stream 只能被遍历一次,一次就用完了。 Stream 可能是无线流。
stream 可以是串行的或者并行的 。串行流执行在一个线程中,并行流执行在多个线程中。
这里,给出一些实例:
int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
stream 提供了丰富的 API 来转换流和执行动作。这些操作包括:中间操作(intermediate)和终止操作(terminal)。
- 中间操作:一个中间操作会允许在流的基础上做进一步操作,然后接着返回这个流。就像上面的 filter 和 map,就是中间操作,它们返回当前 stream,以允许操作链上的处理继续进行。
- 终止操作:一个终止操作意味在在这个stream会最后被调用。一旦终止操作被调用,stream 就被“消费”完了,不能再被使用。上面的 sum 方法就是一个终止操作。
通常,处理 stream 会按照下面的步骤进行调用:
- 从数据源获得一个 stream;
- 执行一个或者多个中间操作;
- 执行一个终止操作。
这里有两个通用属性需要考虑:
- 状态(Stateful ):中间操作分为有状态的和无状态的。通常有状态的要比无状态的实现代价更高。(译注:有状态意味着要解决更多依赖问题)
- 短路(short-circuiting):终止操作分为短路和非短路的。所谓短路,意味着允许流处理提前结束,而不需要检查所有元素。
中间操作:
- filter : 排除所有不匹配 Predicate 的元素;
- map :通过Function 对所有元素进行一一映射;
- flatMap :通过flatMap,可以使用一个Stream替换某个值,然后把所有的Stream合并起来。map操作将值替换一个新的值,但是有时候我们想替换为一个新的Stream对象,更常见的是把多个Stream和合并为一个Stream.下面假设order是购物清单
//函数原型
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
orders.flatMap(order -> order.getLineItems().stream())...
- peek :对当当前流中的每一个元素做一些操作。
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
- distinct :通过 .equals 行为排除所有相同的元素。这是一个有状态的操作。
- sorted :通过输入进去的 Comparator,确保流中的元素是有序的。这是一个有状态的操作。
- limit :确保序列操作只能访问查阅到最大的数量。这是一个有状态的、短路操作。
- skip :跳过前面 n 个元素。
终止操作:
- forEach : 遍历 stream 中的每个元素,进行操作。
- toArray :把 stream 中的元素转为数组。
- reduce : 通过 BinaryOperator 将 stream 中的元素聚合成 1 个;(译注:这种聚合是针对不可变类型数据的聚合,将问题变成了f(g(x,y))的问题,这里,g(x,y)即是上面的BinaryOperator。下面我以代码举个例子)
Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum +item);
- collect :将流中的元素聚合到一些容器中,比如Collection或者Map(译注:以下示例来自官方文档)。
List<String> asList = stringStream.collect(Collectors.toList());
Map<String, List<Person>> peopleByCity
= personStream.collect(Collectors.groupingBy(Person::getCity));
- min :根据 Comparator 找到流中的最小元素。
- max :根据 Comparator 找到流中的最大元素。
- count :算出流中元素的个数;
- anyMatch :根据 Predicate 查找时候至少有一个元素是匹配的。这是一个短路操作,意味着一旦找到一个,就停止寻找过程。
- allMatch :根据 Predicate 判断是否所有元素都满足匹配,这也是一个短路操作,一旦发现有一个不匹配,就停止这个过程。
- noneMatch :查找是否没有元素匹配。
- findFirst :查找流中的第一个元素,这是一个短路操作。
- findAny :查找一些元素。通常它会比 findFirst 效率高些。 这是一个短路操作。
这些中间操作,都是懒加载的。也就是说,只有终止操作才能开始这个流对元素的处理过程。
Stream 会尽量做更少的工作以提升效率。它会做一些优化处理,比如当它确定元素已经是顺序时,sorted() 会被省略执行。在一些操作例如 limit(x) 或者 substream(x,y)中,stream 通常会避免执行一些不必要的 map 操作(译注:显然它没必要对流中的所有操作进行map,如果中间操作中有 map 的话)。
回到并行流(parallel stream)的概念,很重要的一点是,它将消耗更多的性能,同时,并不能保证结果的一致性。所以,在使用前,需要注意这些问题:这是排序问题吗?函数是无状态的吗?数据量太大、操作太复杂而只能使用并行机制吗?等等等等。
小结
先到这里吧,以上,都是比较常用的功能。后面,还有些关于集合新增函数、同步API、IO这些,与我不是特别常用[捂脸],立个flag,有时间接着写。
让我觉得最好用的就是 lambda 表达式的使用。同时还有 stream 的概念,面向函数式编程,我觉得最大的优势,就是简洁、可读性高。
网友评论