Home » Java核心 » Java 8特性指南 – 终极手册 (PDF下载)

About Andrey Redko

Andrey Redko
Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).

Java 8特性指南 – 终极手册 (PDF下载)

编者按距离Java 8的发布已经有段时间了,就目前看来它的确是一个非常重要的版本。我们之前已经在JavaCodeGeeks发布了一系列专题文章,如玩转Java 8-Lambda表达式和并发Java 8日期/时间API入门:LocalDateTime 以及 JDK8时代的抽象类vs.接口同时我们也从其它来源处引用了15个必读的Java 8教程。当然,我们也对它的一些短板进行了验证,如Java 8的暗面。为了读者们更好的阅读体验,现在是时候整合所有Java 8的主要特性于一处了。一起来享受它吧!

 

1. 简介

毫无疑问,Java 8的发布是自Java 5(它的发布已远在2004年)以来在Java世界中最重大的事情。它带来了超多的新特性,这些特性分别被加入到Java语言本身、Java编译器、类库、工具类和JVM(Java虚拟机)。在这篇入门指南中,我们将概览所有的改变,同时也会通过实例演示一下它们的不同使用场景。

这篇指南包含如下几部分,每一部分都分别剖析其具体特性:

  • 语言
  • 编译器
  • 类库
  • 工具类
  • 运行时 (JVM)

2. Java语言的新特性

无论从哪方面来说Java 8都是一个重大版本。也许有人会说,为了加入这个每一个Java开发者都期望的新特性耗费了太长时间。在本节我们将谈谈这些新特性。

2.1. Lambda表达式和函数式接口

Lambda表达式(也叫闭包)是整个Java 8版本中最大和最令人期待的语言改进。它使得我们可以将函数作为方法的参数(传递函数),或者将代码看作数据:这是每一个函数式开发者都非常熟知的概念。很多基于JVM平台的语言(如Groovy,Scala等),都是在语言的诞生之初就支持lambda表达式;但是对于Java开发者来说,别无选择,只能使用繁冗的匿名类来替代lambda表达式。

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 ) );

如果lambda的代码块太过复杂,也可以使用花括号包裹起来,就像定义普通的Java方法一样。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda表达式也可以引用类成员变量和本地变量(如果不是final的话会被隐式设置为“有效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.Runnablejava.uril.concurrent.Callable是函数式接口的两个典型示例. 但是在实践中,函数式接口是脆弱的:如果有人在接口定义中又加入了一个方法,那么它就不再是一个函数式接口了,而且会导致编译过程失败。为了克服这种脆弱性,同时又能显式声明一个接口是函数式接口,Java 8新增了一种特别的注解@FuncionalInterface(Java类库中的所有的已有接口都已经加上了@FunctionalInteface注解)。我们来看一看如何定义简单的函数式接口:

@FunctionalInterface
public interface Functional {
    void method();
}

有一点需要注意:默认方法和静态方法不会对函数式接口有任何影响,举个例子:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
        
    default void defaultMethod() {            
    }        
}

Lambda是Java 8的最大卖点。它拥有巨大的潜力,来吸引越来越多的开发人员加入到这个伟大的平台;并且对纯Java中的函数式编程概念给予最大的支持。更多信息请参考官方文档

2.2. 接口的默认方法和静态方法

Java 8从两个概念扩展了接口的定义:默认方法和静态方法。默认方法使得接口有点类似于Traits语法但是面向的目标不同。它允许添加新方法到已有接口中,但是不会破坏那些基于老版接口实现的代码的二进制兼容性。

默认方法和抽象方法的区别在于:抽象方法是必须要实现的,而默认方法不是。相反,每个接口必须提供一个所谓的默认方法实现,所有的实现类都会默认继承得到这个方法(如果需要也可以重写这个默认实现)。我们来看看下面这个例子:

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()。有一个类DefaultableImpl实现了这个接口,它没有对默认方法做任何更改。另一个类OverridableImpl,则重写了接口中的默认实现,实现了自己的逻辑。

Java 8提供的另一个很有意思的特性是接口可以声明静态方法(同时提供实现)。举个例子:

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier< Defaulable > supplier ) {
        return supplier.get();
    }
}

下面这段代码将之前例子中提到的默认方法和静态方法放在了一起:

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中默认方法的实现非常高效,而且方法调用的字节码指令支持默认方法。默认方法使得已有的Java接口能够改进而不会导致编译过程失败。接口java.util.Collection中新增了大量的方法,都是很好的示例,如: stream(), parallelStream(), forEach(), removeIf(),等等。

虽然默认方法很强大,我们还是要谨慎使用它:在将一个方法定义为default方法之前,最好三思是不是必须要这么做,因为它可能在层级复杂的情况下引起歧义和编译错误。更多信息请参考官方文档

2.3. 方法引用

方法引用提供了一种很有用的语法,来直接引用Java类或者对象(实例)中的已有方法或者构造函数。将方法引用和Lambda表达式结合起来使用,可以使得语言结构更加紧凑整洁,不需要再使用任何复杂的引用代码。

下面让我们以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<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 );

运行上面的Java程序,在控制台会看到如下输出(实际的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. 重复型注解

自从Java5引入对注解的支持,这个特性已经变得越来越受欢迎而且被广泛应用。不过,对注解的使用有一个限制,就是同一个注解不能在同一个位置多次声明。Java 8打破了这个限制,引入了重复型注解(repeating annotations),它支持在注解声明的位置,可以多次声明相同的注解。

重复型注解自身应该被注解为@Repeatable。事实上,这并不是一个语言层面的变化,而更像是编译器层面的“小把戏”,因为从技术层面来说是完全一样的。我们通过一个简单的例子来看看:

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.class)。接口Filters只是一个用来持有注解Filter的“容器”,所以Java编译器力图对开发者们隐藏它。因此可以看到,在接口Filterable中声明了2次Filter注解(却并未提及接口Filters)。

此外,反射API提供了一个新方法getAnnotationsByType()用来返回特定类型的所有重复型注解(请注意,Filterable.class.getAnnotation(Filters.class)将返回Filters的实例,这是由编译器自动注入实现的)。

该程序的输出如下:

filter1
filter2

更多详细信息请参考官方文档

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<String>类型的用法。

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<String> defaultValue()

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_USEElementType.TYPE_PARAMETER是两个新增加的元素类型,用来描述注解的适用场景。注解处理API(Annotation Processing API)也做了一些小的改动,以使之能够正确处理这些Java编程语言中新加入的的注解类型。

3. Java编译器的新特性

3.1. 参数名

很长时间以来,Java开发者们就一直在尝试各种办法,用来在Java字节码中保存方法的参数名,同时使得它在运行时可用(例如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使用者,参数-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(请查看下载指南)能够支持Java 8,而且提供了编译参数设置项,参看下图。

Picture 1. Configuring Eclipse projects to support new Java 8 compiler –parameters argument.

图1. 在Eclipse项目中设置支持Java 8的编译器参数–parameters.

此外,类Parameter中有一个很便捷的方法isNamePresent()可以用来验证参数名称的可用性。

4. Java类库的新特性

Java 8新增了很多类,同时也扩展了很多已有的类,以更好的支持现代并发、函数式编程、日期/时间和其它操作。

4.1. Optional

著名NullPointerException异常是迄今为止Java应用程序故障中最常见的原因。很久以前,伟大的Google Guava项目推出了Optional,作为NullPointerException的替代者;它不赞成在代码库中添加空值(null)检查造成代码污染,而是建议开发者们书写更整洁的代码。经过Google Guava的官方授权,如今Optional成为Java 8类库的一员。

Optional只是一个容器:它可以是一个类型为T或者空值(null)。它提供了很多有用的方法,从而使得再也无需去显式检查空值(null)。请参考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!" ) );

如果Optional的实例含有非空值的话,方法isPresent()返回true;反之返回false。方法orElseGet()提供了一种回退机制:它的实现是接受一个能够产生默认值的方法;从而可以防止Optional值为空。方法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

新添加的Stream APIjava.util.stream)为Java引入了现实世界中的函数式编程风格。这是迄今为止Java类库最全面的一次改进,旨在让Java开发人员可以编写有效、整洁、简洁的代码,从而显著提高其工作效率。

Stream API使得集合操作大为简化(我们后面会看到其实不仅仅限于Java集合类)。下面我们通过一个简单的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类有一个属性-点数(一个虚拟的任务复杂度),状态可以是OPENCLOSED。我们首先创建一个小的Task集合。

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

我们要解决的第一个问题是,所有状态为OPEN的任务一共有多少点数?在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::getPoints方法,mapToInt操作可以把Task Stream转换为Integer Stream。最后,使用sum方法,计算出所有点数的总和,进而产生最终结果。

在继续看下一个例子之前,我们要牢记Stream的几点说明(更多详情)。Stream操作分为“中间操作”和“终端操作”。

“中间操作”会返回一个新的Stream。这些操作总是延迟执行的,当执行一个中间操作比如filter时,并不会真的执行任何过滤操作;而是创建一个新的Stream,当这个新的Stream被遍历时,它里面会包含原来Stream中与参数中指定的谓词(Predicate)匹配的那些元素。

“终端操作”,如forEach或者sum,则会遍历stream从而产生结果或者附带结果。在终端操作执行完毕后,Stream的管道会被视为已经消费完毕,不可再次使用。在大多数情况下,终端操作是“贪婪的”,总是能够即时完成数据的遍历操作。

Stream还有另一个变化是对它现在提供对并行处理的天然支持。我们来看一个例子,如何统计所有任务中的点数总和。

// 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所占的总体百分比(或比重)。

// 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%]

最后,正如我们之前提到过的,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()方法被调用时会执行。

Stream API和Lambda表达式,还有接口中默认方法和静态方法支持的方法引用,都是Java 8对现代软件开发中的编程范式的响应。更多详细信息请参考官方文档

4.3. 日期/时间API (JSR 310)

Java 8引入了新的日期-时间API(JSR 310),改进了日期和时间的操作管理。日期和时间操作曾经一度是让Java开发人员最感痛苦的问题,在java.util.Date之后引入的java.util.Calendar也丝毫没有改善这种状况(甚至可以说它使得日期时间操作更加令人费解)。

这就导致了Joda-Time的诞生:一个面向Java的日期/时间API的理想替代者。Java 8中新的日期-时间API(JSR 301) 深受Joda-Time的影响,可以说是尽取其精华。新的java.time包中包含了所有关于日期、时间、日期/时间、时区、Instant、Duration和时钟操作的类。设计这个API的时候已经着重考虑了原有方法的不可变性:不允许任何更改(来源于java.util.Calendar的前车之鉴)。如果必须改变的话,就会返回一个新的实例。

我们来看一看其中几个关键的类以及他们的用法。第一个是Clock类,它可以通过时区来获得当前Instant、日期和时间。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

我们要看的另外两个类是LocaleDateLocaleTimeLocaleDate只包含ISO-8601日历系统的中日期部分,不含时区信息;与此对应,LocaleTime只包含ISO-8601日历系统中的时间部分,不含时区信息。LocaleDateLocaleTime都可以通过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类整合了LocaleDateLocaleTime,它含有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() );

上面这个例子计算了日期2014年4月16日2015年4月16日之间的差异(以天计、以小时计)。控制台上的示例输出如下:

Duration in days: 365
Duration in hours: 8783

Java 8中新的日期/时间API给人的总体印象是非常积极的。其中一部分原因是因为它是建立在经历了实战考验的Joda-Time的基础之上;还有一部分原因是这一次它终于能够得到认真对待,而且听取了开发者们的声音。更多详细信息请参考官方文档

4.4. Nashorn JavaScript引擎

Java 8引入了一个新的Nashorn JavaScript引擎,它允许在JVM上开发和运行特定类型的JavaScript应用。Nashorn JavaScript引擎仅仅是javx.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

我们在以后的文章《Java 8工具类详解》中会继续讨论Nashorn。

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. 并发

java.util.concurrent.ConcurrentHashMap中加入了一些新的方法来支持聚集操作。这些聚集操作基于新增加的stream功能和lambda表达式。此外,类java.util.concurrent.ForkJoinPoolclass也新增了一些方法,用来支持公共池(Common Pool,可以查看我们的Java并发免费课程)。

新加入的java.util.concurrent.locks.StampedLock类,提供了一种基于能力的锁,它含有三种模式,用于控制读/写访问(它可以视为臭名昭著的java.util.concurrent.locks.ReadWriteLock的更好的替代者)。

java.util.concurrent.atomic包中新加入的类包括:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

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

更多详细信息请参考官方文档

5.2. 类的依赖关系分析:jdeps

jdeps确实可说是一个非常好用的命令行工具。它可以显示Java类文件的包级或类级的依赖关系。它的合法输入包括:类文件、目录、或JAR文件。默认情况下,jdeps会把分析结果输出到系统标准输出(控制台)。

下面我们以流行的Spring Framework为例,来看一下依赖关系报告。简单起见,我们只分析其中一个JAR文件: org.springframework.core-3.0.5.RELEASE.jar

jdeps org.springframework.core-3.0.5.RELEASE.jar

这个命令会输出很多内容,所以我们只看其中一部分。在结果中依赖关系是按包分组显示的。如果在classpath中找不到任何依赖,会显示“未找到”(not found)。

org.springframework.core-3.0.5.RELEASE.jar -> C:Program FilesJavajdk1.8.0jrelibrt.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. Java运行时 (JVM)的新特性

JVM移除了PermGen空间,取而代之的是MetaspaceJEP 122)。相应的,JVM参数-XXPermSize-XXMaxPermSize已被替换为-XXMetaSpaceSize-XX MaxMetaspaceSize

7. 结语

未来就在这里:Java 8提供了这些新特性以使开发者们更加高效工作,从而推动这个伟大的平台向前发展。虽然现在就将生产系统迁移至Java 8平台还为时尚早,但是在未来几个月,它的采用率应该会逐渐增多。不管怎样,现在可以开始准备你的代码库,使之能与Java 8兼容,并准备好随时切换至Java 8;一旦Java 8被证实是足够安全和稳定的,就可以立即开始迁移。

作为社区对Java 8的认可和支持,Pivotal最近发布了Spring Framework 4.0.3,完全支持符合生产环境要求的Java 8平台开发。

如果你喜欢这篇文章,请订阅我们的newsletter来享受每周一次的更新推送和免费技术白皮书!此外,请访问JCG学院查看更多高级培训课程!

非常欢迎你们能够为这么多令人振奋的Java 8新特性提供意见和建议!

8. 相关资源

这里还有更多资源更加深入的讨论了Java 8的不同特性:

Translated by: Vincent Jia
This post is a translation of Java 8 Features Tutorial – The ULTIMATE Guide from Andrey Redko
(0 rating, 0 votes)
You need to be a registered member to rate this.
1 Comment Views Tweet it!
Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Medwyn
Medwyn
5 years ago

thx for your sharing