原文链接,原文作者:Andrey Redko ,译者:Justin,校对:郭蕾
1.简介
毫无疑问,Java 8是自Java 5(2004年)发布以来Java语言最大的一次版本升级,Java 8带来了很多的新特性,比如编译器、类库、开发工具和JVM(Java虚拟机)。在这篇教程中我们将会学习这些新特性,并通过真实例子演示说明它们适用的场景。
本教程由下面几部分组成,它们分别涉及到Java平台某一特定方面的内容:
- 语言
- 编译器
- 类库
- 开发工具
- 运行时(Java虚拟机)
2.Java的新特性
总体来说,Java 8是一个大的版本升级。有人可能会说,Java 8的新特性非常令人期待,但是也要花费大量的时间去学习。这一节我们会讲到这些新特性。
2.1 Lambda表达式和函数式接口
Lambda表达式(也叫做闭包)是Java 8中最大的也是期待已久的变化。它允许我们将一个函数当作方法的参数(传递函数),或者说把代码当作数据,这是每个函数式编程者熟悉的概念。很多基于JVM平台的语言一开始就支持Lambda表达式,但是Java程序员没有选择,只能使用匿名内部类来替代Lambda表达式。
Lambda表达式的设计被讨论了很久,而且花费了很多的功夫来交流。不过最后取得了一个折中的办法,得到了一个新的简明并且紧凑的Lambda表达式结构。最简单的Lambda表达式可以用逗号分隔的参数列表、->符号和功能语句块来表示。示例如下:
1 |
Arrays.asList( "a" , "b" , "d" ).forEach( e -> System.out.println( e ) ); |
请注意到编译器会根据上下文来推测参数的类型,或者你也可以显示地指定参数类型,只需要将类型包在括号里。举个例子:
1 |
Arrays.asList( "a" , "b" , "d" ).forEach( ( String e ) -> System.out.println( e ) ); |
如果Lambda的功能语句块太复杂,我们可以用大括号包起来,跟普通的Java方法一样,如下:
1 |
String separator = "," ; |
2 |
Arrays.asList( "a" , "b" , "d" ).forEach( |
3 |
( String e ) -> System.out.print( e + separator ) ); |
Lambda表达式可能会引用类的成员或者局部变量(会被隐式地转变成final类型),下面两种写法的效果是一样的:
1 |
String separator = "," ; |
2 |
Arrays.asList( "a" , "b" , "d" ).forEach( |
3 |
( String e ) -> System.out.print( e + separator ) ); |
和
1 |
final String separator = "," ; |
2 |
Arrays.asList( "a" , "b" , "d" ).forEach( |
3 |
( String e ) -> System.out.print( e + separator ) ); |
Lambda表达式可能会有返回值,编译器会根据上下文推断返回值的类型。如果lambda的语句块只有一行,不需要return关键字。下面两个写法是等价的:
1 |
Arrays.asList( "a" , "b" , "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) ); |
和
1 |
Arrays.asList( "a" , "b" , "d" ).sort( ( e1, e2 ) -> { |
2 |
int result = e1.compareTo( e2 ); |
语言的设计者们思考了很多如何让现有的功能和lambda表达式友好兼容。于是就有了函数接口这个概念。函数接口是一种只有一个方法的接口,像这样地,函数接口可以隐式地转换成lambda表达式。
java.lang.Runnable 和java.util.concurrent.Callable是函数接口两个最好的例子。但是在实践中,函数接口是非常脆弱的,只要有人在接口里添加多一个方法,那么这个接口就不是函数接口了,就会导致编译失败。Java 8提供了一个特殊的注解@FunctionalInterface来克服上面提到的脆弱性并且显示地表明函数接口的目的(java里所有现存的接口都已经加上了@FunctionalInterface)。让我们看看一个简单的函数接口定义:
2 |
public interface Functional { |
我们要记住默认的方法和静态方法(下一节会具体解释)不会违反函数接口的约定,例子如下:
2 |
public interface FunctionalDefaultMethods { |
5 |
default void defaultMethod() { |
支持Lambda是Java 8最大的卖点,他有巨大的潜力吸引越来越多的开发人员转到这个开发平台来,并且在纯Java里提供最新的函数式编程的概念。对于更多的细节,请参考官方文档。
2.2 接口的默认方法和静态方法
Java 8增加了两个新的概念在接口声明的时候:默认和静态方法。默认方法和Trait有些类似,但是目标不一样。默认方法允许我们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。作为替代方式,接口可以提供一个默认的方法实现,所有这个接口的实现类都会通过继承得倒这个方法(如果有需要也可以重写这个方法),让我们来看看下面的例子:
01 |
private interface Defaulable { |
04 |
default String notRequired() { |
05 |
return "Default implementation" ; |
09 |
private static class DefaultableImpl implements Defaulable { |
12 |
private static class OverridableImpl implements Defaulable { |
14 |
public String notRequired() { |
15 |
return "Overridden implementation" ; |
接口Defaulable使用default关键字声明了一个默认方法notRequired(),类DefaultableImpl实现了Defaulable接口,没有对默认方法做任何修改。另外一个类OverridableImpl重写类默认实现,提供了自己的实现方法。
Java 8 的另外一个有意思的新特性是接口里可以声明静态方法,并且可以实现。例子如下:
1 |
private interface DefaulableFactory { |
3 |
static Defaulable create( Supplier< Defaulable > supplier ) { |
下面是把接口的静态方法和默认方法放在一起的示例(::new 是构造方法引用,后面会有详细描述):
1 |
public static void main( String[] args ) { |
2 |
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl:: new ); |
3 |
System.out.println( defaulable.notRequired() ); |
5 |
defaulable = DefaulableFactory.create( OverridableImpl:: new ); |
6 |
System.out.println( defaulable.notRequired() ); |
控制台的输出如下:
Default implementation
Overridden implementation
JVM平台的接口的默认方法实现是很高效的,并且方法调用的字节码指令支持默认方法。默认方法使已经存在的接口可以修改而不会影响编译的过程。java.util.Collection中添加的额外方法就是最好的例子:stream(), parallelStream(), forEach(), removeIf()
虽然默认方法很强大,但是使用之前一定要仔细考虑是不是真的需要使用默认方法,因为在层级很复杂的情况下很容易引起模糊不清甚至变异错误。更多的详细信息请参考官方文档。
2.3 方法引用
方法引用提供了一个很有用的语义来直接访问类或者实例的已经存在的方法或者构造方法。结合Lambda表达式,方法引用使语法结构紧凑简明。不需要复杂的引用。
下面我们用Car 这个类来做示例,Car这个类有不同的方法定义。让我们来看看java 8支持的4种方法引用。
01 |
public static class Car { |
02 |
public static Car create( final Supplier< Car > supplier ) { |
03 |
return supplier.get(); |
06 |
public static void collide( final Car car ) { |
07 |
System.out.println( "Collided " + car.toString() ); |
10 |
public void follow( final Car another ) { |
11 |
System.out.println( "Following the " + another.toString() ); |
14 |
public void repair() { |
15 |
System.out.println( "Repaired " + this .toString() ); |
第一种方法引用是构造方法引用,语法是:Class::new ,对于泛型来说语法是:Class<T >::new,请注意构造方法没有参数:
1 |
final Car car = Car.create( Car:: new ); |
2 |
final List< Car > cars = Arrays.asList( car ); |
第二种方法引用是静态方法引用,语法是:Class::static_method请注意这个静态方法只支持一个类型为Car的参数。
1 |
cars.forEach( Car::collide ); |
第三种方法引用是类实例的方法引用,语法是:Class::method请注意方法没有参数。
1 |
cars.forEach( Car::repair ); |
最后一种方法引用是引用特殊类的方法,语法是:instance::method,请注意只接受Car类型的一个参数。
1 |
final Car police = Car.create( Car:: new ); |
2 |
cars.forEach( police::follow ); |
运行这些例子我们将会在控制台得到如下信息(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注释。实际上,他不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的。让我们来看看例子:
01 |
package com.javacodegeeks.java8.repeatable.annotations; |
03 |
import java.lang.annotation.ElementType; |
04 |
import java.lang.annotation.Repeatable; |
05 |
import java.lang.annotation.Retention; |
06 |
import java.lang.annotation.RetentionPolicy; |
07 |
import java.lang.annotation.Target; |
09 |
public class RepeatingAnnotations { |
10 |
@Target ( ElementType.TYPE ) |
11 |
@Retention ( RetentionPolicy.RUNTIME ) |
12 |
public @interface Filters { |
16 |
@Target ( ElementType.TYPE ) |
17 |
@Retention ( RetentionPolicy.RUNTIME ) |
18 |
@Repeatable ( Filters. class ) |
19 |
public @interface Filter { |
25 |
public interface Filterable { |
28 |
public static void main(String[] args) { |
29 |
for ( Filter filter: Filterable. class .getAnnotationsByType( Filter. class ) ) { |
30 |
System.out.println( filter.value() ); |
我们可以看到,注释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;
01 |
package com.javacodegeeks.java8.type.inference; |
03 |
public class Value<T> { |
04 |
public static <T> T defaultValue() { |
08 |
public T getOrDefault( T value, T defaultValue ) { |
09 |
return ( value != null ) ? value : defaultValue; |
这里是Value< String >的用法
1 |
package com.javacodegeeks.java8.type.inference; |
3 |
public class TypeInference { |
4 |
public static void main(String[] args) { |
5 |
final Value<String> value = new Value<>(); |
6 |
value.getOrDefault( "22" , Value.defaultValue() ); |
参数Value.defaultValue()的类型被编译器推断出来,不需要显式地提供类型。在java 7, 相同的代码不会被编译,需要写成:Value.< String >defaultValue()
2.6 注解的扩展
Java 8扩展了注解可以使用的范围,现在我们几乎可以在所有的地方:局部变量、泛型、超类和接口实现、甚至是方法的Exception声明。一些例子如下:
01 |
package com.javacodegeeks.java8.annotations; |
03 |
import java.lang.annotation.ElementType; |
04 |
import java.lang.annotation.Retention; |
05 |
import java.lang.annotation.RetentionPolicy; |
06 |
import java.lang.annotation.Target; |
07 |
import java.util.ArrayList; |
08 |
import java.util.Collection; |
10 |
public class Annotations { |
11 |
@Retention ( RetentionPolicy.RUNTIME ) |
12 |
@Target ( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } ) |
13 |
public @interface NonEmpty { |
16 |
public static class Holder< @NonEmpty T > extends @NonEmpty Object { |
17 |
public void method() throws @NonEmpty Exception { |
21 |
@SuppressWarnings ( "unused" ) |
22 |
public static void main(String[] args) { |
23 |
final Holder< String > holder = new @NonEmpty Holder< String >(); |
24 |
@NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>(); |
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参数)。
01 |
package com.javacodegeeks.java8.parameter.names; |
03 |
import java.lang.reflect.Method; |
04 |
import java.lang.reflect.Parameter; |
06 |
public class ParameterNames { |
07 |
public static void main(String[] args) throws Exception { |
08 |
Method method = ParameterNames. class .getMethod( "main" , String[]. class ); |
09 |
for ( final Parameter parameter: method.getParameters() ) { |
10 |
System.out.println( "Parameter: " + parameter.getName() ); |
如果你编译这个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 |
Optional< String > fullName = Optional.ofNullable( null ); |
2 |
System.out.println( "Full Name is set? " + fullName.isPresent() ); |
3 |
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); |
4 |
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) ); |
如果Optional实例有非空的值,方法 isPresent() 返回true否则返回false。方法orElseGet提供了回退机制,当Optional的值为空时接受一个方法返回默认值。map()方法转化Optional当前的值并且返回一个新的Optional实例。orElse方法和orElseGet类似,但是它不接受一个方法,而是接受一个默认值。上面代码运行结果如下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
让我们大概看看另外一个例子。
1 |
Optional< String > firstName = Optional.of( "Tom" ); |
2 |
System.out.println( "First Name is set? " + firstName.isPresent() ); |
3 |
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); |
4 |
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) ); |
输出如下:
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的用法。
01 |
public class Streams { |
06 |
private static final class Task { |
07 |
private final Status status; |
08 |
private final Integer points; |
10 |
Task( final Status status, final Integer points ) { |
15 |
public Integer getPoints() { |
19 |
public Status getStatus() { |
24 |
public String toString() { |
25 |
return String.format( "[%s, %d]" , status, points ); |
Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子:
1 |
final Collection< Task > tasks = Arrays.asList( |
2 |
new Task( Status.OPEN, 5 ), |
3 |
new Task( Status.OPEN, 13 ), |
4 |
new Task( Status.CLOSED, 8 ) |
第一个问题是所有的开放的Task的点数是多少?在java 8 之前,通常的做法是用foreach迭代。但是Java8里头我们会用Stream。Stream是多个元素的序列,支持串行和并行操作。
2 |
final long totalPointsOfOpenTasks = tasks |
4 |
.filter( task -> task.getStatus() == Status.OPEN ) |
5 |
.mapToInt( Task::getPoints ) |
8 |
System.out.println( "Total points: " + totalPointsOfOpenTasks ); |
控制台的输出将会是:
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的点数加起来。
2 |
final double totalPoints = tasks |
5 |
.map( task -> task.getPoints() ) |
6 |
.reduce( 0 , Integer::sum ); |
8 |
System.out.println( "Total points (all tasks): " + totalPoints ); |
这个例子跟上面那个非常像,除了这个例子里使用了parallel()方法 并且计算最终结果的时候使用了reduce方法。
输出如下:
Total points (all tasks): 26.0
经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子:
2 |
final Map< Status, List< Task > > map = tasks |
4 |
.collect( Collectors.groupingBy( Task::getStatus ) ); |
5 |
System.out.println( map ); |
控制台的输出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。
02 |
final Collection< String > result = tasks |
04 |
.mapToInt( Task::getPoints ) |
06 |
.mapToDouble( points -> points / totalPoints ) |
08 |
.mapToLong( weigth -> ( long )( weigth * 100 ) ) |
09 |
.mapToObj( percentage -> percentage + "%" ) |
10 |
.collect( Collectors.toList() ); |
12 |
System.out.println( result ); |
控制台输出如下:
[19%, 50%, 30%]
最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。
1 |
final Path path = new File( filename ).toPath(); |
2 |
try ( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) { |
3 |
lines.onClose( () -> System.out.println( "Done!" ) ).forEach( System.out::println ); |
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().
2 |
final Clock clock = Clock.systemUTC(); |
3 |
System.out.println( clock.instant() ); |
4 |
System.out.println( clock.millis() ); |
控制台输出如下:
2014-04-12T15:19:29.282Z
1397315969360
其他类我们看看LocalTime和LocalDate。LocalDate只保存有ISO-8601日期系统的日期部分,有时区信息,相应地,LocalTime只保存ISO-8601日期系统的时间部分,没有时区信息。LocalDate和LocalTime都可以从Clock对象创建。
02 |
final LocalDate date = LocalDate.now(); |
03 |
final LocalDate dateFromClock = LocalDate.now( clock ); |
05 |
System.out.println( date ); |
06 |
System.out.println( dateFromClock ); |
09 |
final LocalTime time = LocalTime.now(); |
10 |
final LocalTime timeFromClock = LocalTime.now( clock ); |
12 |
System.out.println( time ); |
13 |
System.out.println( timeFromClock ); |
控制台输出如下:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime类合并了LocalDate和LocalTime,它保存有ISO-8601日期系统的日期和时间,但是没有时区信息。让我们看一个简单的例子。
2 |
final LocalDateTime datetime = LocalDateTime.now(); |
3 |
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock ); |
5 |
System.out.println( datetime ); |
6 |
System.out.println( datetimeFromClock ); |
输出如下:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
如果您需要一个类持有日期时间和时区信息,可以使用ZonedDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。让我们看一些例子:
2 |
final ZonedDateTime zonedDatetime = ZonedDateTime.now(); |
3 |
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock ); |
4 |
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.no |