10万字总结Java8到21新特性详解

文章目录

Java 8 新特性—概述

Java 8 是Java历史上一个重大的版本更新,发布于2014年3月18日。

img

JEP 126:Lambda 表达式

Lambda 表达式是 Java 8 新特性中最重要且最显著的一个,为 Java 增加了函数式编程的能力,使得代码变得更加简洁和易读。Lambda 表达式主要用于简化匿名内部类的实现。

Lambda 表达式的基本语法:

(parameters) -> expression 或 (parameters) -> {
    statements; }
  • parameters :是 Lambda表达式的参数列表,可以为空或包含一个或多个参数。
  • -> :是 Lambda 操作符,用于将参数和 Lambda 主体分开。
  • expression :是 Lambda 表达式的返回值,或者在主体中执行的单一表达式。
  • { statements; } :是 Lambda 主体,包含了一系列语句,如果需要执行多个操作,就需要使用这种形式。

它具有如下几个特点:

  1. 无需声明类型:Lambda 表达式不需要声明参数类型,编译器可以自动推断参数类型。
  2. 可选的参数圆括号:当只有一个参数时,可以省略圆括号。但是当参数个数大于一个时,圆括号是必需的。空括号用于表示空参数集。
  3. 可选的大括号:当 Lambda 表达式的主体只包含一个表达式时,可以省略大括号。当表达式需要包含多个语句时,需要使用大括号。
  4. 可选的返回关键字:当 Lambda 表达式主体只有一个表达式,且该表达式会自动返回结果时,可以省略 return 关键字。

JEP 126:函数式接口

Java 8 引入函数式接口的主要目的是支持函数式编程范式,也就是 Lambda 表达式。在函数式编程语言中,函数被当做一等公民对待,Lambda 表达式的类型是函数,它可以像其他数据类型一样进行传递、赋值和操作。但是在 Java 中,“一切皆对象”是不可违背的宗旨,所以 Lambda 表达式是对象,而不是函数,他们必须要依附于一类特别的对象类型:函数式接口。所以函数式接口是与Lambda表达式紧密相连的,它为Java添加了一种新的抽象层次,允许将方法作为一等公民对待

函数式接口具有两个特点:

  1. 只包含一个抽象方法:函数式接口只能有一个抽象方法,但可以包含多个默认方法或静态方法。
  2. @FunctionalInterface注解标记:该注解不强制,但通常会使用它来标记该接口为函数式接口。这样做可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误。

一般来说函数式接口有两个最主要的用途:

  1. 与 Lambda表达式一起使用,为Java带来更加函数式的编程风格。
  2. 用于实现简单的函数策略或行为,如回调、事件处理等。

JEP 179:方法引用

为了提升 Java 编程语言的表达力和可读性,特别是在配合 Lambda 表达式和函数式编程风格,Java 8 引入方法引用。

方法引用实际上是一个简化版的 Lambda 表达式,它允许我们以更简洁的方式引用方法。它有如下几种类型:

  1. 静态方法引用:使用类名::静态方法名的形式。

    • 例如,String::valueOf 相当于 x -> String.valueOf(x)
  2. 实例方法引用(对象的实例方法):使用 实例对象::实例方法名的形式。

    • 例如,假设有一个 String 对象 myString,那么 myString::length 相当于 () -> myString.length()
  3. 特定类型的任意对象的实例方法引用:使用 类名::实例方法名

    • 例如,String::length 相当于 str -> str.length()。这里不是调用特定对象的 length 方法,而是用于任意的 String 对象。
  4. 构造器引用:使用类名::new

  • 例如,ArrayList::new 相当于 () -> new ArrayList<>()

JEP 150:接口的默认方法

在 Java 8 之前,接口中可以申明方法和变量的,只不过变量必须是 public、static、final 的,方法必须是 public、abstract的。我们知道接口的设计是一项巨大的工作,因为如果我们需要在接口中新增一个方法,需要对它的所有实现类都进行修改,如果它的实现类比较少还可以接受,如果实现类比较多则工作量就比较大了。

为了解决这个问题,Java 8 引入了默认方法,默认方法允许在接口中添加具有默认实现的方法,它使得接口可以包含方法的实现,而不仅仅是抽象方法的定义。

默认方法是接口中带有 default 关键字的非抽象方法。这种方法可以有自己的实现,而不需要子类去覆盖它。

默认方法允许我们向接口添加新方法而不破坏现有的实现。它解决了在 Java 8 之前,向接口添加新方法意味着所有实现该接口的类都必须修改的问题。

JEP 107:Stream API

为了解决 Java 8 之前版本中集合操作的一些限制和不足,提高数据处理的效率和代码的简洁性,Java 8 引入 Stream API,它的引入标志着 Java 对集合操作迎来了的一种全新的处理方式,它在处理集合类时提供了一种更高效、声明式的方法。

Stream API 的核心思想是将数据处理操作以函数式的方式链式连接,以便于执行各种操作,如过滤、映射、排序、归约等,而无需显式编写传统的循环代码。

下面是 Stream API 的一些重要概念和操作:

  1. Stream(流):Stream 是 Java 8 中处理集合的关键抽象概念,它是数据渠道,用于操作数据源所生成的元素序列。这些数据源可以来自集合(Collection)、数组、I/O操作等等。它具有如下几个特点:

    1. Stream 不会存储数据。
    2. Stream 不会改变源数据对象,它返回一个持有结果的新的 Stream
    3. Stream 操作是延迟执行的,这就意味着他们要等到需要结果的时候才会去执行。
  2. 中间操作:这些操作允许您在 Stream 上执行一系列的数据处理。常见的中间操作有 filter(过滤)、map(映射)、distinct(去重)、sorted(排序)、limit(截断)、skip(跳过)等。这些操作返回的仍然是一个 Stream。

  3. 终端操作:终端操作是对流进行最终处理的操作。当调用终端操作时,流将被消费,不能再进行进一步的中间操作。常见的终端操作包括 forEach(遍历元素)、collect(将元素收集到集合中)、reduce(归约操作,如求和、求最大值)、count(计数)等。

  4. 惰性求值:Stream 操作是惰性的,只有在调用终端操作时才会执行中间操作。这可以提高性能,因为只处理需要的数据。

Optional 类

Java 8 引入了 Optional 类,这是一个为了解决空指针异常(NullPointerException)而设计的容器类。它可以帮助开发者在编程时更优雅地处理可能为 null 的情况。

JEP 170:新的日期时间 API

作为 Java 开发者你一定直接或者间接使用过 java.util.Datejava.util.Calendarjava.text.SimpleDateFormat 这三个类吧,这三个类是 Java 用于处理日期、日历、日期时间格式化的。由于他们存在一些问题,诸如:

  1. 线程不安全:

    1. java.util.Datejava.util.Calendar 线程不安全,这就导致我们在多线程环境使用需要额外注意。
    2. java.text.SimpleDateFormat 也是线程不安全的,这可能导致性能问题和日期格式化错误。而且它的模式字符串容易出错,且不够直观。
  2. 可变性java.util.Date类是可变的,这意味着我们可以随时修改它,如果一不小心就会导致数据不一致问题。

  3. 时区处理困难:Java 8 版本以前的日期 API 在时区处理上存在问题,例如时区转换和夏令时处理不够灵活和准确。而且时区信息在 Date 对象中存储不明确,这使得正确处理时区变得复杂。

  4. 设计不佳:

    1. 日期和日期格式化分布在多个包中。
    2. java.util.Date 的默认日期,年竟然是从 1900 开始,月从 1 开始,日从 1 开始,没有统一性。而且 java.util.Date 类也缺少直接操作日期的相关方法。
    3. 日期和时间处理通常需要大量的样板代码,使得代码变得冗长和难以维护。

基于上述原因,Java 8 重新设计了日期时间 API,以提供更好的性能、可读性和可用性,同时解决了这些问题,使得在 Java 中处理日期和时间变得更加方便和可靠。相比 Java 8 之前的版本,Java 8 版本的日期时间 API 具有如下几个优点:

  1. 不可变性(Immutability):Java 8的日期时间类(如LocalDateLocalTimeLocalDateTime)都是不可变的,一旦创建就不能被修改。这确保了线程安全,避免了并发问题。
  2. 清晰的API设计:Java 8 的日期时间 API 采用了更清晰、更一致的设计,相比于以前版本的 DateCalendar 更易于理解和使用。而且它们还提供了丰富的方法来执行日期和时间的各种操作,如加减、比较、格式化等。
  3. 本地化支持:Java 8 的日期时间 API 支持本地化,可以轻松处理不同地区和语言的日期和时间格式。它们能够自动适应不同的时区和夏令时规则。
  4. 新的时区处理:Java 8引入了 ZoneIdZoneOffset 等新的时区类,使时区处理更加精确和灵活。这有助于解决以前版本中时区处理的问题。
  5. 新的格式化API:Java 8引入了 DateTimeFormatter 类,用于格式化和解析日期和时间,支持自定义格式和本地化。这提供了更强大和灵活的格式化选项。
  6. 更好的性能:Java 8 的日期时间API 比以前的API 性能更佳。

JEP 120:重复注解

在 Java 8 之前的版本中,对于一个特定的类型,一个注解在同一个声明上只能使用一次。Java 8 引入了重复注解,它允许对同一个类型的注解在同一声明或类型上多次使用。

工作原理如下:

  1. 定义重复注解:您需要定义一个注解,并用 @Repeatable 元注解标注它。@Repeatable 接收一个参数,该参数是一个容器注解,用于存储重复注解的实例。
  2. 定义容器注解:容器注解定义了一个注解数组,用于存放重复注解的多个实例。这个容器注解也需要具有运行时的保留策略(@Retention(RetentionPolicy.RUNTIME))。

Base64 编码解码

在 Java 8 之前,我们通常需要依赖于第三方库(如 Apache Commons Codec)或者使用 Java 内部类(如 sun.misc.BASE64Encodersun.misc.BASE64Decoder)来处理 Base64 编解码。但是这些内部类并非 Java 官方的一部分,它们的使用并不推荐,因为它们可能会在未来的版本中发生变化,造成兼容性问题。同时使用非官方或内部 API 可能导致安全漏洞或运行时错误,所以 Java 8 引入一个新的 Base64 编解码 API,它处理 Base64 编码和解码的官方、标准化的方法。

Java 8 中的 Base64 API 包含在 java.util 包中。它提供了以下三种类型的 Base64 编解码器:

  1. 基本型(Basic):用于处理常规的 Base64 编码和解码。它不对输出进行换行处理,适合于在URLs和文件名中使用。
  2. URL和文件名安全型(URL and Filename Safe):输出映射到一组 URL 和文件名安全的字符集。它使用 ‘-’ 和 ‘_’ 替换标准 Base64 中的 ‘+’ 和 ‘/’ 字符。
  3. MIME型:用于处理 MIME 类型的数据(例如,邮件)。它在每行生成 76 个字符后插入一个换行符。

JEP 104:类型注解

在 Java 8 之前,注解仅限于声明(如类、方法或字段)。这种限制意味着注解的用途在许多编程情景中受到限制,特别是在需要对类型本身(而不仅仅是声明)进行描述时。为了提高注解的能力,Java 8 引入类型注解来增强注解的功能。

该特性扩展了注解的应用范围,允许我们将注解应用于任何使用类型的地方,而不仅仅是声明。包括以下情况:

  • 对象创建(如 new 表达式)
  • 类型转换和强制类型转换
  • 实现(implements)语句
  • 泛型类型参数(如 List<@NonNull String>

JEP 101:类型推断优化

在 Java 8 之前,Java 的类型推断主要局限于泛型方法调用的返回类型。这意味着在许多情况下,我们不得不显式指定泛型参数,即使它们可以从上下文中推断出来。这种限制使得代码变得冗长且不够直观,特别是在使用泛型集合和泛型方法时。

为了提高编码效率和可读性,同时简化泛型使用,Java 8 中引入了对类型推断机制的优化,扩大了类型推断的范围,使其能在更多情况下自动推断出类型信息,包括:

  1. Lambda 表达式中的类型推断:在使用 Lambda 表达式时,编译器可以根据上下文推断出参数类型,从而减少了在某些情况下编写显式类型的需求。
  2. 泛型方法调用的改进:在调用泛型方法时,编译器可以更好地推断方法参数、返回类型以及链式调用中间步骤的类型。
  3. 泛型构造器的类型推断:在创建泛型对象时,编译器能够推断出构造器参数的类型。

JEP 174:Nashorn JavaScript 引擎

在 Java 8 之前,Java 平台的主要 JavaScript 引擎是 Mozilla 的 Rhino。Rhino 是一个成熟的引擎,但由于其架构和设计年代较早,它在性能和与 Java 的集成方面存在一些限制。随着 JavaScript 在 Web 和服务器端应用中日益重要,需要一个更现代、更高效的 JavaScript 引擎来提供更好的性能和更深度的 Java 集成。因此,Nashorn 引擎被引入作为 Java 平台的一部分。

Nashorn 是一个基于 Java 的 JavaScript 引擎,它完全用 Java 语言编写,并且是 Rhino 的替代品。主要特点:

  1. 基于 JVM 的执行:Nashorn 是作为 Java 虚拟机的一个原生组件实现的,它直接编译 JavaScript 代码到 Java 字节码。这意味着它可以充分利用 JVM 的性能优化和管理能力。
  2. 高性能:与 Rhino 相比,Nashorn 提供了显著的性能提升,特别是在执行 JavaScript 代码方面。
  3. 与 Java 的深度集成:Nashorn 允许 JavaScript 代码和 Java 代码之间有更紧密的交互。开发者可以在 JavaScript 中方便地调用 Java 类库和对象,反之亦然。
  4. ECMAScript 5.1 支持:Nashorn 支持 ECMAScript 5.1 规范,为开发者提供了一个符合标准的现代 JavaScript 编程环境。

JEP 122:移除Permgen

在 Java 8 之前,JJVM使用永久代(PermGen)的内存区域来存储类的元数据和方法数据。随着时间的推移,这个设计开始显现出一些问题,特别是在应用程序频繁加载和卸载类的场景中,比如在 Java EE 应用服务器和热部署环境中。

永久代有一个固定的大小限制,当类的数量和大小超过这个限制时,就会抛出 OutOfMemoryError: PermGen space 错误。这种设计限制了 Java 的灵活性和可伸缩性。

Java 8 移除永久代并用元空间(Metaspace)的新内存区域来取代它。相比永久代,元空间的具有如下优势:

  1. 基于本地内存:元空间不在 JVM 的堆内存中,而是直接使用本地内存(操作系统的内存)。这意味着它不再受到 Java 堆大小的限制。
  2. 动态调整大小:元空间的大小可以根据应用程序的需求动态调整。这减少了内存溢出的风险,并允许应用更高效地管理内存。
  3. 更好的性能:由于移除了固定大小的限制,元空间可以提供更好的性能,尤其是在大型应用和复杂的部署环境中。

Java 8 新特性—Lambda 表达式

img

什么是 Lambda 表达式

Lambda 表达式是在 Java 8 中引入,并且被吹捧为 Java 8 最大的特性。它是函数式编程的的一个重要特性,标志着 Java 向函数式编程迈出了重要的第一步。

它的语法如下:

(parameters) -> expression

或者

(parameters) -> {
    statements; }

其中

  • parameters :是 Lambda表达式的参数列表,可以为空或包含一个或多个参数。
  • -> :是 Lambda 操作符,用于将参数和 Lambda 主体分开。
  • expression :是 Lambda 表达式的返回值,或者在主体中执行的单一表达式。
  • { statements; } :是 Lambda 主体,包含了一系列语句,如果需要执行多个操作,就需要使用这种形式。

Java 8 引入 Lambda 表达式的主要作用是简化部分匿名内部类的写法。使用它可以完成用少量的代码实现复杂的功能,极大的简化代码代码量和代码结构。同时,JDK 中也增加了大量的内置函数式接口供我们使用,使得在使用 Lambda 表达式时更加简单、高效。

下面我们就来看它的一些常见用法。

常见用法

无参数,无返回值

例如 Runnable 接口的 run()

在 Java 8 版本之前的版本,我们一般都是这样用:

        new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                System.out.println("死磕 Java 就是牛逼...");
            }
        }).start();

从 Java 8 开始,无参数匿名内部类可以简写成如下这种方式:

() -> {
   
    执行语句
}

所以上面代码可以简写成这样的:

new Thread(() -> System.out.println("死磕 Java 就是牛逼...")).start();

单参数,无返回值

只有一个参数,无返回值,如下:

(x) -> System.out.println(x);

在 Java 8 中,有一个函数式接口 Consumer,它定义如下:

@FunctionalInterface
public interface Consumer<T> {
   

    void accept(T t);
}

我们用它来演示下:

        Consumer<String> consumer = (String s) -> {
   
            System.out.println(s);
        };
        
        consumer.accept("死磕 Java 就是牛...");

是不是比较简便,但是这段代码还不够简便,它还可以进行多次优化,

  • 如果 Lambda 主体只有一条语句,则 {、} 可以省略
Consumer<String> consumer = (String s) -> System.out.println(s);
  • Lambda 表达式有一个依据:类型推断机制。在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。所以 (String s) 可以简写为 (s)
Consumer<String> consumer = (s) -> System.out.println(s);
  • 对于只有一个参数的情况,左侧括号可以省略:
Consumer<String> consumer = s -> System.out.println(s);

多参数,有返回值

如 Comparator 接口的 compare(T o1, T o2) 方法,在 Java 8 之前,写法如下:

        Comparator<Integer> comparator = new Comparator<Integer>() {
   
            @Override
            public int compare(Integer o1, Integer o2) {
   
                System.out.println("o1:" + o1);
                System.out.println("o2:" + o2);
                return o1.compareTo(o2);
            }
        };
        
        comparator.compare(12,13);

使用 Lambda 表达式后:

        Comparator<Integer> comparator = (o1, o2) -> {
   
            System.out.println("o1:" + o1);
            System.out.println("o2:" + o2);
            return o1.compareTo(o2);
        };

        comparator.compare(12,13);

当然,如果去掉 System.out.println(),还可以简写为 Comparator<Integer> comparator = (o1, o2) -> o1.compareTo(o2); ,这里是可以省略 return 关键字的。

这里就 Lambda 的简写做一个总结:

  1. 类型推断:编译器可以根据上下文推断 Lambda 表达式的参数类型,从而可以省略参数类型的声明。
  2. 单一参数:当 Lambda 表达式只有一个参数时,可以省略参数外的括号。如: (x) → x * 2 可以简写为 x → x * 2
  3. 单表达式:当 Lambda 表达式只有一行代码时,可以省略大括号和 return 关键字。如 (x,y) → {return x + y} 可以简写为 (x,y) → x + y

Lambda 简写依据

Lambda 简写的依据有两个:

1、必须有相应的函数式接口

所谓函数式接口函数式就是指只包含一个抽象方法的接口,它是在 Java 8 版本中引入的,其主要目的是支持函数式编程,有了函数式接口我们可以将函数作为参数传递、将函数作为返回值返回,同时也为使用 Lambda 表达式提供了支持。

函数式接口具有以下特征:

  1. 只包含一个抽象方法:函数式接口只能有一个抽象方法,但可以包含多个默认方法或静态方法(Java 8 中有另一个新特性:default 关键字)。这个唯一的抽象方法通常用来表示某种功能或操作。
  2. **@FunctionalInterface**注解标记:注解不强制,但通常会使用它来标记该接口为函数式接口。这样做可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误。

2、类型推断机制

类型推断机制则是允许编译器根据上下文自动推断 Lambda 表达式的参数类型。这个推断过程包括两个方面:

  1. 目标类型推断

编译器会根据 Lambda 表达式在赋值、传参等地方的上下文来推断Lambda表达式的目标类型。例如,如果Lambda表达式被赋值给一个接口类型的变量,编译器会根据该接口的抽象方法来推断Lambda表达式的参数类型。

Runnable runnable = () -> System.out.println("死磕 Java 就是牛...");

Lambda表达式被赋值给了 Runnable 类型的变量,所以编译器知道 Lambda 表达式需要没有参数且返回类型为void的方法。

  1. 参数类型推断

如果 Lambda 表达式的参数类型可以从上下文中唯一确定,编译器会自动推断参数的类型。例如:

List<String> skList = Arrays.asList("死磕 Java 并发", "死磕 Netty", "死磕 NIO","死磕 Spring");
skList.forEach(sk -> System.out.println(sk));

forEach方法期望一个参数类型为Consumer<String>的函数,编译器可以从 sk的类型推断出Lambda表达式的参数类型为String

虽然类型推断机制允许省略Lambda表达式的参数类型,但有时候显式声明参数类型可以增强代码的可读性和处理复杂的泛型情况,这个时候我们还是将参数类型写上会显得更加友好。


Java 8 新特性—函数式接口

在上边 Lambda 表达式 提过,Lambda 能够简化的一个依据就是函数式接口,这篇文章我们就来深入了解函数式接口。

img

什么是函数式接口

函数式接口是一个只有一个抽象方法的接口,最开始的时候也叫做 SAM 类型的接口(Single Abstract Method)。它具有两个特点:

  1. 只包含一个抽象方法:函数式接口只能有一个抽象方法,但可以包含多个默认方法或静态方法。
  2. @FunctionalInterface注解标记:该注解不强制,但通常会使用它来标记该接口为函数式接口。这样做可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误。

Java 引入函数式接口的主要目的是支持函数式编程范式,也就是 Lambda 表达式。在函数式编程语言中,函数被当做一等公民对待,Lambda 表达式的类型是函数,它可以像其他数据类型一样进行传递、赋值和操作。但是在 Java 中,“一切皆对象”是不可违背的宗旨,所以 Lambda 表达式是对象,而不是函数,他们必须要依附于一类特别的对象类型:函数式接口。

所以,从本质上来说 Lambda 表达式就是一个函数式接口的实例。这就是 Lambda 表达式和函数式接口的关系。简单理解就是只要一个对象时函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示。

自定义函数式接口

根据函数式接口的定义和特点,我们可以自定义函数式接口:

@FunctionalInterface
public interface FunctionInterface {
   

    /**
     * 抽象方法
     */
    void doSomething();

    /**
     * 默认方法
     * @param s
     */
    default void defaultMethod(String s) {
   
        System.out.println("默认方法:" + s);
    }

    /**
     * 静态方法
     * @param s
     */
    static void staticMethod(String s) {
   
        System.out.println("静态方法:" + s);
    }
}

FunctionInterface 是一个自定义函数式接口,它只包含一个抽象方法 doSomething(),还包含一个默认方法 defaultMethod(String s) 和一个静态方法 staticMethod(String s),这两个方法都是可选的。

@FunctionalInterface 注解是可写可可不写的,但是我们一般都推荐写,写上他可以让编译器检查接口是否符合函数式接口的定义,以避免不必要的错误,比如:

img

上面接口定义了两个抽象方法,它会明确告诉你错误了。

使用如下:

        FunctionInterface functionInterface = () -> {
            System.out.println("死磕 Java 就是牛...");
        };

        // 调用抽象方法
        functionInterface.doSomething();
        // 调用默认方法
        functionInterface.defaultMethod("死磕 Netty 就是牛...");
        // 调用静态方法
        FunctionInterface.staticMethod("死磕 Java 并发就是牛...");

执行如下:

img

常用函数式接口

其实在 Java 8 之前就已经有了大量的函数式接口,我们最熟悉的就是 java.lang.Runnable接口了。Java 8 之前已有的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

而在 Java 8 中,新增的函数式接口都在 java.util.function 包中,里面有很多函数式接口,用来支持 Java 的函数式编程,从而丰富了 Lambda 表达式的使用场景。我们使用最多的也是最核心的函数式接口有四个:

  • java.util.function.Consumer:消费型接口
  • java.util.function.Function:函数型接口
  • java.util.function.Supplier:供给型接口
  • java.util.function.Predicate:断定型接口

下面我们就来看这四个函数式接口的使用方法

Consumer 接口

Consumer 代表这一个接受一个输入参数并且不返回任何结果的操作。它包含一个抽象方法 accept(T t),该方法接受一个参数 t,并对该参数执行某种操作:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

由于 Consumer 接口中包含的抽象方法不返回结果,所以它通常用于对对象进行一些操作,如修改、输出、打印等。它的使用方法也比较简单,分为两步。

  1. 创建一个 Consumer 对象:使用 Lambda 表达式来创建一个对象,定义在 accept(T t) 中要执行的操作。
Consumer<String> consumer = str -> System.out.println(str);
  1. 使用 Consumer 对象
consumer.accept("死磕 Java 就是牛...");

// 输出结果...

死磕 Java 就是牛...

在 Consumer 接口中还有一个默认方法 andThen(),该方法接受一个 Consumer 实例对象 after,它允许我们将两个 Consumer 对象组合在一起,形成一个新的 Consumer 对象,该新对象按照顺序执行这两个 Consumer 对象的操作。先执行调用andThen()接口的accept(),然后再执行andThen()参数after中的accept()

Consumer<String> consumer1 = str -> System.out.println("consumer1:" + str);
Consumer<String> consumer2 = str -> System.out.println("consumer2:" + str);

consumer1.andThen(consumer2).accept("死磕 Java 就是牛..");

// 输出结果...

consumer1:死磕 Java 就是牛..
consumer2:死磕 Java 就是牛..

Function 接口

Function 代表一个接受一个输入参数并且产生一个输出结果的函数。它包含一个抽象方法 R apply(T t),该方法接受一个参数 t(类型为 T),并返回一个结果(类型为 R),我们可以理解为根据一个数据类型 T ,经过一系列的操作后得到类型 R。Function 接口是非常通用的,应该是他们四个当中使用最为广泛的。

用途一:函数转换

Function 可以用于将一个类型的值转换为另一个类型的值。它可以用于各种转换操作,如类型转换、数据映射等。

Function<String,Integer> function = str -> Integer.parseInt(str);
int result = function.apply("456");
// 输出结果...
456

用途二:数据处理

Function 可用于对输入数据进行处理并生成输出结果。它可以用于执行各种操作,如过滤、计算、提取、格式化等。

Function<List<String>, String> function = list -> {
    StringBuilder result = new StringBuilder();
        for (String str : list) {
            if (str.startsWith("李")) {
                result.append(str).append(",");
            }
        }

        return result.toString();
    };
List<String> list = Arrays.asList("张三","李四","李武","李柳");
System.out.println(function.apply(list));
// 输出结果...
李四,李武,李柳,   

andThen():方法链式调用

andThen() 接受一个 Function 作为参数,并返回一个新的 Function,该新函数首先应用当前函数,然后将结果传递给参数函数。这种方法链的方式可以用于将多个函数组合在一起,以执行一系列操作。

Function<String,Integer> function1 = t -> Integer.parseInt(t);
Function<Integer,Integer> function2 = t -> t * 10;
System.out.println(function1.andThen(function2).apply("20"));

先将 String 转换为 Integer,然后再 * 10,利用 andThen() 我们可以进行一系列复杂的操作。

compose():顺序执行

compose()andThen()相反,它首先应用参数函数,然后再应用当前函数,这种可能更加好理解些,常用于一些顺序执行。

Function<String,Integer> function1 = t -> {
  System.out.println("function1");
  return Integer.parseInt(t);
};
Function<Integer,Integer> function2 = t -> {
  System.out.println("function2");
  return t * 10;
};
Function<Integer,String> function3 = t -> {
  System.out.println("function3");
  return t.toString();
};
System.out.println(function3.compose(function2.compose(function1)).apply("20"));
        
// 输出结果...
function1
function2
function3
200

从输出结果中可以更加直观地看清楚他们的执行顺序。

identity()恒等函数

identity() 返回一个恒等函数,它仅返回其输入值,对输入值不进行任何操作。源码如下:

    static <T> Function<T, T> identity() {
        return t -> t;
    }

一看感觉 identity() 没啥用处,其实它在某些场景大有用处,例如

  • 作为默认函数

identity() 可以作为函数组合链中的起点或默认函数。当我们想构建一个函数组合链时,可以使用 identity 作为初始函数,然后使用 andThen()compose() 方法添加其他函数。这种方式允许您以一种优雅的方式处理链的起点。

Function<String,String> function1 = Function.identity();
Function<String,String> function2 = str -> str.toUpperCase();
Function<String,String> function3 = str -> str + " WORLD!!!";

System.out.println(function3.compose(function2.compose(function1)).apply("hello"));
复制代码
  • 保持一致性

在某些情况下,我们可能需要一个函数,但不需要对输入进行任何操作。使用 identity() 可以确保函数的签名(输入和输出类型)与其他函数一致。

Supplier 接口

Supplier 是一个代表生产(或供应)某种结果的接口,它不接受任何参数,但能够提供一个结果。它定义了一个 get() 的抽象方法,用于获取结果。

接口定义简单,使用也简单:

Supplier<LocalDate> supplier = () -> LocalDate.now();
LocalDate localDate = supplier.get();

Supplier 接口通常用于惰性求值,只有在需要结果的时候才会执行 get() 。这对于延迟计算和性能优化非常有用。

Predicate 接口

Predicate 表示一个谓词,它接受一个输入参数并返回一个布尔值,用于表示某个条件是否满足。抽象方法为 test(),使用如下:

Predicate<String> predicate = str -> str.length() > 10;
boolean result = predicate.test("www.skjava.com");

判断某个字符长度是否大于 10。

and():表示两个 Predicate 的 与操作

Predicate<Integer> predicate1 = x -> x > 10;
Predicate<Integer> predicate2 = x -> x % 2 == 0;
boolean result = predicate1.and(predicate2).test(13);

or():表示两个 Predicate 的或操作

Predicate<Integer> predicate1 = x -> x > 10;
Predicate<Integer> predicate2 = x -> x % 2 == 0;
boolean result = predicate1.or(predicate2).test(13);

negate():表示 Predicate 的逻辑非操作

Predicate<Integer> predicate1 = x -> x > 10;
boolean result = predicate1.negate().test(14);

其他函数式接口

除了上面四个常用的函数式接口外,java.util.function 包下面还定义了很多函数式接口,下面做一个简单的介绍:

接口 说明
BiConsumer<T,U> 表示接受两个不同类型的参数,但不返回任何结果的操作
BiFunction<T,U,R> 表示接受两个不同类型的参数,并返回一个其它类型的结果的操作
BinaryOperator 表示接受两个相同类型的参数,并返回一个同一类型的结果的操作
BiPredicate<T,U> 表示接受两个不同诶行的参数,且返回布尔类型的结果的操作
BooleanSupplier 不接受任何参数,且返回一个布尔类型的结果的操作
DoubleBinaryOperator 表示接受两个double类型的参数,并返回double类型结果的操作
DoubleConsumer 表示接受一个double类型的参数,但不返回任何结果的操作
DoubleFunction 表示接受一个double类型的参数,且返回一个R类型的结果的操作
DoublePredicate 表示一个接受两个double类型的参数,且返回一个布尔类型的结果的操作
DoubleSupplier 表示一个不接受任何参数,但返回布尔类型的结果的操作
DoubleToIntFunction 表示接受两个double类型的参数,但返回一个int类型的结果的操作
DoubleToLongFunction 表示接受两个double类型的参数,但返回一个long类型的结果的操作
DoubleUnaryOperator 表示接受一个double类型的参数,且返回一个double类型的结果的操作
IntBinaryOperator 表示一个接受两个int类型的参数,且返回一个int类型的结果的操作
IntConsumer 表示接受一个int类型的参数,但不返回任何结果的操作
IntFunction 表示接受一个int类型的参数,但返回一个R类型的结果的操作
IntPredicate 表示接受一个int类型的参数,但返回布尔类型的结果的操作
IntSupplier 表示不接受任何参数,但返回一个int类型的结果的操作
IntToDoubleFunction 表示接受一个int类型的参数,但返回一个double类型的结果的操作
IntToLongFunction 表示接受一个int类型的参数,但返回一个long类型的结果的操作
IntUnaryOperator 表示接受一个int类型的参数,且返回一个int类型的结果的操作
LongBinaryOperator 表示接受两个long类型的参数,且返回一个long类型的结果的操作
LongConsumer 表示不接受任何参数,但返回一个long类型的结果的操作
LongFunction 表示接受一个loing类型的参数,但返回一个R类型的结果的操作
LongPredicate 表示接受一个long类型的参数,但返回布尔类型的结果的操作
LongSupplier 表示不接受任何参数,但返回一个long类型的结果的操作
LongToDoubleFunction 表示接受一个long类型的参数,但返回一个double类型的结果的函数
LongToIntFunction 表示接受一个long类型的参数,但返回int类型的结果的函数
LongUnaryOperator 表示接受一个long类型的参数,并返回一个long类型的结果的操作
ObjDoubleConsumer 表示接受两个参数,一个为T类型的对象,另一个double类型,但不返回任何结果的操作
ObjIntConsumer 表示接受两个参数,一个为T类型的对象,另一个int类型,但不返回任何结果的操作
ObjLongConsumer 表示接受两个参数,一个为T类型的对象,另一个double类型,但不返回任何结果的操作
ToDoubleBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个double类型的结果的操作
ToDoubleFunction 表示一个接受指定类型T的参数,并返回一个double类型的结果的操作
ToIntBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个int类型的结果的操作
ToIntFunction 表示一个接受指定类型T的参数,并返回一个int类型的结果的操作
ToLongBiFunction<T,U> 表示接受两个不同类型的参数,但返回一个long类型的结果的操作
ToLongFunction 表示一个接受指定类型的参数,并返回一个long类型的结果的操作
UnaryOperator 表示接受一个参数,并返回一个与参数类型相同的结果的操作

函数式接口使用非常灵活,上面的举例都是很简单的 demo,它需要我们在日常开发过程中多多使用才能灵活地运用它。


Java 8 新特性—方法引用和构造器引用

在前面我们了解了 Lambda 表达式,它能够简化我们的程序,但是它还不是最简单的,Java 8 引入了方法引用可以对 Lambda 表达式再进一步简化。

img

什么是方法引用

我们先看一个例子。首先定义一个 Student 类:

public class Student {
    private String name;

    private Integer age;

    public static int compareByAge(Student a,Student b) {
        return a.getAge().compareTo(b.getAge());
    }
}

Student 中含有一个静态方法 compareByAge(),它是用来比较年龄的。

现在需要实现一个需求,有一批学生我们希望能够根据 age 进行排序。

在没有学习 Lambda 表达式时,我们这样写:

public class MethodReferenceTest {
    public static void main(String[] args) {
        List<Student> studentList = Arrays.asList(
                new Student("小明",16),
                new Student("小红",14),
                new Student("小兰",15),
                new Student("小李",18),
                new Student("小张",14),
                new Student("小林",15)
        );

        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getAge().compareTo(o2.getAge());
            }
        });
        System.out.println(studentList);
    }
}

学习了 Lambda 表达式后,我们知道 Comparator 接口是一个函数式接口,因此我们可以使用Lambda表达式,而不需要使用这种匿名内部类的方式:

public class MethodReferenceTest {
    public static void main(String[] args) {
        // 省略代码...

        Collections.sort(studentList, (o1,o2) -> Student.compareByAge(o1,o2));
        System.out.println(studentList);
    }
}

注意,这里我们是使用 Student 类中的静态方法:compareByAge()。到这里后其实还有进一步的优化空间:

public class MethodReferenceTest {
    public static void main(String[] args) {
        // 省略代码...

        Collections.sort(studentList, Student::compareByAge);
        System.out.println(studentList);
    }
}

这段代码将 Lambda 表达式 (o1,o2) -> Student.compareByAge(o1,o2) 转变为了 Student::compareByAge 是不是很懵逼?

Student::compareByAge 写法就是我们这篇文章要讲的方法引用。那什么是方法引用呢?

方法引用是 Java 8 引入的特性,它提供了一种更加简洁的可用作 Lambda 表达式的表达方式。 定义:方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。

我们可以简单认为,方法引用是一种更加简洁易懂的 Lambda表达式。当 Lambda 表达式的主体中只有一个执行方法的调用时,我们可以不使用 Lambda 表达式,而是选择更加简洁的方法引用,这样可读性更高一些。

三种方法引用类型

方法引用的标准格式是:类名::方法名。它有如下三种类型:

类型 格式
引用静态方法 类名::静态方法名
引用对象的实例方法 实例对象::方法名
引用类型的任意对象的实例方法 类名::实例方法名

下面我们来看这三种类型的使用方法。

引用静态方法

引用静态方法的格式是:类名::静态方法名。这个是其实和我们使用静态方法一样,只不过是将 “.” 替换成了 “::”。其实我们上面那个例子就是引用静态方法的例子,这里大明哥再举一个示例,java.lang.Math 中有很多静态方法,比如:

// Lambda 表达式
Function<Integer,Integer> function1 = t -> Math.abs(t);
int result1 = function1.apply(-123);
        
// 方法引用
Function<Integer,Integer> function2 = Math::abs;
int result2 = function2.apply(-123); 

引用对象的实例方法

引用对象的实力方法格式是:实例对象名::实例方法名,这种方式引用的是一个实例方法,所以需要提供一个对象实例来引用,如下:

Student student = new Student("小明",15);

// Lambda 表达式
Supplier<String> supplier1 = () -> student.getName();
String name1 = supplier1.get();
 
// 方法引用  
Supplier<String> supplier2 = student::getName;
String name2 = supplier2.get();

这种方式在我们使用 Stream 来操作集合时用得非常多。

引用类型的任意对象的实例方法

引用类型的任意对象的实例方法的格式是:类名::实例方法名,这个有点儿不是很好理解。这种引用方式引用的是一个特定对象的实例方法,通常在函数式接口中作为第一个参数传递给方法引用,怎么理解呢?我们看下面两个例子:

比如 Comparator 中的 int compare(T o1, T o2),我们需要比较两个字符串的大小,使用方式如下:

Comparator<String> comparator = (o1,o2) -> o1.compareTo(o2);
System.out.println(comparator.compare("sike","sk"));

改成 类名::实例方法名 怎么改呢?

Comparator<String> comparator = String::compareTo;
System.out.println(comparator.compare("sike","sk"));

是不是比较懵逼?再看一个:

// Lambda 表达式
List<String> list = Arrays.asList("xiaoming","xiaohong","xiaoli","xiaodu");
list.forEach(name -> name.toUpperCase());
// 方法引用
List<String> list = Arrays.asList("xiaoming","xiaohong","xiaoli","xiaodu");
list.forEach(String::toUpperCase);

是不是比较懵?其实大明哥看到这个也比较懵,确实是不好理解,但是没关系,最后面大明哥教你们一个终极神器,让你使用方法引用不再困难。

方法引用的前提条件

方法引用确实可以极大地降低我们的代码量也更加清晰了,但是并不是所有的 Lambda 表达式都可以转换为方法引用。它有如下几个前提条件。

1、Lambda 表达式中只有一个调用方法的代码

注意这个一个调用方法的含义,它包含两重意思。

  • 只有一行代码
List<String> list = Arrays.asList("xiaoming","xiaohong","xiaoli","xiaodu");
list.forEach(name -> {
    System.out.println("www.skjava.com");
    name.toUpperCase();
});

这个 Lambda 中有两行代码,这是无法转换为方法引用的。

  • 只有一个调用方法
List<String> list = Arrays.asList("xiaoming","xiaohong","xiaoli","xiaodu");
list.forEach(name -> System.out.println(name.toUpperCase()));

这种写法也是转换不了的,虽然只有一行代码,但是它调用了两个方法。

2、方法引用的目标方法必须与Lambda 表达式的函数接口的抽象方法参数类型和返回类型相匹配

这就意味着目标方法的参数数量、类型以及返回类型必须与函数接口的要求一致。但是它只能规范引用静态方法引用对象的实例方法,而引用类型的任意对象的实例方法这种类型其实是不适用。

3、如果方法引用是通过对象引用来实现的,那么 Lambda 表达式中的参数列表的第一个参数必须是方法

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Archie_java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值