第一次尝试翻译文章,有错误请见谅:)
Java 8 特性 – 终极指南 (原文:http://www.javacodegeeks.com/2014/05/java-8-features-tutorial.html )
编者注:Java 8出现在公众视野中已经有一段时间了,在这期间,种种迹象都表明Java 8是一个非常重要的版本。
我们已经在Java Code Geeks提供了很多丰富的教程,比方说Playing with Java 8 – Lambdas and Concurrency,Java 8 Date Time API Tutorial : LocalDateTime和Abstract Class Versus Interface in the JDK 8 Era.
我们还转发了文章15 Must Read Java 8 Tutorials。当然,我们同样分析了Java8的不足,如The Dark Side of Java 8
为了您阅读的便利,我们把Java 8主要特性都收集起来放在本文中,希望您能喜欢。
目录
1. 导言
2. Java语言中的新特性
2.1. Lambdas 和 函数接口
2.2. 接口的Default和Static 方法
2.3.方法引用
2.4. 重复注解(Repeating annotations)
2.5. 更好的类型推断
2.6. 扩展的注解(Annotation)支持
3. Java编译器中的新特性
3.1. 参数命名
4. Java标准库中的新特性
4.1. 可选(Optional)
4.2. 流
4.3. 日期时间API (JSR 310)
4.4. Nashorn JavaScript 引擎
4.5. Base64编码
4.6. 并行数组
4.7. 兵并发
5. Java的新工具
5.1. Nashorn 引擎: jjs
5.2. 类依赖分析器: jdeps
6. JVM新特性
7. 总结
8. 其他资源
1. 导言
毋庸置疑,Java 8是自Java 5(2004年发布)以来 Java世界最重大的事件。Java语言,编译器,标准库,工具和JVM本身都有许多新的特性。在这个指南中,我们将一起来看看所有这些改变,并且用一些真实的例子演示对应的使用场景。
本指南由多部分组成,每一部分都讲述了Java平台的以下某一方面
2. Java语言中的新特性
无论从哪种意义上来说,Java 8都是一个主要的版本更新。你可以说它为了实现所有Java程序员所期待的特性,导致它花了如此长的时间才定稿。在本节我们会覆盖大部分的Java语言新特性
2.1. Lambdas and 函数接口
Lambdas(也称作闭包)是整个Java 8中最大的也是最令人期待的Java语言的改变。他允许我们把函数作为方法的参数(将函数在方法间传递)也可以说像对待数据一样对待代码:这是每个使用函数式语言的开发者所非常熟悉的。许多JVM平台上的语言也从一开始就支持lambdas,但是Java开发者却没有别的选择,只能用匿名类来模拟lambdas效果。
人们已经对Lambdas的设计进行了很长时间的讨论,Java社区也为此做出了许多努力。最终大家找到了一个平衡点,使得我们得到了一个新的简明并且紧凑的语言结构。在它最简单的形式里, lambda可以表现成一组逗号分隔的参数加->符号和程序体。如:
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );
请注意在这里e的类型是由编译器推断出来的。当然,我们也可以显式得提供参数的类型,把类型定义用括号括起来。如:
Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
如果lamdba的程序体比较复杂,我们也可以像通常的Java函数定义一样用花括号把它包起来。如:
Arrays.asList( "a", "b", "d" ).forEach( e -> { System.out.print( e ); System.out.print( e ); } );
Lambdas可以引用类成员和本地变量(如果他们不是final的,Java会把他们隐式得转成final)。如下面的两个例子是等价的:(类成员是不是也是这样?)
String separator = ","; Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );
和
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
Lambdas 也可以有返回值。返回值的类型同样会由编译器推断出来。如果lambda的程序体制有一行,那return语句也可以省略。以下两段代码是等价的
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
和:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> { int result = e1.compareTo( e2 ); return result; } );
Java语言设计者为如何把现有的功能变成lambda兼容绞尽脑汁。函数式接口就是一个产物。函数式接口是一个只有单个方法的接口。他可能会被隐式的转换成lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口两个很好的例子。在实际中,函数式接口是非常容易被破坏的,假设有人对接口定义添加了一个方法,那这个接口就不再是函数式接口,然后就会造成编译失败。为了克服这个缺点,Java 8增加了一个特别的annotation @FunctionalInterface。用这个annotation可以显示的把一个接口声明称函数式接口(所有已经存在于Java库中的函数式接口都已经加上了这个annotation)。让我们来看一下这个简单的函数式接口的定义:
@FunctionalInterface public interface Functional { void method(); }
有一点需要记住的是:default和static方法不会破坏函数式接口的约定,所以我们可以在函数式接口中使用它们。
@FunctionalInterface public interface FunctionalDefaultMethods { void method(); default void defaultMethod() { } }
Lambdas是Java8是最大卖点。他拥有一切吸引越来越多的开发者来Java这一伟大平台的潜力,他也在纯Java语言中对函数式编程概念提供了最先进的支持。
2.2. 接口的Default和Static方法
Java 8对接口的声明进行了扩展,引进了两个新概念:default和static方法。Default方法使得接口有点像traits,但又不是为了完全相同的目的。他们允许我们对已有的接口添加新的方法而不破坏已有的接口实现代码的二进制兼容性。
Default方法和abstract方法的区别是abstract方法是必须要被实现的。但是default方法不一定。每个接口要对default方法提供所谓的默认实现,这样所有的实现这个接口的类都会默认继承这个方法(当然实现类也可以重载这个方法的默认实现)。让我们来看看下面这个例子:
private interface Defaulable { // Interfaces now allow default methods, the implementer may or // may not implement (override) them. default String notRequired() { return "Default implementation"; } } private static class DefaultableImpl implements Defaulable { } private static class OverridableImpl implements Defaulable { @Override public String notRequired() { return "Overridden implementation"; } }
Defaulable接口用default关键字声明了一个名字叫notRequired()的default方法。DefaultableImpl类实现了这个接口但没有去改变default方法。另一个OverridableImpl类则对default方法提供了自己的实现。
Java 8的另外一个有意思的特性是接口能够声明(并且提供实现)static method。这里是一个例子:
private interface DefaulableFactory { // Interfaces now allow static methods static Defaulable create( Supplier< Defaulable > supplier ) { return supplier.get(); } }
下面这段代码把上面例子里的default方法和static方法结合了起来:
public static void main( String[] args ) { Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new ); System.out.println( defaulable.notRequired() ); defaulable = DefaulableFactory.create( OverridableImpl::new ); System.out.println( defaulable.notRequired() ); }
这段代码的控制台输出会是:
Default implementation Overridden implementation
JVM中的default方法的实现是非常高效的,JVM对方法的调用提供了字节码指令级的支持。Default方法使得已有的Java接口能够进化但不破坏已有代码的编译。一个很好的例子就是Java 8在java.util.Collection接口中添加了多如牛毛的方法:stream(), parallelStream(), forEach(), removeIf(),….
虽然Default方法很强大,但是我们使用起来需要特别小心:在声明一个default方法的时候必须要多想想是不是真的有必要,因为在复杂的继承体系下下他可能会引起混淆和编译错误。在官方文档(http://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html)中可以找到更多的细节。
2.3. 方法引用
方法引用提供了实用的语法使得我们能直接引用Java类和对象(实例)的方法和构造函数。方法引用结合Lambdas表达式让Java不再需要样板,使得结构看起来既紧凑又简明,。
下面的Car类是具有不同的方法定义的一个例子,让我们来区分一下4种支持的方法引用类型.
public static class Car { public static Car create( final Supplier< Car > supplier ) { return supplier.get(); } public static void collide( final Car car ) { System.out.println( "Collided " + car.toString() ); } public void follow( final Car another ) { System.out.println( "Following the " + another.toString() ); } public void repair() { System.out.println( "Repaired " + this.toString() ); } }
第一类是构造函数引用,语法为Class:new以及他的泛型变体Class
final Car car = Car.create( Car::new ); final List< Car > cars = Arrays.asList( car );
第二类是对静态方法的引用,语法为Class::static_method。请注意此方法接受一个Car类型的参数
cars.forEach( Car::collide );
第三类是对某个类型的任意对象的实例方法的引用,语法为Class::method。请注意此方法不接受任何参数
cars.forEach( Car::repair );
最后,第四类是对具体类实例的方法的引用,语法为instance::method。请注意此方法接受一个Car类型的参数。
final Car police = Car.create( Car::new ); 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
更多的例子和详情请参考官方文档http://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
2.4. 重复注解(Repeating annotations)
从Java5引入annotation的支持以来,这个特性就非常流行并且被广泛使用。但是annotation使用的一个限制就是同一个annotation不能够在同一个地方重复声明。Java8打破了这个规则,引入了重复注解。它允许同样的annotation在同一地方被声明多次。
重复注解本身需要以@Repeatable annotation注解。事实上,这不是一个语言的改变,因为他底层的实现技术没有变化,所以它更多地像是一个编译器的小花招。让我们来看一个简单的例子:
package com.javacodegeeks.java8.repeatable.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; public class RepeatingAnnotations { @Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) public @interface Filters { Filter[] value(); } @Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) @Repeatable( Filters.class ) public @interface Filter { String value(); }; @Filter( "filter1" ) @Filter( "filter2" ) public interface Filterable { } public static void main(String[] args) { for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) { System.out.println( filter.value() ); } } }
在上面的例子中,有一个annotation类Filter,被@Repeatable(Filter.class)注解。Filters是其实只是Filter annotation的容器,但是Java编译器很努力得把它在开发者面前隐藏起来。因此, Filter annotation在Filterable接口上声明了两次(完全不用到Filters)。
同样的,反射API提供了新的getAnnotationsByType()方法,该方法会返回具体某一类型的重复注解(请注意Filterable.class.getAnnotation(Filters.class)会返回被编译器注入的Filters实例)
程序的输出是这样的:
filter1 filter2
更详细信息请参考官方文档http://docs.oracle.com/javase/tutorial/java/annotations/repeating.html
2.5. 更好的类型推断
Java 8的编译器在类型推断方面有了长足的进步。在很多情况下,明确的参数参数能够被编译器推断出来,这样代码就会简洁很多。让我们看一个例子:
package com.javacodegeeks.java8.type.inference; public class Value< T > { public static< T > T defaultValue() { return null; } public T getOrDefault( T value, T defaultValue ) { return ( value != null ) ? value : defaultValue; } }
下面是关于Value
package com.javacodegeeks.java8.type.inference; public class TypeInference { public static void main(String[] args) { final Value< String > value = new Value<>(); value.getOrDefault( "22", Value.defaultValue() ); } }
参数Value.defaultValue()的类型是被推断出来的,它不需要被提供。在Java 7,上面的例子不会通过编译,我们必须写成Value.
2.6. 扩展的注解支持
Java 8扩展了注解可能使用的情况。现在几乎所有东西都能被注解:本地变量,泛型,超类和接口,甚至是方法的异常声明。下面是一些例子:
package com.javacodegeeks.java8.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collection; public class Annotations { @Retention( RetentionPolicy.RUNTIME ) @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } ) public @interface NonEmpty { } public static class Holder< @NonEmpty T > extends @NonEmpty Object { public void method() throws @NonEmpty Exception { } } @SuppressWarnings( "unused" ) public static void main(String[] args) { final Holder< String > holder = new @NonEmpty Holder< String >(); @NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>(); } }
ElementType.TYPE_USE和ElementType.TYPE_PARAMETER是两个描述注解使用环境的新的元素类型。为了识别这些新的注解类型,Annotation Processing API也进行了一些小的改变。
3. Java编译器里的新特性
3.1. 参数名
多年来,Java开发者发明了许多不同的方式去保持Java字节码中的方法的参数名字,并是它们在运行时可用(如Paranamer library https://github.com/paul-hammant/paranamer )。终于,Java 8把这个令人期待的特性加入了语言(使用反射API和Parameter.getName())和字节码(使用新的javac编译器的参数 –parameters)
package com.javacodegeeks.java8.parameter.names; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class ParameterNames { public static void main(String[] args) throws Exception { Method method = ParameterNames.class.getMethod( "main", String[].class ); for( final Parameter parameter: method.getParameters() ) { System.out.println( "Parameter: " + parameter.getName() ); } } }
如果你不使用-parameters参数去编译这个类,那在运行的时候你会看到:
Parameter: arg0
加上-parameters参数之后编译,程序的输出就不一样了(实际的参数名字会被显示出来):
Parameter: args
为有经验的Maven用户考虑,在maven-compiler-plugin的configuration段, -parameters参数可以被配置并应用到编译器。
org.apache.maven.plugins maven-compiler-plugin 3.1 -parameters 1.8
最新的支持Java 8的Eclipse Kepler SR2版本提供了如下图所示的配置选项来控制这个编译器的参数。如下图所示
图1. 配置Eclipse工程以支持Java8编译器的-parameters参数。
另外,在Parameter类里提供了一个很有用的方法叫isNamePersent()可以被用来验证方法名是否存在。
4. Java标准库中的新特性
Java 8为了提供对现代化并发,函数式变成,日期/时间等等更好的支持,增加和扩展了许多类。
4.1. Optional
著名的NullPointerException(http://examples.javacodegeeks.com/java-basics/exceptions/java-lang-nullpointerexception-how-to-handle-null-pointer-exception/)是到目前为止导致Java应用运行失败最常见的原因的。很多年以前,伟大的Google Guava(http://code.google.com/p/guava-libraries/)项目引入了Optionals作为NullPointerExceptions的一个解决方法,它阻止了到处存在的null检查对代码的污染,而鼓励开发者写更干净的代码。受Google Guava的启发,现在Optional成为了Java 8标准库的一部分。
Optional是一个容器:它可以持有某个类型T的值,也可以是null。它通过提供很多有用的方法使得对null的显式的检查不再有必要。更多详情请参考官方文档http://docs.oracle.com/javase/8/docs/api/
我们来看看Optional的两个小例子:用于可以为null的值和用于不允许为null的值。
Optional< String > fullName = Optional.ofNullable( null ); System.out.println( "Full Name is set? " + fullName.isPresent() ); System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
isPresent()方法对非null值返回true,对null值返回false. orElseGet()方法接收一个产生默认值的函数,当Optional持有null值时会使用这个函数来产生默认值返回。map()方法把现有的Optional值转换成新值并返回一个新的Optional实例。orElse()方法和orElseGet()方法类似,只是他接收一个默认值而不是一个产生默认值的方法。下面是这段代码的输出:
Full Name is set? false Full Name: [none] Hey Stranger!
我们来看一下另外一个例子(译者注:Optional.of的参数如果是null会抛出NullPointerException):
Optional< String > firstName = Optional.of( "Tom" ); System.out.println( "First Name is set? " + firstName.isPresent() ); System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) ); System.out.println();
下面是输出:
First Name is set? true First Name: Tom Hey Tom!
更详细内容请参考官方文档http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
4.2. Streams
新增加的Stream API(java.util.stream)给Java引进了现实世界的函数式编程。这是到目前为止加入Java标准库的最复杂的特性。它让Java开发者能写高效,干净和简明的代码,从而极大的提高生产率。
Stream API极大的简化了集合操作(我们后面会看到,它并不仅仅限于集合操作)。让我们从一个简单的Task类开始
public class Streams { private enum Status { OPEN, CLOSED }; private static final class Task { private final Status status; private final Integer points; Task( final Status status, final Integer points ) { this.status = status; this.points = points; } public Integer getPoints() { return points; } public Status getStatus() { return status; } @Override public String toString() { return String.format( "[%s, %d]", status, points ); } } }
Task具有points的概念(或者可以看作伪复杂度),并且有OPEN和CLOSED两种状态。然后我们把task放入一个小集合中
final Collection< Task > tasks = Arrays.asList( new Task( Status.OPEN, 5 ), new Task( Status.OPEN, 13 ), new Task( Status.CLOSED, 8 ) );
我们要解决的第一个问题是:这里的OPEN task总共有多少个points?在Java 8之前,通常的做法是用类似foreach迭代。但在Java 8中,答案会是stream:一堆支持连续和并行聚合操作的元素
// Calculate total points of all active tasks using sum() final long totalPointsOfOpenTasks = tasks .stream() .filter( task -> task.getStatus() == Status.OPEN ) .mapToInt( Task::getPoints ) .sum(); System.out.println( "Total points: " + totalPointsOfOpenTasks );
代码输出为:
Total points: 18
这里发生了一些事情. 首先,task集合被转换成它的stream的表现形式。然后stream的filter操作把所有状态为CLOSED的task都过滤掉了。下一步,mapToInt操作用每个task实例上的Task::getPoints方法把Task的stream转换成Integer的stream。最后,sum方法把所有的task的points加起来,产生了最终的结果。
在看下一个例子之前,这里有一些需要注意的事项(更多的内容http://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#StreamOps)。 Stream操作被分为中间操作和终端操作两类。
中间操作会返回一个新的stream。他们总是延迟(lazy)的,执行一个类似filter样的中间操作并不会真正的进行任何过滤操作,而只会产生一个新的stream。这个新的stream在遍历的时候只会包含最初的stream中符合断言的元素。
终端操作,如forEach和sum, 会遍历stream产生一个结果,或者一个副作用。在终端操作完成后,stream管道被认为消耗了,就不能够在被使用了。在几乎所有的情况下,终端操作都是即时的(eager)完成数据源的遍历。
Stream的另外一个有价值的特性是它生来就支持并行处理。让我们看看下面这个例子,它把所有task的point加起来。
// Calculate total points of all tasks final double totalPoints = tasks .stream() .parallel() .map( task -> task.getPoints() ) // or map( Task::getPoints ) .reduce( 0, Integer::sum ); System.out.println( "Total points (all tasks): " + totalPoints );
这和之前的那个例子很像,除了我们尝试并行处理所有的task,并且用reduce方法来计算最后的结果。
这里是输出
Total points (all tasks): 26.0
我们经常会有需要对集合元素以某些条件做分组操作。Stream也可以做到这点,下面是一个例子:
// Group tasks by their status final Map< Status, List< Task > > map = tasks .stream() .collect( Collectors.groupingBy( Task::getStatus ) ); System.out.println( map );
这个例子的控制台输出会是这样:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
作为task例子的结果,我们来根据每个task的point来计算它在整个集合中所占的比例(也可以称为权重)。
// Calculate the weight of each tasks (as percent of total points) final Collection< String > result = tasks .stream() // Stream< String > .mapToInt( Task::getPoints ) // IntStream .asLongStream() // LongStream .mapToDouble( points -> points / totalPoints ) // DoubleStream .boxed() // Stream< Double > .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream .mapToObj( percentage -> percentage + "%" ) // Stream< String> .collect( Collectors.toList() ); // List< String > System.out.println( result );
控制台输出为:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
最后,我们曾说过,Stream API不仅仅能用在Java集合上。像读写文本文件行等典型的I/O操作也能很好的被stream处理。下面这个小例子能证明这点:
final Path path = new File( filename ).toPath(); try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) { lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println ); }
在stream上调用的onClose()方法会返回一个等价的stream加上一个额外的close处理器。close处理器会在stream上的close()方法被调用的时候触发。
Stream API加上Lambdas,方法引用,和接口的Default和Static方法是Java 8对软件开发中现代编程范式(paradigm?)的回应。更多详情请参见官方文档(http://docs.oracle.com/javase/tutorial/collections/streams/index.html)
4.3. Date/Time API (JSR 310)
Java 8的另一个改进是关于日期时间的管理。他提供了新的Date-Time API(JSR 310 https://jcp.org/en/jsr/detail?id=310)。日期时间的操作是Java开发者的最痛点之一。标准的java.util.Date和java.util.Calendar一点都没有改善这个状态(也有争议说它们使得情况更加混乱)。
这也是为什么产生了Joda-Time: 一个可选的非常好的Java date/time API。 Java 8新的Date-Time API受Joda-Time影响颇深,它吸取了Joda-time中最好的思想。新的java.time包包含了日期,时间,日期/时间,时区,时刻,时间跨度,时钟操作的所有API。在设计这些API的时候,不可变性被慎重考虑了:你不能够对这些对象做任何改变(从java.util.Calendar中学到的深刻教训)。如果需要改变,这些API会返回一个新的实例。
让我们看看Date Time API中主要的类和它们用法的例子吧。第一个类是Clock,它提供了对当前时刻以及以某个时区表示的日期和时间的操作。Clock能够被用来替代System.currentTimeMillis()和TimeZone.getDefault().
// Get the system clock as UTC offset final Clock clock = Clock.systemUTC(); System.out.println( clock.instant() ); System.out.println( clock.millis() );
下面是输出:
2014-04-12T15:19:29.282Z 1397315969360
我们再来看看新类LocalDate和LocalTime。LocalDate持有ISO-8601日历体系中的不带时区信息的日期部分。相应的,LocalTime持有ISO-8601日历体系中的不带时区信息的时间部分。LocalDate和LocalTime都可以从Clock创建出来。
// Get the local date and local time final LocalDate date = LocalDate.now(); final LocalDate dateFromClock = LocalDate.now( clock ); System.out.println( date ); System.out.println( dateFromClock ); // Get the local date and local time final LocalTime time = LocalTime.now(); final LocalTime timeFromClock = LocalTime.now( clock ); System.out.println( time ); System.out.println( timeFromClock );
下面是输出:
2014-04-12 2014-04-12 11:25:54.568 15:25:54.568
LocalDateTime包含了LocalDate和LocalTime.它持有ISO-8601日历体系中的不带时区信息的日期加时间部分。下面是一个简单的例子:
// Get the local date/time final LocalDateTime datetime = LocalDateTime.now(); final LocalDateTime datetimeFromClock = LocalDateTime.now( clock ); System.out.println( datetime ); System.out.println( datetimeFromClock );
输出为:
2014-04-12T11:37:52.309 2014-04-12T15:37:52.309
如果你需要特定时区的日期/时间,那就要使用ZonedDateTime。它持有ISO-8601日历体系中的带时区信息的日期加时间部分。下面是一些不同时区的例子:
// Get the zoned date/time final ZonedDateTime zonedDatetime = ZonedDateTime.now(); final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock ); final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) ); System.out.println( zonedDatetime ); System.out.println( zonedDatetimeFromClock ); System.out.println( zonedDatetimeFromZone );
下面是输出:
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类:由秒和纳秒组成的一段时间。它使得我们计算两个日期的间隔非常简单。让我们来看看例子:
// Get duration between two dates final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 ); final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 ); final Duration duration = Duration.between( from, to ); System.out.println( "Duration in days: " + duration.toDays() ); System.out.println( "Duration in hours: " + duration.toHours() );
例子里计算了2个日期,2014/04/16到2015/04/16之间的duration(用天和小时表示)。下面是控制台输出:
Duration in days: 365 Duration in hours: 8783
Java 8的新日期/时间API给我们的印象非常积极。部分原因是因为他是基于Joda-Time的,具有扎实的实战基础,另一部分是因为这次它终于慎重的解决问题并且听取了开发者的声音。更多详情请参见官方文档http://docs.oracle.com/javase/tutorial/datetime/index.html
4.4. Nashorn JavaScript engine
Java 8带来了新的Nashorn JavaScript引擎(http://docs.oracle.com/javase/tutorial/datetime/index.html),它允许在JVM上开发和运行某些JavaScript的应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的一种实现,它遵循了同样一组规则,允许Java和JavaScript互相调用。这里是一个小例子:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName( "JavaScript" ); System.out.println( engine.getClass().getName() ); System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
下面是输出:
jdk.nashorn.api.scripting.NashornScriptEngine Result: 2
我们会在dedicated to new Java tools部分再来看看Noshorn.
4.5. Base64
Base64编码的支持终于在java 8中被加入了Java标准库。从下面的例子可以看出,它非常容易被使用:
package com.javacodegeeks.java8.base64; import java.nio.charset.StandardCharsets; import java.util.Base64; public class Base64s { public static void main(String[] args) { final String text = "Base64 finally in Java 8!"; final String encoded = Base64 .getEncoder() .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) ); System.out.println( encoded ); final String decoded = new String( Base64.getDecoder().decode( encoded ), StandardCharsets.UTF_8 ); System.out.println( decoded ); } }
上面例子的输出显示了编码后和解码后的文本。
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ== Base64 finally in Java 8!
Base64类同样提供了对URL友好的编码解码器和对MIME友好的编码解码器(Base64.getUrlEncoder()/Base64.getUrlDecoder(), Base64.getMimeEncoder()/Base64.getMimeDecoder()
4.6. 并行数组
Java 8为并行数组处理添加了许多新的方法。其中最重要的可能要数parallelSort()了,它可以在多核机器上显著提高排序速度。下面的小例子演示了这些新的并行处理方法(parallelXxx)
package com.javacodegeeks.java8.parallel.arrays; import java.util.Arrays; import java.util.concurrent.ThreadLocalRandom; public class ParallelArrays { public static void main( String[] args ) { long[] arrayOfLong = new long [ 20000 ]; Arrays.parallelSetAll( arrayOfLong, index -> ThreadLocalRandom.current().nextInt( 1000000 ) ); Arrays.stream( arrayOfLong ).limit( 10 ).forEach( i -> System.out.print( i + " " ) ); System.out.println(); Arrays.parallelSort( arrayOfLong ); Arrays.stream( arrayOfLong ).limit( 10 ).forEach( i -> System.out.print( i + " " ) ); System.out.println(); } }
这段代码用parallelSetAll()填充了20000个随机值到一个数组中。然后用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. 并发
为了支持streams中添加的聚合操作和lambda表达式,java.util.concurrent.ConcurrentHashMap类也添加了一些新的方法。同样,为了支持一种通用的池,java.util.concurrent.ForkJoinPool也加入了新的方法。
新加入的java.util.concurrent.locks.StampedLock类提供了基于容量的三模式读写控制(它可以看作是不那么流行的java.util.concurrent.locks.ReadWriteLock的一个更好的替换方案)
以下类被添加到了 java.util.concurrent.atomic包中:
5. 新的Java工具
Java 8提供了一组命令行工具。在这部分我们来一起看看他们中最有趣的部分。
5.1. Nashorn 引擎: jjs
jjs是一个基于独立Nashorn引擎的一个命令行工具。它接受一组JavaScript源文件作为参数并运行他们。让我们创建一个func.js作为例子:
function f() { return 1; }; print( f() + 1 );
为了从命令行运行它,我们把它作为参数传给jjs:
jjs func.js
控制台输出将是:
2
更多信息请参见官方文档http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jjs.html
5.2. 类依赖分析器: jdeps
jdeps是一个非常好的命令行工具。他能显示Java类文件在包或者类级别的依赖关系。他接受.class文件,目录 或者JAR file作为输入。默认jdeps会把依赖关系输出到系统输出(控制台)
让我们来看流行的Spring框架的依赖报告吧。为了让这个例子比较简短,我们只分析一个jar文件: org.springframework.core-3.0.5.RELESE.jar.
jdeps org.springframework.core-3.0.5.RELEASE.jar
这个命令的输出有非常多,我们在这里只看一下其中的一部分。依赖关系以包分组,如果classpath中找不到依赖的包,它就会显示not found.
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar) -> java.io -> java.lang -> java.lang.annotation -> java.lang.ref -> java.lang.reflect -> java.util -> java.util.concurrent -> org.apache.commons.logging not found -> org.springframework.asm not found -> org.springframework.asm.commons not found org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar) -> java.lang -> java.lang.annotation -> java.lang.reflect -> java.util
更多详情请参见官方文档http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jdeps.html.
6. Java运行时(JVM)中的新特性
永久代(PermGen)被移掉,取而代之的是元空间Metaspace(http://www.javacodegeeks.com/2013/02/java-8-from-permgen-to-metaspace.html)(JEP_122http://openjdk.java.net/jeps/122)。相应的,JVM配置项-XX:PermSize和-XX:MaxPermSize也被-XX:MetaSpaceSize和-XXMaxMetaspaceSize替代了。
7. 总结
未来将在这里:Java 8通过让开发者更有效率的特性,使得Java这个伟大的平台更上一层楼。现在把生产系统迁移到Java 8可能还为时过早,但在接下去的几个月后,它的接受程度必定会慢慢的增长。不过现在应该是正确的时间开始准备让你的代码和Java 8兼容,然后等Java 8被证明足够安全和稳定之后可以容易地迁移过去。
作为社区对Java 8的接受程度的证明,Pivotal最近发布了可用于生产环境的支持Java 8的Spring 4.0.3
如果你喜欢这篇文章,可以订阅我们的newsletter(http://eepurl.com/xRlkj)来获得每周更新和免费的白皮书!也可以在JCG Academy (http://academy.javacodegeeks.com/)找到一些高级的培训课程。
欢迎对Java 8激动人心新特性做出你的评价
8. 资源
以下是一些关于java 8特性更深入的讨论: