马上Java14要来了,你还不知道Java8的新特性?

401 阅读15分钟
原文链接: mp.weixin.qq.com

JDK8的新特性主要有以下几个:

  • Lambda表达式

  • 函数式接口

  • 方法引用

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

  • Optional

  • Stream API

  • 并行数组

1、Lambda表达式

Lambda表达式, 也可以称为闭包,它是Java8这个版本最重要的新特性.Lambda允许把函数作为一个方法的参数, 可以使代码变得更加简洁.

基本语法:

(参数列表) -> {代码块}

注意:

  • 参数类型可以省略,编译器可以自己判断

  • 如果只有一个参数,圆括号也可以省略

  • 代码块如果只要一行代码,大括号也可以省略

  • 如果代码块是一行,且是有结果的表达式,return可以省略

示例1: 一个参数

先准备一个集合:

List<Intger> list = Arrays.asList(10,5,25,-15,20);

遍历集合中的元素,并且打印.

jdk7的方式:

for (Integer i : list) {    System.out.println(i);}

jdk8 多了一个foreach()方法,

list.foreach(i -> System.out.println(i)); //只有一个参数,可以省略小括号

示例2: 多个参数

还是使用示例1的集合, 对集合进行排序,

jdk7:  需要通过匿名内部类来构造一个Comparator

Collections.sort(list,new Comparator<Integer>() {    @Override    public int compare(Integer o1, Integer o2) {        return o1 - o2;    }});System.out.println(list);// [-15, 5, 10, 20, 25]

jdk8: 新增了一个集合API: sort(Comparator c)方法,接收比较器,可以使用Lambda来代替Comparator的匿名内部类:

list.sort((i1,i2) -> {return i1 - i2;});//符合代码块是一个有返回值的表达式,可以省略return和大括号list.sort((i1,i2) -> i1 - i2);Syetem.out.println(list);  //[-15, 5, 10, 20, 25]

示例3:  把Lambda赋值给变量

Lambda表达式的实质其实还是匿名内部类,所以我们可以把Lambda表达式赋值给某个变量.

// 将一个Lambda表达式赋值给某个接口:Runnable task = () -> {    // 这里其实是Runnable接口的匿名内部类,我们在编写run方法。    System.out.println("hello lambda!");};new Thread(task).start();

示例4: 隐式final

Lambda表达式的实质其实还是匿名内部类,而匿名内部类在访问外部局部变量时,要求变量必须声明为final !不过我们在使用Lambda表达式时无需声明final ,这并不是说违反了匿名内部类的规则,因为Lambda底层会隐式的把变量设置为final ,在后续的操作中,一定不能修改该变量:

正确示范:

// 定义一个局部变量int num = -1;Runnable r = () -> {    // 在Lambda表达式中使用局部变量num,num会被隐式声明为final    System.out.println(num);};new Thread(r).start();// -1

错误案例:

// 定义一个局部变量int num = -1;Runnable r = () -> {    // 在Lambda表达式中使用局部变量num,num会被隐式声明为final,不能进行任何修改操作    System.out.println(num++);};new Thread(r).start();//报错

2、函数式接口

在实践中,函数接口是非常脆弱的,只要有人在接口里添加多一个方法,那么这个接口就不是函数接口了,就会导致编译失败。Java 8提供了一个特殊的注解@FunctionalInterface 来克服上面提到的脆弱性并且显示地表明函数接口。而且jdk8版本中,对很多已经存在的接口都添加了@FunctionalInterface 注解,例如Runnable 接口:

  • Function类型接口

@FunctionalInterfacepublic interface Function<T, R> {  // 接收一个参数T,返回一个结果R    R apply(T t);}

Function代表的是有参数,有返回值的函数。 还有很多类似的Function接口:

接口名

描述
BiFunction<T,U,R> 接收两个T和U类型的参数,并且返回R类型结果的函数
DoubleFunction<R> 接收double类型参数,并且返回R类型结果的函数
IntFunction<R> 接收int类型参数,并且返回R类型结果的函数
LongFunction<R> 接收long类型参数,并且返回R类型结果的函数
ToDoubleFunction<T> 接收T类型参数,并且返回double类型结果
ToIntFunction<T> 接收T类型参数,并且返回int类型结果
ToLongFunction<T> 接收T类型参数,并且返回long类型结果
DoubleToIntFunction 接收double类型参数,返回int类型结果
DoubleToLongFunction 接收double类型参数,返回long类型结果

Consumer系列

@FunctionalInterfacepublic interface Consumer<T> {  // 接收T类型参数,不返回结果    void accept(T t);}

Consumer系列与Function系列一样,有各种衍生接口,这里不一一列出了。不过都具备类似的特征:那就是不返回任何结果。

  • Predicate系列

@FunctionalInterfacepublic interface Predicate<T> {  // 接收T类型参数,返回boolean类型结果    boolean test(T t);}

Predicate系列参数不固定,但是返回的一定是boolean类型。

  • Supplier系列

@FunctionalInterfacepublic interface Supplier<T> {  // 无需参数,返回一个T类型结果    T get();}

Supplier系列,英文翻译就是“供应者”,顾名思义:只产出,不收取。所以不接受任何参数,返回T类型结果。

3、方法的引用

方法引用使得开发者可以将已经存在的方法作为变量来传递使用。方法引用可以和Lambda表达式配合使用。

总共有四类方法引用:

语法 描述
类名::静态方法名 类的静态方法的引用
类名::非静态方法名 类的非静态方法的引用
实例对象::非静态方法名 类的指定实例对象的非静态方法引用
类名::new 类的构造方法引用

示例:

首先我们先准备一个集合工具类,提供一个方法:

 public class CollectionUtil{      /**       * 利用function将list集合中的每一个元素转换后形成新的集合返回       * @param list 要转换的源集合       * @param function 转换元素的方式       * @param <T> 源集合的元素类型       * @param <R> 转换后的元素类型       * @return       */     public static <T,R> List<R> convert(List<T> list, Function<T,R> function){            List<R> result = new ArrayList<>();            list.forEach(t -> result.add(function.apply(t)));            return result;      }  }

可以看到这个方法接收两个参数:

  • List<T> list :需要进行转换的集合

  • Function<T,R> :函数接口,接收T类型,返回R类型。用这个函数接口对list中的元素T进行转换,变为R类型

示例1: 类的静态方法引用

List<Integer> list = Arrays.asList(1000,2000,3000);

我们需要把这个集合中的元素转为十六进制保存,需要调用 Integer.toHexString() 方法:

public static String toHexString(int i) {    return toUnsignedString0(i, 4);}

这个方法接收一个 i 类型,返回一个String 类型,可以用来构造一个Function 的函数接口:

我们先按照Lambda原始写法,传入的Lambda表达式会被编译为Function 接口,接口中通过Integer.toHexString(i) 对原来集合的元素进行转换:

// 通过Lambda表达式实现List<String> hexList = CollectionUtil.convert(list, i -> Integer.toHexString(i));System.out.println(hexList);// [3e8, 7d0, bb8]

上面的Lambda表达式代码块中,只有对 Integer.toHexString() 方法的引用,没有其它代码,因此我们可以直接把方法作为参数传递,由编译器帮我们处理,这就是静态方法引用:

// 类的静态方法引用List<String> hexList = CollectionUtil.convert(list, Integer::toHexString);System.out.println(hexList);// [3e8, 7d0, bb8]

示例2: 类的非静态方法引用

接下来,我们把刚刚生成的String 集合hexList 中的元素都变成大写,需要借助于String类的toUpperCase()方法:

public String toUpperCase() {    return toUpperCase(Locale.getDefault());}

这次是非静态方法,不能用类名调用,需要用实例对象,因此与刚刚的实现有一些差别,我们接收集合中的每一个字符串 s 。但与上面不同然后s 不是toUpperCase() 的参数,而是调用者:

// 通过Lambda表达式,接收String数据,调用toUpperCase()List<String> upperList = CollectionUtil.convert(hexList, s -> s.toUpperCase());System.out.println(upperList);// [3E8, 7D0, BB8]

因为代码体只有对 toUpperCase() 的调用,所以可以把方法作为参数引用传递,依然可以简写:

// 类的成员方法List<String> upperList = CollectionUtil.convert(hexList, String::toUpperCase);System.out.println(upperList);// [3E8, 7D0, BB8]

示例3: 指定示例的非静态方法引用

下面一个需求是这样的,我们先定义一个数字Integer num = 2000 ,然后用这个数字和集合中的每个数字进行比较,比较的结果放入一个新的集合。比较对象,我们可以用IntegercompareTo 方法:

public int compareTo(Integer anotherInteger) {    return compare(this.value, anotherInteger.value);}

先用Lambda实现,

List<Integer> list = Arrays.asList(1000, 2000, 3000);// 某个对象的成员方法Integer num = 2000;List<Integer> compareList = CollectionUtil.convert(list, i -> num.compareTo(i));System.out.println(compareList);// [1, 0, -1]

与前面类似,这里Lambda的代码块中,依然只有对 num.compareTo(i) 的调用,所以可以简写。但是,需要注意的是,这次方法的调用者不是集合的元素,而是一个外部的局部变量num ,因此不能使用 Integer::compareTo ,因为这样是无法确定方法的调用者。要指定调用者,需要用 对象::方法名 的方式:

// 某个对象的成员方法Integer num = 2000;List<Integer> compareList = CollectionUtil.convert(list, num::compareTo);System.out.println(compareList);// [1, 0, -1]

示例4: 构造函数引用

最后一个场景:把集合中的数字作为毫秒值,构建出Date 对象并放入集合,这里我们就需要用到Date的构造函数:

/**  * @param   date   the milliseconds since January 1, 1970, 00:00:00 GMT.  * @see     java.lang.System#currentTimeMillis()  */public Date(long date) {    fastTime = date;}

我们可以接收集合中的每个元素,然后把元素作为 Date 的构造函数参数:

// 将数值类型集合,转为Date类型List<Date> dateList = CollectionUtil.convert(list, i -> new Date(i));// 这里遍历元素后需要打印,因此直接把println作为方法引用传递了dateList.forEach(System.out::println);

上面的Lambda表达式实现方式,代码体只有 new Date() 一行代码,因此也可以采用方法引用进行简写。但问题是,构造函数没有名称,我们只能用new 关键字来代替:

// 构造方法List<Date> dateList = CollectionUtil.convert(list, Date::new);dateList.forEach(System.out::println);

注意两点:

  • 上面代码中的System.out::println 其实是 指定对象System.out的非静态方法println的引用

  • 如果构造函数有多个,可能无法区分导致传递失败

4、接口的默认方法和静态方法

Java8使用两个新概念扩展了接口的含义:默认方法和静态方法

  • 默认方法

默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写

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 ) {    // 调用接口的静态方法,并且传递DefaultableImpl的构造函数引用来构建对象    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );    System.out.println( defaulable.notRequired() );  // 调用接口的静态方法,并且传递OverridableImpl的构造函数引用来构建对象    defaulable = DefaulableFactory.create( OverridableImpl::new );    System.out.println( defaulable.notRequired() );}

由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection 接口添加新方法,如stream()parallelStream()forEach()removeIf() 等等。

尽管默认方法有这么多好处,但在实际开发中应该谨慎使用:在复杂的继承体系中,默认方法可能引起歧义和编译错误。如果你想了解更多细节,可以参考官方文档。

5、Optional

Optional仅仅是一个容器,可以存放T类型的值或者null, 它提供了一些有用的接口来避免显式的null检查

接下来看一点使用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;如果Optional实例持有null,orElseGet()方法可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Optional 实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值,而不是通过Lambda来生成。

上述代码的输出结果如下:

Full Name is set? falseFull Name: [none]Hey Stranger!

6、Streams

新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。

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

首先看一个问题:在这个task集合中一共有多少个OPEN状态的?计算出它们的points属性和。在Java 8之前,要解决这个问题,则需要使用foreach循环遍历task集合;但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。

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

这里有很多知识点值得说。首先,tasks 集合被转换成steam 表示;其次,在steam 上的filter 操作会过滤掉所有CLOSEDtask ;第三,mapToInt 操作基于tasks 集合中的每个task 实例的Task::getPoints 方法将task 流转换成Integer 集合;最后,通过sum 方法计算总和,得出最后的结果。

在学习下一个例子之前,还需要记住一些steams(点此更多细节)的知识点。Steam之上的操作可分为中间操作和晚期操作。

中间操作会返回一个新的steam——执行一个中间操作(例如filter)并不会执行实际的过滤操作,而是创建一个新的steam,并将原steam中符合条件的元素放入新创建的steam。

晚期操作(例如forEach或者sum),会遍历steam并得出结果或者附带结果;在执行晚期操作之后,steam处理线已经处理完毕,就不能使用了。在几乎所有情况下,晚期操作都是立刻对steam进行遍历。

steam的另一个价值是创造性地支持并行处理(parallel processing)。对于上述的tasks集合,我们可以用下面的代码计算所有task的points之和:

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

对于一个集合,经常需要根据某些条件对其中的元素分组。利用steam提供的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]]}

最后一个关于tasks集合的例子问题是:如何计算集合中每个任务的点数在集合中所占的比重,具体处理的代码如下:

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

最后,正如之前所说,Steam API不仅可以作用于Java集合,传统的IO操作(从文件或者网络一行一行的读取数据)可以受益于steam处理,这里有一个小例子:

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对软件开发的现代范式的响应。

7、并行数组

Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是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()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。

Java中的流程控制语句 (基础篇四)