Java 8新特性-终极版(翻译Java 8 Features Tutorial – The ULTIMATE Guide)

来源:互联网 发布:html5连接mysql数据库 编辑:程序博客网 时间:2024/05/29 23:25

1. 简介

毫无疑问,Java 8的发布是Java 自2004年发布Java 5以来最大的事件。它带来了很多的新特性,包括语言特性、编译器、库、工具以及JVM。下面我们将一一探索这些新特性并且看如何在实际中运用。

2. Java语言的新特性

Java 8是一个意义重大的版本。有些人说它花了N久的时间来实现了每一个Java开发者都向往的特性。

2.1 lambda表达式

lambda表达式是Java 8中最大和最令人期待的改变,它允许我们将函数当成参数传递给方法(也成为闭包)。Java之前是只能使用匿名内部类来代替lambda表达式。最简单的lambda表达式可以由逗号分隔的参数列表,->符号和语句块组成。

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

请注意参数e会由编译器去推断出它的类型。当然,你可以明确的告知编译器是什么类型,如下。
  

Arrays.asList("str1", "str2", "str2").forEach((String str) -> System.out.println(str));

如果lambda表达式是稍微复杂的语句块,也可以使用{}花括号括起来。

Arrays.asList("str1", "str2", "str2").forEach((String str) -> {    System.out.println(str)  });

lambda表达式可以引用类变量和局部变量,局部变量会变成隐式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 ) );

但如果lambda表达式有多行语句,那么return语句则是必须的。

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {    int result = e1.compareTo( e2 );    return result;} );

lambda表达式如果有返回值,返回值也是由编译器推理的出的。lambda的设计者为了让现有的功能与lambda表达式良好兼容,产生了函数接口这个概念。函数接口指的是只有一个函数的接口(Interface),这样的接口可以隐式转换为lambda表达式,比如Runnable接口。在实践中,函数式接口很脆弱,只要开发者在该接口添加一个函数就导致编译失败。为了避免这种情况的发生,Java 8 提供了 @FunctionalInterface 这个注解。

@FunctionalInterfacepublic interface Functional {    void method();}

不过Java 8 中新增的接口默认方法和静态方法不会破坏函数接口的定义,如下。

@FunctionalInterface  public interface FunctionalMethod {    void method();    default void defaultMethod() {      System.out.println("default method");    }  }

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

在Java 8中,接口多了默认方法以及静态方法这两个概念。在Java 8中,可以在接口中使用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()。在DefaultableImpl中,它可以不需要实现notRequired()方法,但是,在OverridableImpl接口中,它却实现notRequired()方法。

另一个有趣的地方是接口可以声明静态方法。

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 implementationOverridden implementation

由于JVM提供了字节码层级的支持,默认方法在JVM上的实现是很高效的。默认方法可以使得接口在不破坏现有的编译进程的基础上得到扩展。在java.util.Collection中的stream()、forEach()中均得到了良好地实现。

虽然很强大,但在使用默认方法的地方必须小心谨慎。必须要确认在必要的前提下才使用默认方法,否则在复杂的继承体系中,有可能会引起歧义和编译错误。

2.3 方法引用

方法引用提供了一种非常方便地能直接引用类或者实例存在方法或者构造函数。结合使用lambda表达式,方法引用使得语法显得十分紧凑和简洁。

在下面的例子中,类Car中有几种不同的方法定义,可以帮助大家区分不同类型的引用。

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::new。需要注意的是,类Car的构造函数没有参数。

final Car car = Car.create( Car::new );final List< Car > cars = Arrays.asList( car );

第二种类型的方法引用是静态方法,使用语法Class::static_method来引用。需要注意的是,该静态方法只接受一个Car对象作为参数。引用静态方法的语法与Java 7之前的语法几乎相同,就是讲.替换成::。

cars.forEach( Car::collide );

第三种类型的方法引用是对任意对象(同一个类中)的成员方法,使用语法Class::method来引用。需要注意的是,该方法没有参数。在下面的例子中,遍历了cars列表,用过Car::repair来引用这个类型任意一个Car对象的repair方法。

cars.forEach( Car::repair );

第四种类型的方法引用是对具体某个对象的成员方法,使用语法instance::method来引用。需要注意的是,该方法只接收一个Car类型的参数。

final Car police = Car.create( Car::new );cars.forEach( police::follow );

2.4 重复注解

自Java 5发布注解特性之后,注解这一特性得到了广泛的使用。但是,注解的限制是在同一处不能声明多个同类型的注解。Java 8打破了这一特点,并推出了重复注解的特性。Java 8允许同类型的注解可以在同一处声明多次。

需要注意的是,允许重复注解的注解,自己需要加上@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可以使用两次Filter注解声明。

同时,反射API也提供了新的getAnnotationsByType的方法,这个方法会返回某个类型的重复注解,以上运行结果如下。

filter1filter2

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;    }}
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 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_USE和ElementType.TYPE_PARAMETER是新增的两种注解,用于描述注解的使用场景。

3. Java编译器的新特性

3.1 参数名称

在很长时间里,很多Java程序员都很期待在运行时获得参数名称。现在,Java 8终于把这一特性添加到语言层面,通过使用反射API的Parameter.getName()方法来获得。

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

这一特性在Java 8中是默认关闭的, 在编译这个类的时候,如果未添加-parameters参数然后执行,会看到以下结果。

Parameter: arg0

如果添加了-parameters参数之后,执行结果如下。

Parameter: args

4. Java官方库新特性

Java 8增加了一些类以及扩展了一些已有的类来支持并发、函数式编程、data/time等等。

4.1 Optional

目前,空指针异常是导致Java程序发生崩溃的最大因素。很久之前,Google Guava项目被开发出使用了Optionals来解决这个问题。现在Optional已经成为了Java标准库的一部分。

Optional其实只是一个容器,它可以持有一个对象的实例或者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()方法可以判断Optional中的实例是否为空。orElseGet()方法提供了一种当Optional里实例为空时可以获取默认值的方式。map()方法将当前Optional实例转换成一个新的Optional实例。orElse()方法与orElseGet()方法类似,但它接收的是一个值而不是一个方法。下面是输出结果。

First Name is set? trueFirst Name: TomHey Tom!

4.2 Stream

新添加的Stream API将如今很流行的函数式编程引入了Java中。这是目前为止最大的一次对Java库的完善,以便开发者能写出更高效的、整洁的代码。

Stream API使得集合类的操作变得高效很多。

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,另外还有一个成员属性status,可以是OPEN或CLOSED。现在假设有一个Task集合对象tasks。

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之前,最普遍的方法就是对集合进行遍历然后判断每个实例的状态再进行加和。在Java 8里,可以使用Stream解决。在Java 8中,Stream的定义是:A sequence of elements supporting sequential and parallel aggregate operations.从定义中可以明确两点:(1)
Stream是一组元素的容器。(2)Stream支持顺序的和并发的汇聚操作。下面来看Stream如何解决上述问题。Stream可以理解为高级的Iterator。初级版本的Iterator只能一个个的遍历元素再对其进行操作。作为高级版本的Stream,用户只需要给出对其包含元素执行的操作,比如判断Task对象中的status是否是OPEN状态的,操作就会被执行到每一个元素上。

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

Stream的语法可以总结为三部分,stream()方法是创建Stream,filter()方法是转换Stream,最后一部分操作比如sum()、count()方法是聚合。对照上面的例子,首先是根据tasks集合创建了Stream,随后使用filter操作转换了Stream,排除掉了status是CLOSED的Task。然后调用了mapToInt操作进一步将保存Task的Stream转换成保存Integer的Stream。最后再求出该Stream的总和。

在学习接下来的知识之前,我们先要记住一些关于Stream的知识点。在Stream上的操作可分为中期操作和晚期操作。中期操作会创建一个新的Stream,并将符合条件的元素放入新创建的Stream中。晚期操作(比如forEach和sum),会遍历Stream并得出结果或者附带结果。

Stream另一个很有用的特性就是支持并行处理。以下代码是求出tasks集合中所有point属性之和。

// Calculate total points of all tasksfinal 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提供的API能很方便的实现。

// Group tasks by their statusfinal 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中的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%]

最后,Stream API实际上不只是适用于Java集合。精简的IO操作比如逐行读取文本也可以很好的使用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,当Stream的close()方法被调用时这个句柄就会执行。Stream API、Lambda表达式还有接口默认方法和静态方法的引用,是Java 8对软件开发的现在范式的相应。

4.3 Date/Time API

Java 8引入了新的Date/Time API来改进时间、日期的处理。时间和日期的管理一直是Java开发者头疼的问题。

我们接下来看看java.time包中的关键类和例子。首先,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.282Z1397315969360

另外我们需要关注的类是LocalDate和LocalTime。LocalDate包含ISO-8601日历系统中的日期部分。LocalTime则仅仅包含日历系统中的时间部分。

// Get the local date and local timefinal LocalDate date = LocalDate.now();final LocalDate dateFromClock = LocalDate.now( clock );System.out.println( date );System.out.println( dateFromClock );// Get the local date and local timefinal LocalTime time = LocalTime.now();final LocalTime timeFromClock = LocalTime.now( clock );System.out.println( time );System.out.println( timeFromClock );

控制台输出如下。

2014-04-122014-04-1211:25:54.56815:25:54.568

LocalDateTime类结合了LocalDate和LocalTime,但是不包含ISO-8601日历系统中的时区信息。

// Get the local date/timefinal LocalDateTime datetime = LocalDateTime.now();final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );System.out.println( datetime );System.out.println( datetimeFromClock );

控制台输出如下。

2014-04-12T11:37:52.3092014-04-12T15:37:52.309

如果需要特定时区的date/time信息,则可以使用ZoneDateTime,它保存了ISO-8601日期系统的日期和时间,而且有时区信息。

// Get duration between two datesfinal 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 in days: 365Duration in hours: 8783

4.4 Nashorn JavaScript引擎

Java 8提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一个版本的实现。这类ScriptEngine遵循相同的规则,允许Java和JS相互调用。

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.NashornScriptEngineResult: 2

4.5 Base64

终于,Base64编码支持终于被添加进入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 );    }}

4.6 并行数组

Java 8新增了很多方法,用于支持并行数组处理。最重要的方法是parallelSort(),可以显著加快多核机器上的数组排序。

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()方法进行排序。

5. 新的Java工具

Java 8新增了一些命令行工具,这部分讲解这些工具。

5.1 Nashorn引擎

jjs是一个基于标准Nashorn引擎的命令行工具,可以接收JS代码并执行。我们在func.js文件中输入以下内容。

function f() {      return 1; }; print( f() + 1 );

可以在命令行中执行命令:jjs func.js,控制台输出结果如下。

2

5.2 类依赖分析器:jdeps

jdeps是一个很棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以class文件、目录或者jar文件为输入,然后会把依赖关系输出到控制台。

我们可以使用jdeps命令分析Spring Framework库,这里仅仅分析其中的一个jar包。

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

控制台输出结果如下。

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新特性

使用Metaspace代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。

7. 总结

以上内容只是对Java 8新特性的简单总结,由于篇幅有限,未能深入介绍,对于一些细致的知识点可通过阅读其他博文来了解,感谢阅读。

原创粉丝点击