免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1638 | 回复: 0
打印 上一主题 下一主题

Java 8 的新特性和改进总览 [复制链接]

论坛徽章:
49
15-16赛季CBA联赛之福建
日期:2016-06-22 16:22:002015年亚洲杯之中国
日期:2015-01-23 16:25:12丑牛
日期:2015-01-20 09:39:23未羊
日期:2015-01-14 23:55:57巳蛇
日期:2015-01-06 18:21:36双鱼座
日期:2015-01-02 22:04:33午马
日期:2014-11-25 09:58:35辰龙
日期:2014-11-18 10:40:07寅虎
日期:2014-11-13 22:47:15申猴
日期:2014-10-22 15:29:50摩羯座
日期:2014-08-27 10:49:43辰龙
日期:2014-08-21 10:47:58
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2013-04-29 14:27 |只看该作者 |倒序浏览
这篇文章是对Java 8中即将到来的改进做一个面向开发者的综合性的总结,JDK的这一特性将会在2013年9月份发布。

在写这篇文章的时候,Java 8的开发工作仍然在紧张有序的进行中,语言特新和API仍然有可能改变,我会尽我最大的努力保持这份文档跟得到Java 8的改动。


Java 8的预览版,也就是"roject Lambda”,现在可以从java.net下载到。


我使用了IntelliJ的预览版做我的IDE,在我看来他是目前支持java 8特性最好的一个IDE,你可以从这里下载到.


由于我没有找到Oracle发布的Java 8的官方文档,所以目前Java 8的文档还只有本地版本,等Oracle公开文档的时候,我将会重新链接到官方文档。


接口改善


现在接口里已经完全可以定义静态方法了. 举一个比较普遍的例子就是在java类库中, 对于一些接口如Foo, 都会有一个有静态方法的工具类Foos 来生成或者配合Foo对象实例来使用. 既然静态方法可以存在于接口当中, 那么大多数情况下 Foos工具类完全可以使用接口中的公共方法来代理 (或者将Foos置成package-private).


除此之外更重要的就是, Java 8中接口可以定义默认的方法了.举个例子,一个for-each循环的方法就可以加入到java.lang.Iterable中:


public default void forEach(Consumer action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }


在过去,java类库的接口中添加方法基本上是不可能的. 在接口中添加方法意味着破坏了实现了这个接口的代码. 但是现在, 只要能够提供一个正确明智的默认的方法的实现, java类库的维护者就可以在接口中添加方法.


Java 8中, 大量的默认方法已经被添加到核心的JDK接口中了. 稍候我会详细介绍它们.


为什么不能用默认方法来重载equals,hashCode和toString?


接口不能提供对Object类的任何方法的默认实现。特别是,这意味着从接口里不能提供对equals,hashCode或toString的默认实现。


这刚看起来挺奇怪的,但考虑到一些接口实际上是在文档里定义他们的equals行为的。List接口就是一个例子了。因此,为什么不允许这样呢?


Brian Goetz在这个问题上的冗长的回复里给出了4个原因。我这里只说其中一个,因为那个已经足够说服我了:


它会变得更困难来推导什么时候该调用默认的方法。现在它变得很简单了:如果一个类实现了一个方法,那总是优先于默认的实现的。一旦所有接口的实例都是Object的子类,所有接口实例都已经有对 equals/hashCode/toString的非默认实现。因此,一个在接口上这些的默认版本都是没用的,它也不会被编译。


要看更多的话,看下由Brian Goetz写的解释: 对“允许默认方法来重载Object的方法”的回复


函数式接口


Java 8 引入的一个核心概念是函数式接口。如果一个接口定义个唯一一个抽象方法,那么这个接口就成为函数式接口。比如,java.lang.Runnable就是一个函数式接口,因为它只顶一个一个抽象方法:


public abstract void run();


留意到“abstract”修饰词在这里是隐含的,因为这个方法缺少方法体。为了表示一个函数式接口,并非想这段代码一样一定需要“abstract”关键字。


默认方法不是abstract的,所以一个函数式接口里可以定义任意多的默认方法,这取决于你。


同时,引入了一个新的Annotation:@FunctionalInterface。可以把他它放在一个接口前,表示这个接口是一个函数式接口。加上它的接口不会被编译,除非你设法把它变成一个函数式接口。它有点像@Override,都是声明了一种使用意图,避免你把它用错。


Lambdas


一个函数式接口非常有价值的属性就是他们能够用lambdas来实例化。这里有一些lambdas的例子:


左边是指定类型的逗号分割的输入列表,右边是带有return的代码块:


(int x, int y) -> { return x + y; }


左边是推导类型的逗号分割的输入列表,右边是返回值:


(x, y) -> x + y


左边是推导类型的单一参数,右边是一个返回值:


x -> x * x


左边没有输入 (官方名称: "burger arrow"),在右边返回一个值:


() -> x


左边是推导类型的单一参数,右边是没返回值的代码块(返回void):


x -> { System.out.println(x); }


静态方法引用:


String::valueOf


非静态方法引用:


Object::toString


继承的函数引用:


x::toString


构造函数引用:


ArrayList::new


你可以想出一些函数引用格式作为其他lambda格式的简写。


方法引用


等价的lambda表达式


String::valueOf


x -> String.valueOf(x)


Object::toString


x -> x.toString()
                    
x::toString

() -> x.toString()


ArrayList::new


() -> new ArrayList()


当然,在Java里方法能被重载。类可以有多个同名但不同参数的方法。这同样对构造方法有效。ArrayList::new能够指向它的3个构造方法中任何一个。决定使用哪个方法是根据在使用的函数式接口。


一个lambda和给定的函数式接口在“外型”匹配的时候兼容。通过“外型”,我指向输入、输出的类型和声明检查异常。


给出两个具体有效的例子:


Comparator c = (a, b) -> Integer.compare(a.length(), b.length());


一个Comparator的compare方法需要输入两个阐述,然后返回一个int。这和lambda右侧的一致,因此这个任务是有效的。


Runnable r = () -> { System.out.println("Running!"); }


一个Runnable的run方法不需要参数也不会返回值。这和lambda右侧一致,所以任务有效。


在抽象方法的签名里的受检查异常(如果存在)也很重要。如果函数式接口在它的签名里声明了异常,lambda只能抛出受检查异常。


捕获和非捕获的Lambda表达式


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


int x = 5; return y -> x + y;


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


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

lambdas不做的事


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

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


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

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


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


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

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


final String secret = "foo"; boolean containsSecret(Iterable values) { values.forEach(s -> { if (secret.equals(s)) { ??? // want to end the loop and return true, but can't } }); }


进一步阅读关于这些问题的资料,看看这篇Brian Goetz写的说明:在 Block中响应“已验证例外”

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


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


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

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


CacheLoader loader = (key) -> ...;


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


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


此外,有一个简单地解决方法。事实上,上述两个摘自Guava 库的实例类已经证明了这种方法。增加工厂方法将lambda转换成实例。
            
Ordering order = Ordering.from((a, b) -> ...); CacheLoader 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作为输出
  • Predicate -T作为输入,返回的boolean值作为输出
  • Consumer - T作为输入,执行某种动作但没有返回值
  • Supplier - 没有任何输入,返回T
  • BinaryOperator -两个T作为输入,返回一个T作为输出,对于“reduce”操作很有用
这些最原始的特征同样存在。他们以int,long和double的方式提供。例如:


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

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


java.util.stream


包汇总: java.util.stream


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


Stream stream = collection.stream();


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


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

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


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


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


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


            
  • -中间的操作保持流打开状态,并允许后续的操作。上面例子中的filter和map方法就是中间的操作。这些操作的返回数据类型是流;它们返回当前的流以便串联更多的操作。
  • 末端的 - 末端的操作必须是对流的最终操作。当一个末端操作被调用,流被“消耗”并且不再可用。上面例子中的sum方法就是一个末端的操作。
通常,处理一个流涉及了这些步骤:
     从某个源头获得一个流。
               执行一个或更多的中间的操作。
               执行一个末端的操作。
            
可能你想在一个方法中执行所有那些步骤。那样的话,你就要知道源头和流的属性,而且要可以保证它被正确的使用。你可能不想接受任意的Stream实例作为你的方法的输入,因为它们可能具有你难以处理的特性,比如并行的或无限的。

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


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


本文来自ChinaUnix新闻频道,如果查看原文请点:http://news.chinaunix.net/opensource/2013/0429/2743526.shtml
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP