--- title: Java 8的新特性 tags: - JVM - Java - Java 8 - 新特性 - Optional - Stream - Lambda categories: - - JVM - Java keywords: 'Java,Java 8,新特性,Optional,Stream,Lambda,引用,函数,方法' date: 2021-05-20 15:06:57 --- 虽然Java 8已经推出了好久了,但是因为Java的各个版本之间都是渐进改善的,所以还是有必要回顾一下,为后面其他版本的特性总结做一个准备。 这一系列的文章中对于各个版本Java的新特性总结的可能并不全,但是将尽力保证一些变化较大的新特性都能够被收录进来。 本系列的文章有: - {% post_link nf-java8 %} - {% post_link nf-java9 %} - {% post_link nf-java10 %} - {% post_link nf-java11 %} - {% post_link nf-java12 %} - {% post_link nf-java13 %} - {% post_link nf-java14 %} - {% post_link nf-java15 %} - {% post_link nf-java16 %} - {% post_link nf-java17 %} ## Lambda表达式 Lambda表达式在目前多数语言中都获得了支持,有的语言称其为Lambda表达式,有的称其为闭包。Java 8作出的最重大的改进就是增加了Lambda表达式的支持。 Lambda表达式允许将一个函数作为函数的参数传入函数来执行,可以使之前需要使用匿名对象的语法结构变得更加简洁和紧凑。以 `java.util.Collections.sort()` 方法为例,以下分别是在Java 7和Java 8两个版本中的语法对比。 ```java // Java 7中需要使用匿名对象来进行排序方式的定义 Collections.sort(students, new Comparator() { @Override int compare(Person p1, Person p2) { return p1.getScore() - p2.getScore(); } }); // Java 8中的排序可以直接使用Lambda表达式来定义 Collections.sort(students, (p1, p2) -> p1.getScore() - p2.getScore()); ``` 从以上示例可以看出,Lambda表达式的引入,不仅增强了Java整体语法的函数式编程特征,也大大降低了编码的复杂度,代码的表达性更加清晰。 Java中的Lambda表达式可以采用以下语法格式编写。 ```java // 直接返回表达式的值 (参数表) -> 表达式 // 执行一个函数体,其中可以返回值,也可不返回值 (参数表) -> { 语句; } ``` Lambda表达式的参数表中可以显式书写各个参数的类型声明,也可以省略,编译器可以自动识别参数值;并且定义参数列表的圆括号在只有一个参数时,也是可以省略的,但是多个参数或者没有参数,圆括号是必需的。当Lambda表达式的主体使用了大括号,则Lambda表达式将按照函数的特性来执行主体内的语句,如果需要返回值,则必须使用`return`显式声明;如果只有一个语句,则不需要大括号,而Lambda表达式也会自动返回这条语句的值作为Lambda表达式的返回值。所以根据Java中Lambda表达式的规则,以下Lambda表达式都是合法的。 ```java // 不接受任何参数并返回固定值 () -> 5 // 接受一个参数并返回处理后的值 x -> x * 2 // 接受两个参数,返回处理后的值 (x, y) -> x + y (int x, int y) -> x + y // 执行一个语句,不返回任何值,类似于返回值为void的函数 String s -> System.out.print(s) // 执行批量的语句,用return返回值 (String a, String b) -> { t = new StringBuilder(); t.append(a); t.append(b); return t.toString(); } // 替代匿名对象,定义只拥有一个方法的接口实例(函数式接口) // 如有以下接口 interface SimpleOne { int call(String message); } // 以下两种调用方式是等价的,但第二种调用方式是使用Lambda表达式实现的 something.needSimpleOne(new SimpleOne() { @Override int call(String message) { return doSomething(): } }); something.needSimpleOne(message -> { return doSomething(); }) ``` Lambda表达式只能引用使用`final`标记的外层局部变量,即定义在定义Lambda表达式作用域以外的变量。但Lambda表达式可以直接使用与Lambda表达式同作用域的变量而不需要使用`final`标记。Lambda表达式的参数名不能与同作用域的局部变量重名,而且Lambda表达式也不能修改所有外部局部变量的值。 ## 方法引用 在引入Lambda表达式以后,就经常可能会出现下面这种形式的代码。 ```java bool hasStaff = userList.stream().anyMatch(user -> user.isStaff()); ``` 这种形式的代码十分的啰嗦,而且并不美观。为了解决这个问题,Java 8引入了操作符`::`来提供对于方法的引用。所以借助方法引用操作符,就可以把上面这个示例修改成下面的样子。 ```java bool hasStaff = userList.stream().anyMatch(user::isStaff); ``` 虽然在这个示例中还不太能看出来这个方法引用操作符的优势,但是它可以将原本必能复杂繁琐的代码进行简化,使代码的逻辑更加容易看清楚。 `::`方法引用操作符除了可以应用在对象实例上,还可以应用在以下这些对象上。 - 调用类中的静态方法:`ClassName::methodName`。 - 调用类实例中的方法:`instanceName::methodName`。 - 针对特定类的任意实例调用方法:`Class::methodName`。 - 调用类自身(`this`)的方法:`this::methodName`。 - 调用类的构造函数:`ClassName::new`。 ## 默认方法 接口在声明时一般只需要声明其中的方法签名即可,这严格遵循了面向抽象编程的概念。但是严格遵循面向抽象编程所带来的不便就是当接口发生修改时,所有实现了这个接口的类都需要进行修改。对于大型项目来说,这种修改的工作量往往巨大,而且还会造成新版本与旧版本之间的不兼容。为了解决这个问题,Java 8引入了默认方法。 默认方法的声明十分简单,只需要在方法名前增加`default`关键字即可,默认方法必须要提供方法实现。 ```java public interface Vehicle { default void run() { System.out.println("On run"); } } ``` 由于一个类一个实现多个接口,如果被实现的接口中有相同的默认方法,直接调用这些同名方法将产生错误,这时就可以使用`super`来调用。 ```java public interface IFunctionOne { default void print() { doSomething(); } } public interface IFunctionTwo { default void print() { doSomethingAlter(); } } class Something implements IFunctionOne, IFunctionTwo { public void doTheJob() { IFunctionOne.super.print(); IFunctionTwo.super.print(); } } ``` 除了默认方法之外,接口现在还可以定义静态默认方法,只需要用`static`关键字替代`default`关键字即可。静态默认方法可以直接使用接口调用。 ## 函数式接口 函数式接口就是有且仅有一个抽象方法,但可以有多个非抽象方法的接口。函数式接口可以被隐式转换为Lambda表达式。Java 8引入了一个新的注解`@FunctionalInterface`,用来标注函数式接口,这个注解仅在编译时起效,如果被标注的接口不符合函数式接口的规范,编译器将报错。关于函数式接口的定义,可参考前面Lambda表达式中的相关示例。 为了支持Java 8的函数式编程,Java引入了`java.util.function`包,其中定义了以下接口来支持高阶函数的使用。以下中所示的接口可以用在高阶函数的参数列表中来定义高阶函数所接收的函数参数。 * `BiConsumer`,接受两个参数但不返回任何结果的函数。 * `BiFunction`,接受两个参数并返回`R`类型值的函数。 * `BinaryOperator`,作用于两个`T`类型值的操作符,并返回同类型结果。 * `BiPredicate`,接受两个参数并返回布尔类型值的函数。 * `Consumer`, 接受一个参数但不返回任何结果的函数。 * `Function`,接受一个参数并返回`R`类型值的函数。 * `Predicate`,接受一个参数并返回布尔类型值的函数。 * `Supplier`,不接受参数,只返回一个`T`类型结果的函数。 * `UnaryOperator`,作用于一个`T`类型值的操作符,并返回同类型结果。 `java.util.function`包中还以以上几种接口为基础,提供了能够覆盖大部分常用Lambda表达式形态的函数式接口。在`java.util.function`包中的函数式接口中,以`Function`和`Operator`结尾的接口都提供了`apply()`作为其中的唯一抽象方法,而以`Predicate`结尾的接口则提供了`test()`作为其唯一抽象方法,所以在书写高阶函数时要注意接口中抽象方法的不同。 ## Optional `Optional`类是一个容器,用于存放可能为`null`的`T`类型对象。通过使用`Optional`类,可以避免显式的空值检测,避免`NullPointerException`的发生。`Optional`类提供了以下常用的方法来操作可空对象。 * `empty()`,返回`Optional`,静态方法,返回一个空白的`Optional`实例。 * `equals(Object obj)`,返回`boolean`,判断与其他对象是否相等。 * `filter(Predicate)`,返回`Optional`,对被包装的值进行过滤。 * `flatMap(Function>)`,返回`Optional`,对被包装的值进行展平映射。 * `get()`,返回`T`,获取被包装的值。 * `hashCode()`,返回`int`,返回被包装值的哈希码。 * `ifPresent(Consumer)`,如果被包装值存在,则执行相应函数。 * `isPresent()`,返回`boolean`,判断被包装值是否存在。 * `map(Function)`,返回`Optional`,对被包装的值进行映射。 * `of(T)`,返回`Optional`,静态方法,返回一个指定非空值的Optional。 * `ofNullable(T)`,返回`Optional`,静态方法,返回一个可能为空的Optional。 * `orElse(T)`,返回`T`,获取被包装值,否则返回指定值。 * `orElseGet(Supplier)`,返回`T`,获取被包装值,否则返回指定函数调用结果。 * `orElseThrow(Supplier)`,返回`T`,获取被包装值,否则抛出指定异常。 * `toString()`,返回`String`,输出一个代表字符串。 ## Stream Java 8引入的流(Stream)提供了一种通过定义管道来依次使元素逐步获得处理的操作。流实际上是一个来自数据源元素的队列,在其上支持管道操作和聚合操作。流的数据来源可以是集合、数组、输入输出通道、生成器等。管道操作则会返回流对象本身,以供调用下一个管道操作,这样就可以形成一个链式操作,这也是流的操作风格特点。流中不会存储任何元素,只会按照管道操作进行按需计算,所以在所有管道操作的最后,需要使用聚合操作来归集流的操作结果。 > 流不需要显式对集合进行迭代,集合将在流内部通过访问者模式完成迭代。 流可以使用以下几个方法来生成: * `Collection.stream()`,从集合创建一个串行流,所有元素依次进行处理。 * `Collection.parallelStream()`,从集合创建一个并行流,所有元素同时进行处理,不保证归集后的集合顺序与原始集合相同。 * `Stream.empty()`,创建一个空白流。 * `Stream.concat()`,连接两个流以形成新的流。 * `Stream.generate(Supplier)`,通过生成函数生成一个包含无穷元素的流。 * `Stream.iterate(T, UnaryOperator)`,通过迭代操作种子生成一个包含无穷元素的流。 * `Stream.of()`,通过单个值或者一系列值形成一个流。 流接口(Stream)提供了以下这些常用的管道操作方法,它们都会返回与调用者类型相同的流对象。 * `distinct()`,元素去重。 * `filter(Predicate)`,过滤并只保留匹配Predicate的元素。 * `flatMap(Function)`,展平并映射。 * `map(Function)`,映射为新元素。 * `limit(long)`,只保留指定数目的元素。 * `peek(Consumer)`,直接返回原始流,但利用流中的元素进行一些副作用操作。 * `skip(long)`,丢弃指定数目的元素。 * `sorted(Comparator)`,按照指定顺序进行排序。 如果没有归集操作,管道操作是不会被应用在流上的,而也只能通过归集操作,我们才能够获得原始值通过流处理后的最终结果。相比管道操作方法,归集操作方法要多不少。 * `allMatch(Predicate)`,判断所有元素是否都匹配Predicate。 * `anyMatch(Predicate)`,判断至少有一个元素匹配Predicate。 * `collect(Collector)`,利用`java.util.stream.Collectors`中的方法来归集成一个集合。 * `collect(Supplier, BiConsumer, BiConsumer)`,归集一个流为集合。 * `count()`,获得流中元素的数量。 * `findAny()`,从流中获取一个元素并使用Optional包装,当流为空时,Optional为空。 * `findFirst()`,从流中获取第一个元素的值,并使用Optional包装,当流为空时,Optional为空。 * `forEach(Consumer)`,对流中的元素依次调用指定函数。 * `max(Comparator)`,获取最大值,返回一个Optional。 * `min(Comparator)`,获取最小值,返回一个Optional。 * `noneMatch(Predicate)`,判断是否所有元素都不匹配Predicate。 * `reduce(BinaryOperator)`,将一个集合归集为单一值,返回一个Optional。 * `reduce(T, BinaryOperator)`,以指定值为基础归集集合。 ## Base64编码 Java 8将Base64编码标准内置在了JDK中,Base64工具类主要提供了针对纯文本、URL和MIME三种方式编码的功能,全部以静态方法的形式出现,主要有以下这些。 * `getDecoder()`、`getEncoder()`,获取纯文本型Base64解码器和编码器。 * `getMimeDecoder()`、`getMimeEncoder()`,获取MIME型Base64解码器和编码器。 * `getMimeEncoder(int, byte[])`,获取自定义行长度和行分隔符的MIME型Base64编码器。 * `getUrlDecoder()`、`getUrlEncoder()`,获取URL型Base64解码器和编码器。 工具类中这些静态方法主要返回`Decoder`和`Encoder`两个内嵌类实例,其中主要通过`decode()`方法和`encode()`方法完成Base64的解码和编码。