1.简介javascript
毫无疑问,Java 8是自Java 5(2004年)发布以来Java语言最大的一次版本升级,Java 8带来了不少的新特性,好比编译器、类库、开发工具和JVM(Java虚拟机)。在这篇教程中咱们将会学习这些新特性,并经过真实例子演示说明它们适用的场景。html
本教程由下面几部分组成,它们分别涉及到Java平台某一特定方面的内容:java
2.Java的新特性程序员
整体来讲,Java 8是一个大的版本升级。有人可能会说,Java 8的新特性很是使人期待,可是也要花费大量的时间去学习。这一节咱们会讲到这些新特性。spring
2.1 Lambda表达式和函数式接口express
Lambda表达式(也叫作闭包)是Java 8中最大的也是期待已久的变化。它容许咱们将一个函数看成方法的参数(传递函数),或者说把代码看成数据,这是每一个函数式编程者熟悉的概念。不少基于JVM平台的语言一开始就支持Lambda表达式,可是Java程序员没有选择,只能使用匿名内部类来替代Lambda表达式。apache
Lambda表达式的设计被讨论了好久,并且花费了不少的功夫来交流。不过最后取得了一个折中的办法,获得了一个新的简明而且紧凑的Lambda表达式结构。最简单的Lambda表达式能够用逗号分隔的参数列表、->符号和功能语句块来表示。示例以下:编程
1api |
|
请注意到编译器会根据上下文来推测参数的类型,或者你也能够显示地指定参数类型,只须要将类型包在括号里。举个例子:
1 |
|
若是Lambda的功能语句块太复杂,咱们能够用大括号包起来,跟普通的Java方法同样,以下:
1 2 3 |
|
Lambda表达式可能会引用类的成员或者局部变量(会被隐式地转变成final类型),下面两种写法的效果是同样的:
1 2 3 |
|
和
1 2 3 |
|
Lambda表达式可能会有返回值,编译器会根据上下文推断返回值的类型。若是lambda的语句块只有一行,不须要return关键字。下面两个写法是等价的:
1 |
|
和
1 2 3 4 |
|
语言的设计者们思考了不少如何让现有的功能和lambda表达式友好兼容。因而就有了函数接口这个概念。函数接口是一种只有一个方法的接口,像这样地,函数接口能够隐式地转换成lambda表达式。
java.lang.Runnable 和java.util.concurrent.Callable是函数接口两个最好的例子。可是在实践中,函数接口是很是脆弱的,只要有人在接口里添加多一个方法,那么这个接口就不是函数接口了,就会致使编译失败。Java 8提供了一个特殊的注解@FunctionalInterface来克服上面提到的脆弱性而且显示地代表函数接口的目的(java里全部现存的接口都已经加上了@FunctionalInterface)。让咱们看看一个简单的函数接口定义:
1 2 3 4 |
|
咱们要记住默认的方法和静态方法(下一节会具体解释)不会违反函数接口的约定,例子以下:
1 2 3 4 5 6 7 |
|
支持Lambda是Java 8最大的卖点,他有巨大的潜力吸引愈来愈多的开发人员转到这个开发平台来,而且在纯Java里提供最新的函数式编程的概念。对于更多的细节,请参考官方文档。
2.2 接口的默认方法和静态方法
Java 8增长了两个新的概念在接口声明的时候:默认和静态方法。默认方法和Trait有些相似,可是目标不同。默认方法容许咱们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。
默认方法和抽象方法的区别是抽象方法必需要被实现,默认方法不是。做为替代方式,接口能够提供一个默认的方法实现,全部这个接口的实现类都会经过继承得倒这个方法(若是有须要也能够重写这个方法),让咱们来看看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
接口Defaulable使用default关键字声明了一个默认方法notRequired(),类DefaultableImpl实现了Defaulable接口,没有对默认方法作任何修改。另一个类OverridableImpl重写类默认实现,提供了本身的实现方法。
Java 8 的另一个有意思的新特性是接口里能够声明静态方法,而且能够实现。例子以下:
1 2 3 4 5 6 |
|
下面是把接口的静态方法和默认方法放在一块儿的示例(::new 是构造方法引用,后面会有详细描述):
1 2 3 4 5 6 7 |
|
控制台的输出以下:
Default implementation
Overridden implementation
JVM平台的接口的默认方法实现是很高效的,而且方法调用的字节码指令支持默认方法。默认方法使已经存在的接口能够修改而不会影响编译的过程。java.util.Collection中添加的额外方法就是最好的例子:stream(), parallelStream(), forEach(), removeIf()
虽然默认方法很强大,可是使用以前必定要仔细考虑是否是真的须要使用默认方法,由于在层级很复杂的状况下很容易引发模糊不清甚至变异错误。更多的详细信息请参考官方文档。
2.3 方法引用
方法引用提供了一个颇有用的语义来直接访问类或者实例的已经存在的方法或者构造方法。结合Lambda表达式,方法引用使语法结构紧凑简明。不须要复杂的引用。
下面咱们用Car 这个类来作示例,Car这个类有不一样的方法定义。让咱们来看看java 8支持的4种方法引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
第一种方法引用是构造方法引用,语法是:Class::new ,对于泛型来讲语法是:Class<T >::new,请注意构造方法没有参数:
1 2 |
|
第二种方法引用是静态方法引用,语法是:Class::static_method请注意这个静态方法只支持一个类型为Car的参数。
1 |
|
第三种方法引用是类实例的方法引用,语法是:Class::method请注意方法没有参数。
1 |
|
最后一种方法引用是引用特殊类的方法,语法是:instance::method,请注意只接受Car类型的一个参数。
1 2 |
|
运行这些例子咱们将会在控制台获得以下信息(Car的实例可能会不同):
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
关于方法引用更多的示例和详细信息,请参考官方文档
2.4 重复注释
自从Java 5支持注释以来,注释变得特别受欢迎于是被普遍使用。可是有一个限制,同一个地方的不能使用同一个注释超过一次。 Java 8打破了这个规则,引入了重复注释,容许相同注释在声明使用的时候重复使用超过一次。
重复注释自己须要被@Repeatable注释。实际上,他不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是同样的。让咱们来看看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
咱们能够看到,注释Filter被@Repeatable( Filters.class )注释。Filters 只是一个容器,它持有Filter, 编译器尽力向程序员隐藏它的存在。经过这样的方式,Filterable接口能够被Filter注释两次。
另外,反射的API提供一个新方法getAnnotationsByType() 来返回重复注释的类型(请注意Filterable.class.getAnnotation( Filters.class )将会返回编译器注入的Filters实例)。
程序的输出将会是这样:
filter1
filter2
更多详细信息请参考官方文档。
2.5 更好的类型推断
Java 8在类型推断方面改进了不少,在不少状况下,编译器能够推断参数的类型,从而保持代码的整洁。让咱们看看例子:
package com.javacodegeeks.java8.type.inference;
1 2 3 4 5 6 7 8 9 10 11 |
|
这里是Value< String >的用法
1 2 3 4 5 6 7 8 |
|
参数Value.defaultValue()的类型被编译器推断出来,不须要显式地提供类型。在java 7, 相同的代码不会被编译,须要写成:Value.< String >defaultValue()
2.6 注解的扩展
Java 8扩展了注解可使用的范围,如今咱们几乎能够在全部的地方:局部变量、泛型、超类和接口实现、甚至是方法的Exception声明。一些例子以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Java 8 新增长了两个注解的程序元素类型ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER ,这两个新类型描述了可使用注解的新场合。注解处理API(Annotation Processing API)也作了一些细微的改动,来识别这些新添加的注解类型。
3.Java编译器的新特性
3.1 参数名字
很长时间以来,Java程序员想尽办法把参数名字保存在java字节码里,而且让这些参数名字在运行时可用。Java 8 终于把这个需求加入到了Java语言(使用反射API和Parameter.getName() 方法)和字节码里(使用java编译命令javac的–parameters参数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
若是你编译这个class的时候没有添加参数–parameters,运行的时候你会获得这个结果:
Parameter: arg0
编译的时候添加了–parameters参数的话,运行结果会不同:
Parameter: args
对于有经验的Maven使用者,–parameters参数能够添加到maven-compiler-plugin的配置部分:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
最新版的Eclipse Kepler SR2 提供了编译设置项,以下图所示:
Picture 1. Configuring Eclipse projects to support new Java 8 compiler –parameters argument.
额外的,有一个方便的方法Parameter.isNamePresent() 来验证参数名是否是可用。
4.Java 库的新特性
Java 8 新添加了不少类,而且扩展了不少现有的类来更好地支持现代并发、函数式编程、日期\时间等等。
4.1 Optional
著名的NullPointerException 是引发系统失败最多见的缘由。好久之前Google Guava项目引入了Optional做为解决空指针异常的一种方式,不同意代码被null检查的代码污染,指望程序员写整洁的代码。受Google Guava的鼓励,Optional 如今是Java 8库的一部分。
Optional 只是一个容器,它能够保存一些类型的值或者null。它提供不少有用的方法,因此没有理由不显式地检查null。请参照java 8的文档查看详细信息。
让咱们看看两个Optional 用法的小例子:一个是容许为空的值,另一个是不容许为空的值。
1 2 3 4 |
|
若是Optional实例有非空的值,方法 isPresent() 返回true不然返回false。方法orElseGet提供了回退机制,当Optional的值为空时接受一个方法返回默认值。map()方法转化Optional当前的值而且返回一个新的Optional实例。orElse方法和orElseGet相似,可是它不接受一个方法,而是接受一个默认值。上面代码运行结果以下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
让咱们大概看看另一个例子。
1 2 3 4 5 |
|
输出以下:
First Name is set? true
First Name: Tom
Hey Tom!
更多详细信息请参考官方文档。
4.2 Stream
新增长的Stream API (java.util.stream)引入了在Java里能够工做的函数式编程。这是目前为止对java库最大的一次功能添加,但愿程序员经过编写有效、整洁和简明的代码,可以大大提升生产率。
Stream API让集合处理简化了不少(咱们后面会看到不只限于Java集合类)。让咱们从一个简单的类Task开始来看看Stream的用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值能够为OPEN或CLOSED的状态.让咱们引入一个Task的小集合做为演示例子:
1 2 3 4 5 |
|
第一个问题是全部的开放的Task的点数是多少?在java 8 以前,一般的作法是用foreach迭代。可是Java8里头咱们会用Stream。Stream是多个元素的序列,支持串行和并行操做。
1 2 3 4 5 6 7 8 |
|
控制台的输出将会是:
Total points: 18
上面代码执行的流程是这样的,首先Task集合会被转化为Stream表示,而后filter操做会过滤掉全部关闭的Task,接下来使用Task::getPoints 方法取得每一个Task实例的点数,mapToInt方法会把Task Stream转换成Integer Stream,最后使用Sum方法将全部的点数加起来获得最终的结果。
在咱们看下一个例子以前,咱们要记住一些关于Stream的说明。Stream操做被分为中间操做和终点操做。
中间操做返回一个新的Stream。这些中间操做是延迟的,执行一个中间操做好比filter实际上不会真的作过滤操做,而是建立一个新的Stream,当这个新的Stream被遍历的时候,它里头会包含有原来Stream里符合过滤条件的元素。
终点操做好比说forEach或者sum会遍历Stream从而产生最终结果或附带结果。终点操做执行完以后,Stream管道就被消费完了,再也不可用。在几乎全部的状况下,终点操做都是即时完成对数据的遍历操做。
Stream的另一个价值是Stream创造性地支持并行处理。让咱们看看下面这个例子,这个例子把全部task的点数加起来。
1 2 3 4 5 6 7 8 |
|
这个例子跟上面那个很是像,除了这个例子里使用了parallel()方法 而且计算最终结果的时候使用了reduce方法。
输出以下:
Total points (all tasks): 26.0
常常会有这个一个需求:咱们须要按照某种准则来对集合中的元素进行分组。Stream也能够处理这样的需求,下面是一个例子:
1 2 3 4 5 |
|
控制台的输出以下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
让咱们来计算整个集合中每一个task分数(或权重)的平均值来结束task的例子。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
控制台输出以下:
[19%, 50%, 30%]
最后,就像前面提到的,Stream API不只仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操做也很适合用Stream API来处理。下面用一个例子来应证这一点。
1 2 3 4 |
|
Stream的方法onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行。
Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。
4.3日期时间API(JSR310)
Java 8引入了新的日期时间API(JSR 310)改进了日期时间的管理。日期和时间管理一直是Java开发人员最痛苦的问题。java.util.Date和后来的java.util.Calendar一点也没有改变这个状况(甚至让人们更加迷茫)。
由于上面这些缘由,产生了Joda-Time ,能够替换Java的日期时间API。Joda-Time深入影响了 Java 8新的日期时间API,Java 8吸取了Joda-Time 的精华。新的java.time包包含了全部关于日期、时间、日期时间、时区、Instant(跟日期相似但精确到纳秒)、duration(持续时间)和时钟操做的类。设计这些API的时候很认真地考虑了这些类的不变性(从java.util.Calendar吸收的痛苦教训)。若是须要修改时间对象,会返回一个新的实例。
让咱们看看一些关键的类和用法示例。第一个类是Clock,Clock使用时区来访问当前的instant, date和time。Clock类能够替换 System.currentTimeMillis() 和 TimeZone.getDefault().
1 2 3 4 |
|
控制台输出以下:
2014-04-12T15:19:29.282Z
1397315969360
其余类咱们看看LocalTime和LocalDate。LocalDate只保存有ISO-8601日期系统的日期部分,有时区信息,相应地,LocalTime只保存ISO-8601日期系统的时间部分,没有时区信息。LocalDate和LocalTime均可以从Clock对象建立。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
控制台输出以下:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime类合并了LocalDate和LocalTime,它保存有ISO-8601日期系统的日期和时间,可是没有时区信息。让咱们看一个简单的例子。
1 2 3 4 5 6 |
|
输出以下:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
若是您须要一个类持有日期时间和时区信息,可使用ZonedDateTime,它保存有ISO-8601日期系统的日期和时间,并且有时区信息。让咱们看一些例子:
1 2 3 4 5 6 7 8 |
|
输出以下:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
最后让咱们看看Duration类,Duration持有的时间精确到纳秒。它让咱们很容易计算两个日期中间的差别。让咱们来看一下:
1 2 3 4 5 6 7 |
|
上面的例子计算了两个日期(2014年4月16日和2014年5月16日)之间的持续时间(基于天数和小时)输出以下:
Duration in days: 365
Duration in hours: 8783
对于Java 8的新日期时间的整体印象仍是比较积极的。一部分是由于有经历实战的Joda-Time的基础,还有一部分是由于日期时间终于被认真对待并且听取了开发人员的声音。关于更多的详细信息,请参考官方文档。
4.4 Nashorn javascript引擎
Java 8提供了一个新的Nashorn javascript引擎,它容许咱们在JVM上运行特定的javascript应用。Nashorn javascript引擎只是javax.script.ScriptEngine另外一个实现,并且规则也同样,容许Java和JavaScript互相操做。这里有个小例子:
1 2 3 4 5 |
|
输出以下:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
4.5 Base64
对Base64的支持最终成了Java 8标准库的一部分,很是简单易用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
控制台输出的编码和解码的字符串
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder()).
4.6 并行数组
Java 8新增长了不少方法支持并行的数组处理。最重要的大概是parallelSort()这个方法显著地使排序在多核计算机上速度加快。下面的小例子演示了这个新的方法(parallelXXX)的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
这一小段代码使用parallelSetAll() t方法填充这个长度是2000的数组,而后使用parallelSort() 排序。这个程序输出了排序前和排序后的10个数字来验证数组真的已经被排序了。示例可能的输出以下(请注意这些数字是随机产生的)
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
4.7 并发
在新增Stream机制与lambda的基础之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法来支持汇集操做。同时也在java.util.concurrent.ForkJoinPool类中加入了一些新方法来支持共有资源池(common pool)(请查看咱们关于Java 并发的免费课程)。
新增的java.util.concurrent.locks.StampedLock类提供一直基于容量的锁,这种锁有三个模型来控制读写操做(它被认为是不太有名的java.util.concurrent.locks.ReadWriteLock类的替代者)。
在java.util.concurrent.atomic包中还增长了下面这些类:
5. 新的工具
Java 8 提供了一些新的命令行工具,在这节里咱们将会介绍它们中最有趣的部分。
5.1 Nashorn引擎:jjs
jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,而且执行这些源代码。例如,咱们建立一个具备以下内容的func.js文件:
1 2 3 4 5 6 7 |
|
咱们能够把这个文件做为参数传递给jjs使得这个文件能够在命令行中执行
1 |
|
输出结果以下
2
更多的详细信息请参考官方文档。
5.2 类依赖分析工具:jdeps
Jdeps是一个功能强大的命令行工具,它能够帮咱们显示出包层级或者类层级java类文件的依赖关系。它接受class文件、目录、jar文件做为输入,默认状况下,jdeps会输出到控制台。
做为例子,让咱们看看如今很流行的Spring框架的库的依赖关系报告。为了让报告短一些,咱们只分析一个jar: org.springframework.core-3.0.5.RELEASE.jar.
jdeps org.springframework.core-3.0.5.RELEASE.jar 这个命令输出内容不少,咱们只看其中的一部分,这些依赖关系根绝包来分组,若是依赖关系在classpath里找不到,就会显示not found.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
更多的详细信息请参考官方文档。
6. JVM的新特性
JVM内存永久区已经被metaspace替换(JEP 122)。JVM参数 -XX:PermSize 和 –XX:MaxPermSize被XX:MetaSpaceSize 和 -XX:MaxMetaspaceSize代替。
7. 结论
更多展望:Java 8经过发布一些能够增长程序员生产力的特性来推动这个伟大的平台的进步。如今把生产环境迁移到Java 8还为时尚早,可是在接下来的几个月里,它会被大众慢慢的接受。毫无疑问,如今是时候让你的代码与Java 8兼容,而且在Java 8足够安全稳定的时候迁移到Java 8。
做为社区对Java 8的承认,最近Pivotal发布了可在生产环境下支持Java 8的Spring Framework 4.0.3。