Java 8 新特性终极指南
1.前言 毫无疑问,Java 8的发布是自从Java5以来Java世界中最重大的事件,它在编译器、工具类和Java虚拟机等方面为Java语言带来的很多新特性。在本文中我们將一起关注下这些新变化,使用实际的例子展示它们的使用场景。本教程涉及到一下几个部分的内容:语法规范编译器类库工具Java虚拟机2.Java语言中的新特性Java 8 是一个主发行版本,为了实现每一位
1.前言
- 语法规范
- 编译器
- 类库
- 工具
- Java虚拟机
2.Java语言中的新特性
2.1.Lambda表达式和函数式编程接口
Lambda表达式是整个Java 8体系中最让人期待的特性,它允许我们將函数作为一个方法的参数传递到方法体中或者將一段代码作为数据,这些概念有过函数式编程经验的会比较熟悉,很多基于Java虚拟机平台的语言(Groovy、Scala...)都引入了Lambda表达式。
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 );
} );
lambda表达式可以引用类的成员和局部变量(隐式的使用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 ) );
lambda表达式还可以返回一个值,返回值的类型由编译器推断,如果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;
} );
语言设计者们在现存的功能中如何更友好的支持lambda表达式上做出了大量的思考。因此出现了函数式接口的概念,一个函数接口中只能声明一个单独的函数。因此,它可以隐式的转换为lambda表达式。java.lang.Runnable和java.util.concurrent.Callable就是两个很好的函数式接口的例子。在编程实践中,函数式接口是相当容易被破坏的,如果有人在接口的定义中添加了另外一个方法,它將不再是函数式接口而且编译过程可能会失败。为了解决这种易破坏性并且显式的声明该接口属于函数式接口,Java 8新增了一个特殊的注解@FunctionalInterface,我们来看一下一个简单的函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
有一件事需要注意:default和static方法并不破坏函数式接口的约定,可以声明如下:
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
lambda表达式是Java 8最大的卖点,它潜在的吸引越来越多的开发者使用这个伟大的平台并且提供了先进的纯Java支持的函数式编程的概念。更多细节请参考官方文档。2.2.接口的default和static方法
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声明了一个default方法notRequired()。类DefaultableImpl实现了该接口,保留了notRequired()方法默认的实现,另外一个类OverridableImpl重写了默认的实现。
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面一小段代码展示了上例中static方法和default方法的使用:
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
default方法在jvm中的实现是非常高效的,并且提供了方法调用的字节码指令。default方法允许对现有的Java接口进行演变而不打断编译过程。在java.util.Collection接口中新增stream(), parallelStream(), forEach(), removeIf(), …方法就是很好的例子。
2.3.方法引用(Method References)
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<T>::new,请注意构造函数没有参数。
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 );
运行这些案例,控制台输出:
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.重复注解(Repeating annotations)
自从Java 5 引入注解以来,这个特性变得非常流行并且被广泛使用。然而,注解的一个使用限制是,在同一个位置,相同的注解不能声明两次。Java 8解除了这个规则并且引入了重复注解的概念,它允许相同的注解在同一个位置声明多次。
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() );
}
}
}
我们可以看到,Filter是使用@Repeatable修饰的注解类。Filters注解是Filter注解的持有者,但是Java编译器试图向开发人员掩饰它的存在。在Filterable接口中定义了两次Filter。
filter1
filter2
更多细节请查看
官方文档。
2.5.更佳的类型引用
Java 8编译器在类型引用上做了很大的改进。在很多情况下,,显式的类型参数可以由编译器推断以保持代码的简洁,我们来看一下下面的例子。public class Value< T > {
public static< T > T defaultValue() {
return null;
}
public T getOrDefault( T value, T defaultValue ) {
return ( value != null ) ? value : defaultValue;
}
}
这里使用Value<String>类型。
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.<String>defaultValue()。
2.6.扩展注解支持
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是描述注解使用环境的两个新的元素。Java注解处理API也发生了一些细微的变化去识别那些新的注解类型。
3.Java编译器新特性
3.1.参数名
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使用人员,为编译器增加-parameters 参数支持可以使用如下配置:
<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>
在支持Java 8的Eclipse发行版Eclipse Kepler SR2中,如下图所示,勾选相关选项:
此外,验证参数名称的可用性,可以使用Parameter类的isNamePresent()方法。
4.Java库的新特性
4.1.Optional
著名的空指针异常(NullPointerException)是迄今为止最流行的导致Java应用程序失败的原因。很久以前,伟大的Google Guava项目提供了Optional去解决空指针异常,冗余的检查null的代码,鼓励着程序员写更加简洁的代码。受到Google Guava的启示,Optional成为Java 8 库的一部分。我们將会一起看一下Optional的使用案例:可以为空的引用,但实际使用中不能让它为空
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()返回true,如果Optional实例持有非空的值,否则为false。orElseGet()提供一种回调机制,为了防止Optional持有值为null,接收一个函数提供一个默认的值。map()函数转换现有的Optional的值,返回一个新的Optional实例。orElse()方法和orElseGet()类似,但是它接收一个默认的值,程序输出如下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
我们再简单的看一下另外一个例子:
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!
更多细节请查看
官方文档。
4.2.流(Streams)
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中持有Status类型和Integer类型的引用points,然后我们向集合中添加Task对象:
final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
第一个问题是我们需要知道所有对象中Task的status属性值为OPEN的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形式,接着调用filter操作过滤掉状态为CLOSED的Task对象,下一步是调用mapToInt操作將Task流转换为Integer流,使用每个实例的Task::getPoints方法。最后使用sum方法,计算出最终结果。
stream的另一个价值是并行操作,我们来看下面一个计算所有Task对象points之和的例子:
// 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 );
这个例子和上一个比较类似,不同的是使用parallel方法处理所有的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对象的points值占整个集合中所有Task对象points属性之和的百分比:
// 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 );
控制台输出:
[19%, 50%, 30%]
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 );
}
onClose方法的调用返回一个等价的流和一个额外的关闭处理程序。stream的close()方法调用时,关闭处理程序会被执行。
4.3.日期/时间 API(JSR310)
// 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
接下来我们要看的类是LocaleDate和LocalTime。LocalDate只持有ISO-8601日历系统中的日期部分而没有时区部分。而LocalTime只持有IOS-8601日历系统中的时间部分,没有时区部分。LocaleDate和LocaleTime都能够使用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將LocaleDate和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() );
上面的例子中我们使用Duration类计算出两个日期相差的间隔,控制台输出:
Duration in days: 365
Duration in hours: 8783
总得来讲Java 8中新的日期/时间API是非常实用的。更多细节请查看
官方文档。
4.4.犀牛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
在后面的Java tools章节中将会再对犀牛引擎进行介绍。
4.5.Base64编码
最后,Java 8发行版的标准库中新增了Base64编码支持,它的使用较为简单,例如: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.并行的数组工具类Arrays(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()方法在数组中填充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.并发(Concurrency)
为了支持基于stream和lambda表达式的聚合操作,java.util.concurrent.ConcurrentHashMap类中增加了新的方法。java.util.concurrent.ForkJoinPool类也增加了方法以支持线程池。增加了用于控制读写加锁操作的类java.util.concurrent.locks.StampedLock,和java.util.concurrent.locks.ReadWriteLock相比会是更好的选择。
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
5.新的Java工具
Java 8中新增了一些命令行工具,在本节中我们来看一下它们的有趣之处。5.1.犀牛引擎:jjs
function f() {
return 1;
};
print( f() + 1 );
在控制台中运行以下命令:
2
更多细节请查看相关
官方文档。
5.2.类依赖关系分析工具:jdeps
jdeps org.springframework.core-3.0.5.RELEASE.jar
控制台输出大量信息,这里我们只截取一小段。依赖关系以包进行分组,如果在类路径中找不到,將显示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
6.JVM新特性
7.相关资源
- What’s New in JDK 8: http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html
- The Java Tutorials: http://docs.oracle.com/javase/tutorial/
- WildFly 8, JDK 8, NetBeans 8, Java EE 7: http://blog.arungupta.me/2014/03/wildfly8-jdk8-netbeans8-javaee7-excellent-combo-enterprise-java/
- Java 8 Tutorial: http://winterbe.com/posts/2014/03/16/java-8-tutorial/
- JDK 8 Command-line Static Dependency Checker:http://marxsoftware.blogspot.ca/2014/03/jdeps.html
- The Illuminating Javadoc of JDK 8: http://marxsoftware.blogspot.ca/2014/03/illuminating-javadoc-of-jdk-8.html
- The Dark Side of Java 8: http://blog.jooq.org/2014/04/04/java-8-friday-the-dark-side-of-java-8/
- Installing Java™ 8 Support in Eclipse Kepler SR2: http://www.eclipse.org/downloads/java8/
- Java 8: http://www.baeldung.com/java8
- Oracle Nashorn. A Next-Generation JavaScript Engine for the JVM:http://www.oracle.com/technetwork/articles/java/jf14-nashorn-2126515.html
更多推荐
所有评论(0)