设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客

新版观察:Java 8的新特性和改进总览

2013-5-2 10:12| 发布者: joejoe0332| 查看: 4491| 评论: 0|原作者: super0555, throwable, 等PM, LinuxQueen, JeromeCui, 乔学士, Sender, 叫我蝴蝶吧|来自: oschina

摘要:   这篇文章是对Java 8中即将到来的改进做一个面向开发者的综合性的总结,JDK的这一特性将会在2013年9月份发布。   在写这篇文章的时候,Java 8的开发工作仍然在紧张有序的进行中,语言特新和API仍然有可能改变 ...

捕获和非捕获的Lambda表达式

当Lambda表达式访问一个定义在Lambda表达式体外的非静态变量或者对象时,这个Lambda表达式称为“捕获的”。比如,下面这个lambda表达式捕捉了变量x:

1int x = 5; return y -> x + y;
为了保证这个lambda表达式声明是正确的,被它捕获的变量必须是“有效final”的。所以要么它们需要用final修饰符号标记,要么保证它们在赋值后不能被改变。

Lambda表达式是否是捕获的和性能悄然相关。一个非不捕获的lambda通常比捕获的更高效,虽然这一点没有书面的规范说明(据我所知),而且也不能为了程序的正确性指望它做什么,非捕获的lambda只需要计算一次. 然后每次使用到它都会返回一个唯一的实例。而捕获的lambda表达式每次使用时都需要重新计算一次,而且从目前实现来看,它很像实例化一个匿名内部类的实例。

lambdas不做的事

你应该记住,有一些lambdas不提供的特性。为了Java 8它们被考虑到了,但是没有被包括进去,由于简化以及时间限制的原因。

Non-final* 变量捕获 - 如果一个变量被赋予新的数值,它将不能被用于lambda之中。"final"关键字不是必需的,但变量必须是“有效final”的(前面讨论过)。这个代码不会被编译:

1int count = 0;
2List<String> strings = Arrays.asList("a", "b", "c");
3strings.forEach(s -> {
4    count++; // error: can't modify the value of count });

例外的透明度 - 如果一个已检测的例外可能从lambda内部抛出,功能性的接口也必须声明已检测例外可以被抛出。这种例外不会散布到其包含的方法。这个代码不会被编译:

1void appendAll(Iterable<String> values, Appendable out) throws IOException { // doesn't help with the error values.forEach(s -> {
2        out.append(s); // error: can't throw IOException here // Consumer.accept(T) doesn't allow it });
3}

有绕过这个的办法,你能定义自己的功能性接口,扩展Consumer的同时通过像RuntimeException之类抛出 IOException。我试图用代码写出来,但发现它令人困惑是否值得。

控制流程 (break, early return) -在上面的 forEach例子中,传统的继续方式有可能通过在lambda之内放置 "return;"来实现。但是,没有办法中断循环或者从lambda中通过包含方法的结果返回一个数值。例如:

1final String secret = "foo"; boolean containsSecret(Iterable<String> values) {
2    values.forEach(s -> { if (secret.equals(s)) {
3            ??? // want to end the loop and return true, but can't }
4    });
5}
进一步阅读关于这些问题的资料,看看这篇Brian Goetz写的说明:在 Block<T>中响应“已验证例外”

为什么抽象类不能通过利用lambda实例化

抽象类,哪怕只声明了一个抽象方法,也不能使用lambda来实例化。

下面有两个类 Ordering 和 CacheLoader的例子,都带有一个抽象方法,摘自于Guava 库。那岂不是很高兴能够声明它们的实例,像这样使用lambda表达式?

Ordering<String> order = (a, b) -> ...;

1CacheLoader<String, String> loader = (key) -> ...;

这样做引发的最常见的争论就是会增加阅读lambda的难度。以这种方式实例化一段抽象类将导致隐藏代码的执行:抽象类的构造方法。

另一个原因是,它抛出了lambda表达式可能的优化。在未来,它可能是这种情况,lambda表达式都不会计算到对象实例。放任用户用lambda来声明抽象类将妨碍像这样的优化。

外,有一个简单地解决方法。事实上,上述两个摘自Guava 库的实例类已经证明了这种方法。增加工厂方法将lambda转换成实例。

1Ordering<String> order = Ordering.from((a, b) -> ...);
2CacheLoader<String, String> loader = CacheLoader.from((key) -> ...);

要深入阅读,请参看由 Brian Goetz所做的说明: response to "Allow lambdas to implement abstract classes"。  

java.util.function

包概要:java.util.function

作为Comparator 和Runnable早期的证明,在JDK中已经定义的接口恰巧作为函数接口而与lambdas表达式兼容。同样方式可以在你自己的代码中定义任何函数接口或第三方库。

但有特定形式的函数接口,且广泛的,通用的,在之前的JD卡中并不存在。大量的接口被添加到新的java.util.function 包中。下面是其中的一些:

  • Function<T, R> -T作为输入,返回的R作为输出
  • Predicate<T> -T作为输入,返回的boolean值作为输出
  • Consumer<T> - T作为输入,执行某种动作但没有返回值
  • Supplier<T> - 没有任何输入,返回T
  • BinaryOperator<T> -两个T作为输入,返回一个T作为输出,对于“reduce”操作很有用

这些最原始的特征同样存在。他们以int,long和double的方式提供。例如:

  • IntConsumer -以int作为输入,执行某种动作,没有返回值

这里存在性能上的一些原因,主要释在输入或输出的时候避免装箱和拆箱操作。

java.util.stream

包汇总: java.util.stream

新的java.util.stream包提供了“支持在流上的函数式风格的值操作”(引用javadoc)的工具。可能活动一个流的最常见方法是从一个collection获取:

1Stream<T> stream = collection.stream();

一个流就像一个地带器。这些值“流过”(模拟水流)然后他们离开。一个流可以只被遍历一次,然后被丢弃。流也可以无限使用。

流能够是 串行的 或者 并行的。 它们可以使用其中一种方式开始,然后切换到另外的一种方式,使用stream.sequential()或stream.parallel()来达到这种切换。串行流在一个线程上连续操作。而并行流就可能一次出现在多个线程上。

所以,你想用一个流来干什么?这里是在javadoc包里给出的例子:

1int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED)
2                                  .mapToInt(b -> b.getWeight())
3                                  .sum();

注意:上面的代码使用了一个原始的流,以及一个只能用在原始流上的sum()方法。下面马上就会有更多关于原始流的细节。

流提供了流畅的API,可以进行数据转换和对结果执行某些操作。流操作既可以是“中间的”也可以是“末端的”。

  •  -中间的操作保持流打开状态,并允许后续的操作。上面例子中的filter和map方法就是中间的操作。这些操作的返回数据类型是流;它们返回当前的流以便串联更多的操作。
  • 末端的 - 末端的操作必须是对流的最终操作。当一个末端操作被调用,流被“消耗”并且不再可用。上面例子中的sum方法就是一个末端的操作。

通常,处理一个流涉及了这些步骤:

  1. 从某个源头获得一个流。
  2. 执行一个或更多的中间的操作。
  3. 执行一个末端的操作。

可能你想在一个方法中执行所有那些步骤。那样的话,你就要知道源头和流的属性,而且要可以保证它被正确的使用。你可能不想接受任意的Stream<T>实例作为你的方法的输入,因为它们可能具有你难以处理的特性,比如并行的或无限的。

有几个更普通的关于流操作的特性需要考虑:

  • 有状态的 - 有状态的操作给流增加了一些新的属性,比如元素的唯一性,或者元素的最大数量,或者保证元素以排序的方式被处理。这些典型的要比无状态的中间操作代价大。
  • 短路 - 短路操作潜在的允许对流的操作尽早停止,而不去检查所有的元素。这是对无限流的一个特殊设计的属性;如果对流的操作没有短路,那么代码可能永远也不会终止。

对每个Sttream方法这里有一些简短的,一般的描述。查阅javadoc获取更详尽的解释。下面给出了每个操作的重载形式的链接。

中间的操作:

  • filter 1 - 排除所有与断言不匹配的元素。
  • map 1 2 3 4 - 通过Function对元素执行一对一的转换。
  • flatMap 1 2 3 4 5 - 通过FlatMapper将每个元素转变为无或更多的元素。
  • peek 1 - 对每个遇到的元素执行一些操作。主要对调试很有用。
  • distinct 1 - 根据.equals行为排除所有重复的元素。这是一个有状态的操作。
  • sorted 1 2 - 确保流中的元素在后续的操作中,按照比较器(Comparator)决定的顺序访问。这是一个有状态的操作。
  • limit 1 - 保证后续的操作所能看到的最大数量的元素。这是一个有状态的短路的操作。
  • substream 1 2 - 确保后续的操作只能看到一个范围的(根据index)元素。像不能用于流的String.substring一样。也有两种形式,一种有一个开始索引,一种有一个结束索引。二者都是有状态的操作,有一个结束索引的形式也是一个短路的操作。

末端的操作:

  • forEach 1 - 对流中的每个元素执行一些操作。
  • toArray 1 2 - 将流中的元素倾倒入一个数组。
  • reduce 1 2 3 - 通过一个二进制操作将流中的元素合并到一起。
  • collect 1 2 - 将流中的元素倾倒入某些容器,例如一个Collection或Map.
  • min 1 - 根据一个比较器找到流中元素的最小值。
  • max 1 -根据一个比较器找到流中元素的最大值。
  • count 1 - 计算流中元素的数量。
  • anyMatch 1 - 判断流中是否至少有一个元素匹配断言。这是一个短路的操作。
  • allMatch 1 - 判断流中是否每一个元素都匹配断言。这是一个短路的操作。
  • noneMatch 1 - 判断流中是否没有一个元素匹配断言。这是一个短路的操作。
  • findFirst 1 - 查找流中的第一个元素。这是一个短路的操作。
  • findAny 1 - 查找流中的任意元素,可能对某些流要比findFirst代价低。这是一个短路的操作。

如 javadocs中提到的 , 中间的操作是延迟的(lazy)。只有末端的操作会立即开始流中元素的处理。在那个时刻,不管包含了多少中间的操作,元素会在一个传递中处理(通常,但并不总是)。(有状态的操作如sorted() 和distinct()可能需要对元素的二次传送。) 

流试图尽可能做很少的工作。有一些细微优化,如当可以判定元素已经有序的时候,省略一个sorted()操作。在包含limit(x) 或 substream(x,y)的操作中,有些时候对一些不会决定结果的元素,流可以避免执行中间的map操作。在这里我不准备实现公平判断;它通过许多细微的但却很重要的方法表现得很聪明,而且它仍在进步。

回到并行流的概念,重要的是要意识到并行不是毫无代价的。从性能的立场它不是无代价的,你不能简单的将顺序流替换为并行流,且不做进 一步思考就期望得到相同的结果。在你能(或者应该)并行化一个流以前,需要考虑很多特性,关于流、它的操作以及数据的目标方面。例如:访问顺序确实对我有 影响吗?我的函数是无状态的吗?我的流有足够大,并且我的操作有足够复杂,这些能使得并行化是值得的吗?

有针对int,long和double的专业原始的Stream版本:

可以在众多函数中,通过专业原始的map和flatMap函数,在一个stream对象与一个原始stream对象之间来回转换。给几个虚设例子:

1List<String> strings = Arrays.asList("a", "b", "c");
2strings.stream() //
3Stream<String> .mapToInt(String::length) // IntStream .longs() //
4LongStream .mapToDouble(x -> x / 10.0) // DoubleStream .boxed() //
5Stream<Double> .mapToLong(x -> 1L) // LongStream .mapToObj(x -> "") //
6Stream<String> ...

原始的stream也为获得关于stream的基础数据统计提供方法,那些stream是指作为数据结构的。你可以发现count, sum, min, max, 以及元素平均值全部是来自于一个终端的操作。

原始类型的剩余部分没有原始版本,因为这需要一个不可接受的JDK数量的膨胀。IntStream, LongStream, 和 DoubleStream被认为非常有用应当被包含进去,其他的数字型原始stream可以由这三个通过扩展的原始转换来表示。

在flatMap操作中使用的 FlatMapper 接口是具有一个抽象方法的功能性接口:

1void flattenInto(T element, Consumer<U> sink);

在一个flatMap操作的上下文中,stream为你提供element和 sink,然后你定义该用element 和 sink做什么。element是指在stream中的当前元素,而sink代表当flatMap操作结束之后在stream中应该显示些什么。例如:

1Set<Color> colors = ...;
2List<Person> people = ...;
3Stream<Color> stream = people.stream().flatMap(
4    (Person person, Consumer<Color> sink) -> { // Map each person to the colors they like. for (Color color : colors) { if (person.likesColor(color)) {
5                sink.accept(color);
6            }
7        }
8    });

注意上面lambda中的参数类型是指定的。在大多数其它上下文中,你可以不需要指定类型,但这里由于FlatMapper的自然特性,编译器需要你帮助判定类型。如果你在使用flatMap又迷惑于它为什么不编译,可能是因为你没有指定类型。

最令人感到困惑,复杂而且有用的终端stream操作之一是collect。它引入了一个称为Collector的新的非功能性接口。这个接口有些难理解,但幸运的是有一个Collectors工具类可用来产生所有类型的有用的Collectors。例如:

1List<String> strings = values.stream()
2                             .filter(...)
3                             .map(...)
4                             .collect(Collectors.toList());

如果你想将你的stream元素放进一个Collection,Map或String,那么Collectors可能具有你需要的。在javadoc中浏览那个类绝对是值得的。


酷毙

雷人

鲜花
1

鸡蛋

漂亮

刚表态过的朋友 (1 人)

  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部