Java 8 Lambda表达式详解:从入门到实践

文章目录

    • 1. 引言:告别匿名内部类,拥抱Lambda
    • 2. Lambda表达式初探:匿名函数的魅力
      • 2.1 什么是Lambda?
      • 2.2 Lambda表达式与匿名内部类的对比
    • 3. Lambda的基石:函数式接口
      • 3.1 什么是函数式接口?
      • 3.2 Lambda表达式与函数式接口的关系
    • 4. 实战演练:环绕执行模式的Lambda化
      • 4.1 经典模式回顾
      • 4.2 Lambda重塑环绕执行
    • 5. Java 8的函数式接口工具箱
      • 5.1 `Predicate`:谓词判断
      • 5.2 `Consumer`:数据消费
      • 5.3 `Function`:类型转换
      • 5.4 常用函数式接口汇总
      • 5.5 其他实用函数式接口
    • 6. Lambda的类型世界:检查、推断与约束
      • 6.1 编译器如何看待Lambda?
      • 6.2 一样的Lambda,不一样的归宿
      • 6.3 编译器的小助手:类型推断
      • 6.4 自由变量的捕获:局部变量的限制
    • 7. 方法引用:Lambda的快捷方式
      • 7.1 什么是方法引用?
      • 7.2 构造函数引用
    • 8. Lambda和方法引用实战:排序案例研究
      • 8.1 传统方式:Comparator接口
      • 8.2 进化一:Lambda表达式
      • 8.3 进化二:方法引用
    • 9. 复合Lambda表达式:链式编程的艺术
      • 9.1 比较器复合
      • 9.2 谓词复合
      • 9.3 函数复合
    • 10 . 总结与展望

1. 引言:告别匿名内部类,拥抱Lambda

在Java的世界里,我们经常需要将一段代码(行为)作为参数传递。无论是启动一个新线程、处理事件,还是遍历集合,我们都离不开传递代码块。在Java 8之前,匿名内部类是实现这一目标的常用手段。然而,匿名内部类往往显得冗长而笨拙。

让我们来看一个经典的例子:启动一个新线程。在Java 8之前,你可能会这样写:

// 使用匿名内部类创建并启动一个线程
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }
}).start();

这段代码创建了一个Runnable接口的匿名内部类实例,重写了run方法,然后将这个实例传递给Thread的构造函数,最后启动线程。尽管逻辑非常简单——仅仅是打印一句话——但代码却显得相当繁琐。我们不得不编写大量的模板代码(new Runnable() { ... }),而真正核心的逻辑(打印消息)却被淹没在其中。

Lambda表达式的出现,正是为了解决这类问题。Lambda表达式提供了一种更简洁、更优雅的方式来表示可传递的代码块。有了Lambda表达式,上面的代码可以简化成这样:

// 使用Lambda表达式创建并启动一个线程
new Thread(() -> System.out.println("Hello from a thread!")).start();

看,代码一下子变得清爽多了!() -> System.out.println("Hello from a thread!")就是一个Lambda表达式。它表示“一个不需要参数的函数,其功能是打印一条消息”。我们不再需要创建匿名内部类,不再需要那些冗余的模板代码,只需要关注我们要执行的操作本身。

Lambda表达式带来的不仅仅是代码量的减少,它还带来了:

  • 更高的可读性: 代码更接近于我们的意图,更容易理解。
  • 更好的可维护性: 更少的代码通常意味着更少的错误和更容易的维护。
  • 更友好的并行处理: Lambda表达式与Stream API结合,可以轻松利用多核处理器的优势(这部分内容将在后续章节中详细讨论)。

在这篇文章中,我们将深入探讨Lambda表达式的方方面面。我们将从Lambda表达式的基本概念开始,逐步了解函数式接口、方法引用、类型推断等高级特性,并通过大量的实例来演示Lambda表达式在实际开发中的应用。让我们一起开启Lambda表达式的学习之旅,感受函数式编程的魅力吧!

2. Lambda表达式初探:匿名函数的魅力

2.1 什么是Lambda?

在上一节中,我们已经初步领略了Lambda表达式的简洁。现在,让我们更正式地定义它:

Lambda表达式本质上就是一个匿名函数。 它可以像普通函数一样接受参数、执行代码,并返回值。但与普通函数不同的是,Lambda表达式没有名称。

一个Lambda表达式由以下几个部分组成:

  • 参数列表: Lambda表达式可以有零个或多个参数。参数的类型可以显式声明,也可以由编译器根据上下文推断。
  • 箭头符号(->): 箭头符号将参数列表与Lambda表达式的主体分隔开。
  • 函数体: 函数体包含Lambda表达式要执行的代码。它可以是一个表达式,也可以是一个代码块。如果函数体是一个表达式,Lambda表达式会隐式地返回这个表达式的值。如果函数体是一个代码块,则需要使用return语句来返回值(或者没有返回值,就像void方法一样)。

让我们通过几个例子来更具体地理解Lambda表达式的构成:

// 1. 无参数,返回值为5的Lambda表达式
() -> 5

// 2. 接收一个String参数,打印该参数,无返回值的Lambda表达式
(String s) -> System.out.println(s)

// 3. 接收两个int参数,返回它们的和的Lambda表达式
(int a, int b) -> a + b

// 4. 接收一个String参数,判断其长度是否大于5,返回boolean值的Lambda表达式
(String s) -> s.length() > 5

// 5. 接收一个String参数,将其转换为大写并打印,无返回值的Lambda表达式(使用代码块)
(String s) -> {
    String upperCase = s.toUpperCase();
    System.out.println(upperCase);
}

// 6. 无参数的lambda, 且方法体是代码块的lambda
() -> {
    System.out.println("无参数的lambda");
    System.out.println("且方法体是代码块的lambda");
}

可以看到,Lambda表达式的形式非常灵活。你可以根据需要选择不同的写法。

2.2 Lambda表达式与匿名内部类的对比

在Java 8之前,匿名内部类是创建“一次性”对象的主要方式。现在,Lambda表达式在很多情况下都可以取代匿名内部类,使代码更简洁。

让我们再次回顾一下上一节中启动线程的例子。使用匿名内部类的写法是:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }
}).start();

使用Lambda表达式的写法是:

new Thread(() -> System.out.println("Hello from a thread!")).start();

对比这两段代码,我们可以发现:

  • Lambda表达式省去了创建匿名内部类的模板代码(new Runnable() { ... })。
  • Lambda表达式直接表达了我们想要执行的操作(System.out.println("Hello from a thread!")),而没有被淹没在冗余的代码中。

这使得Lambda表达式版本的代码更易读、更易理解。

需要注意的是,Lambda表达式并非仅仅是匿名内部类的“语法糖”。它们之间存在一些重要的区别(例如,this 关键字的含义不同),但就目前而言,你可以将Lambda表达式视为一种更简洁、更强大的匿名内部类替代品。

3. Lambda的基石:函数式接口

Lambda表达式的简洁和强大令人印象深刻。但是,你可能会有一个疑问:Lambda表达式可以被用在哪里?我们如何知道一个Lambda表达式应该写成什么样子?答案就是:函数式接口

3.1 什么是函数式接口?

函数式接口是只有一个抽象方法的接口。 这里的重点是"只有一个"和"抽象方法"。

  • 只有一个: 这意味着接口中不能有多个抽象方法。如果一个接口有多个抽象方法,那么Lambda表达式就无法确定它应该实现哪个方法了。
  • 抽象方法: 这意味着这个方法没有具体的实现(没有方法体)。接口中的默认方法(default methods)和静态方法(static methods)不属于抽象方法,它们都有具体的实现。

Java 8引入了一个特殊的注解:@FunctionalInterface。这个注解用于标记一个接口是函数式接口。虽然这个注解不是必需的(即使没有这个注解,只要接口满足函数式接口的定义,它仍然是函数式接口),但建议使用它。因为@FunctionalInterface注解有两个好处:

  1. 明确性: 它可以清楚地表明这个接口是一个函数式接口,方便其他开发者理解。
  2. 编译时检查: 如果你在一个标记了@FunctionalInterface的接口中添加了多个抽象方法,编译器会报错,帮助你避免错误。

让我们来看几个函数式接口的例子:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    // 其他默认方法和静态方法...
}

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
// 可以抛出异常

这些都是Java中常见的函数式接口。它们都只有一个抽象方法。

3.2 Lambda表达式与函数式接口的关系

现在,让我们来探讨Lambda表达式与函数式接口之间的紧密关系。

Lambda表达式可以用在任何需要函数式接口的地方

为什么呢?因为Lambda表达式本质上就是函数式接口的一个实例。当你写下一个Lambda表达式时,你实际上是在创建一个实现了函数式接口的匿名对象。Lambda表达式的类型就是它所实现的函数式接口的类型。

还记得我们之前启动线程的例子吗?

new Thread(() -> System.out.println("Hello from a thread!")).start();

() -> System.out.println("Hello from a thread!")这个Lambda表达式的类型是什么?是Runnable。因为Runnable是一个函数式接口,而这个Lambda表达式恰好实现了Runnable接口的唯一抽象方法run

那么, 编译器是如何知道Lambda应该实现哪一个方法的呢?

函数描述符:函数式接口的抽象方法的签名被称为函数描述符。Lambda表达式的签名必须与函数描述符匹配。

在上面的Runnable的例子中:

  • Runnable接口的run方法没有参数,也没有返回值(void)。
  • Lambda表达式() -> System.out.println("Hello from a thread!")也没有参数,也没有返回值。

它们的函数描述符是匹配的,所以这个Lambda表达式可以作为Runnable接口的一个实例。

再举一个例子,假设我们有一个函数式接口:

@FunctionalInterface
interface StringToIntConverter {
    int convert(String s);
}

这个接口的函数描述符是:接受一个String参数,返回一个int值。那么,以下Lambda表达式都是合法的StringToIntConverter实例:

StringToIntConverter c1 = s -> Integer.parseInt(s);
StringToIntConverter c2 = s -> s.length();

而以下Lambda表达式则是不合法的:

// 错误:Lambda表达式没有参数
StringToIntConverter c3 = () -> 42;

// 错误:Lambda表达式返回的是String,而不是int
StringToIntConverter c4 = s -> "The length is: " + s.length();

通过理解函数式接口和函数描述符,你就可以准确地判断一个Lambda表达式是否可以用于某个特定的上下文。

4. 实战演练:环绕执行模式的Lambda化

理论知识固然重要,但将理论付诸实践才能真正掌握。下面,我们将通过一个经典的例子——环绕执行模式——来展示Lambda表达式如何简化代码、提高可读性和可维护性。

4.1 经典模式回顾

什么是环绕执行模式?简单来说,它是一种处理资源(如文件、数据库连接、网络连接等)的常见模式。这种模式通常包含三个步骤:

  1. 打开资源: 获取资源,准备进行操作。
  2. 处理资源: 对资源进行读取、写入或其他操作。
  3. 关闭资源: 释放资源,避免资源泄漏。

一个典型的例子是读取文件内容。在Java中,我们通常会这样写:

public static String processFile() throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine(); // 读取一行
    }
}

这段代码展示了环绕执行模式的三个步骤:

  • new BufferedReader(new FileReader("data.txt")):打开文件资源。
  • br.readLine():处理文件资源(读取一行)。
  • try-with-resources语句:确保资源被正确关闭(即使发生异常)。

这段代码本身没有问题,但如果我们想要执行不同的文件处理操作(例如,读取两行、读取整个文件、过滤特定内容等),我们就需要编写不同的方法。这会导致代码重复,而且不够灵活。

4.2 Lambda重塑环绕执行

现在,让我们看看如何使用Lambda表达式来改进环绕执行模式。我们的目标是:将“处理资源”这一步的行为参数化,使其更灵活。

第一步:定义行为的抽象

首先,我们需要一种方式来表示不同的文件处理行为。这正是函数式接口的用武之地!我们可以定义一个函数式接口,来表示“接收一个BufferedReader,返回一个String”的行为:

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}

这里要注意的是,接口方法声明中抛出了IOException

第二步:让处理方法接受行为参数

现在,我们可以修改processFile方法,让它接受一个BufferedReaderProcessor参数:

public static String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br); // 使用传入的行为处理BufferedReader
    }
}

第三步:使用Lambda表达式传递行为

现在,我们可以使用Lambda表达式来调用processFile方法,并传入不同的文件处理行为:

// 读取一行
String oneLine = processFile(br -> br.readLine());

// 读取两行
String twoLines = processFile(br -> br.readLine() + br.readLine());

// 读取所有行并添加换行
String allLines = processFile(br -> {
   StringBuilder sb = new StringBuilder();
    String line;
    while((line = br.readLine()) != null){
        sb.append(line).append(System.lineSeparator());
    }
    return sb.toString();
});

我们通过Lambda表达式将不同的文件处理行为传递给了processFile方法,而processFile方法本身的代码保持不变。这大大提高了代码的灵活性和可重用性。我们不再需要为每一种不同的文件处理操作编写单独的方法,只需要传递不同的Lambda表达式即可。

通过这个例子,我们展示了Lambda表达式如何将“行为”作为参数传递,从而使代码更灵活、更简洁。在接下来的部分,我们将进一步探索Java 8为我们提供的丰富函数式接口工具箱。

完整的代码示例:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}

public class FileProcessing {

    public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return p.process(br); // 使用传入的行为处理BufferedReader
        }
    }

    public static void main(String[] args) throws IOException {
        // 读取一行
        String oneLine = processFile(br -> br.readLine());
        System.out.println("One line: " + oneLine);

        // 读取两行
        String twoLines = processFile(br -> br.readLine() + br.readLine());
        System.out.println("Two lines: " + twoLines);
        
         // 读取所有行并添加换行
        String allLines = processFile(br -> {
           StringBuilder sb = new StringBuilder();
            String line;
            while((line = br.readLine()) != null){
                sb.append(line).append(System.lineSeparator());
            }
            return sb.toString();
        });
        System.out.println("All lines: " + allLines);

    }
}

5. Java 8的函数式接口工具箱

通过前面的学习,我们已经知道Lambda表达式的强大之处在于可以将行为参数化,而函数式接口则是Lambda表达式的“载体”。为了更好地支持Lambda表达式,Java 8在java.util.function包中提供了大量预定义的函数式接口,它们涵盖了各种常见的应用场景。

5.1 Predicate:谓词判断

Predicate接口定义了一个名为test的抽象方法,它接受一个泛型类型T的对象,并返回一个boolean值。当你需要表示一个涉及类型T的布尔表达式时,就可以使用Predicate

  • 抽象方法: boolean test(T t)
  • 使用场景: 过滤集合中的元素。
  • 示例:
// 筛选出长度大于5的字符串
List<String> strings = Arrays.asList("Java", "Lambda", "Action", "in");
List<String> longStrings = strings.stream()
                                 .filter(s -> s.length() > 5) // 使用Lambda表达式
                                 .toList();
System.out.println(longStrings); // 输出:[Lambda, Action]

为了避免在处理基本类型时发生装箱操作(将int包装成Integer等),Java 8还提供了Predicate的原始类型特化版本,如IntPredicateLongPredicateDoublePredicate等。

5.2 Consumer:数据消费

Consumer接口定义了一个名为accept的抽象方法,它接受一个泛型类型T的对象,没有返回值(void)。当你需要对一个类型T的对象执行一些操作,但不需要返回任何结果时,就可以使用Consumer

  • 抽象方法: void accept(T t)
  • 使用场景: 打印对象、更新对象状态等。
  • 示例:
// 打印列表中的每个字符串
List<String> strings = Arrays.asList("Java", "Lambda", "Action");
strings.forEach(s -> System.out.println(s)); // 使用Lambda表达式

Predicate类似,Consumer也有原始类型特化版本,如IntConsumerLongConsumerDoubleConsumer等。

5.3 Function:类型转换

Function接口定义了一个名为apply的抽象方法,它接受一个泛型类型T的对象,并返回一个泛型类型R的对象。当你需要将一个类型T的对象转换为类型R的对象时(即进行类型转换或映射),就可以使用Function

  • 抽象方法: R apply(T t)
  • 使用场景: 将对象的一个字段提取出来、将对象转换为另一种类型等。
  • 示例:
// 将字符串列表转换为它们的长度列表
List<String> strings = Arrays.asList("Java", "Lambda", "in", "Action");
List<Integer> lengths = strings.stream()
                                .map(s -> s.length()) // 使用Lambda表达式
                                .toList();
System.out.println(lengths); // 输出:[4, 6, 2, 6]

Function的原始类型特化版本比较多,例如:

  • IntFunction:接受一个int值,返回一个R类型对象。
  • IntToLongFunction:接受一个int值,返回一个long值。
  • ToIntFunction:接受一个T类型对象,返回一个int值。
  • ToLongFunction:接受一个T类型对象,返回一个long值。

5.4 常用函数式接口汇总

为了方便大家查阅,我将常用的函数式接口总结在下表中:

接口 函数描述符 原始类型特化 用途
Predicate T -> boolean IntPredicate, LongPredicate, DoublePredicate 表示一个谓词(一个返回布尔值的函数)
Consumer T -> void IntConsumer, LongConsumer, DoubleConsumer 对类型为T的对象执行操作
Function T -> R IntFunction, IntToLongFunction T类型对象转换为R类型对象
Supplier () -> T IntSupplier, LongSupplier, DoubleSupplier 提供一个T类型的对象(例如工厂方法)
UnaryOperator T -> T IntUnaryOperator, LongUnaryOperator T类型对象进行一元操作(结果类型仍为T
BinaryOperator (T, T) -> T IntBinaryOperator, LongBinaryOperator 对两个T类型对象进行二元操作(结果类型仍为T
BiPredicate (L, R) -> boolean 表示一个接受两个参数的谓词
BiConsumer (T, U) -> void 对两个不同类型的对象执行操作
BiFunction (T, U) -> R 将两个不同类型的对象转换为R类型的对象

5.5 其他实用函数式接口

除了上述表格中的函数式接口,java.util.function 包中还有很多其他有用的函数式接口。

这些函数式接口为我们提供了丰富的工具,可以满足各种不同的编程需求。通过熟练掌握这些接口,我们可以写出更简洁、更灵活、更强大的代码。掌握了这些内置的函数式接口,我们就可以在大多数情况下避免自己定义新的函数式接口。

6. Lambda的类型世界:检查、推断与约束

前面的内容中,我们已经了解了Lambda表达式如何与函数式接口协同工作。现在,让我们更深入地了解Java编译器是如何处理Lambda表达式的类型,以及Lambda表达式在使用上的一些限制。

6.1 编译器如何看待Lambda?

当你编写一个Lambda表达式时,Java编译器会进行类型检查,以确保Lambda表达式可以安全地用在上下文中。类型检查的过程大致如下:

  1. 寻找目标类型: 编译器首先会查看Lambda表达式出现的上下文,确定它的目标类型(即它所期望的函数式接口类型)。例如,如果你将Lambda表达式作为参数传递给一个方法,编译器会查看该方法的参数类型,以确定目标类型。
  2. 匹配函数描述符: 编译器会检查Lambda表达式的参数和返回值类型是否与目标类型的函数描述符(抽象方法的签名)匹配。参数的数量和类型必须一致,返回值类型必须兼容(相同或者是目标类型的子类型)。
  3. 异常兼容性检查: 如果Lambda表达式体抛出了异常,编译器会检查这些异常是否与目标类型的抽象方法声明中允许抛出的异常兼容(相同或者是目标类型的子类型,或者是未经检查的异常)。

如果类型检查通过,Lambda表达式就可以被认为是目标类型的有效实例。否则,编译器会报错。

6.2 一样的Lambda,不一样的归宿

有趣的是,同一个Lambda表达式可以被用于不同的函数式接口,只要它们的函数描述符兼容。这意味着Lambda表达式本身并没有固定的类型,它的类型取决于它所处的上下文。

例如,考虑以下Lambda表达式:

() -> System.out.println("Hello");

这个Lambda表达式没有参数,也没有返回值。它可以被用作Runnable接口的实例:

Runnable r = () -> System.out.println("Hello");

也可以被用作Callable接口的实例(虽然Callable通常用于有返回值的场景):

Callable<Void> c = () -> {
    System.out.println("Hello");
    return null; // Callable需要返回值,这里返回null
};

甚至可以被用作我们自定义的一个无参无返回值的函数式接口的实例。

这种灵活性是Lambda表达式的一个重要特性。它使得Lambda表达式可以在不同的场景中重复使用,而无需进行修改。

6.3 编译器的小助手:类型推断

为了进一步简化Lambda表达式的编写,Java编译器提供了类型推断的功能。这意味着在很多情况下,你可以省略Lambda表达式中参数的类型,编译器会根据上下文自动推断出这些类型。

例如,考虑以下Lambda表达式:

Comparator<String> c = (String s1, String s2) -> s1.compareTo(s2);

这里,我们显式地声明了参数s1s2的类型为String。但实际上,我们可以省略这些类型声明:

Comparator<String> c = (s1, s2) -> s1.compareTo(s2);

编译器会根据Comparator接口的函数描述符推断出s1s2的类型为String

类型推断可以使Lambda表达式更简洁,但它也依赖于编译器能够明确地推断出类型。如果上下文信息不足,导致编译器无法推断类型,你仍然需要显式地声明类型。

6.4 自由变量的捕获:局部变量的限制

Lambda表达式可以访问其定义所在的作用域中的变量。这被称为“变量捕获”。但是,对于局部变量的捕获,Lambda表达式有一些限制。

Lambda表达式可以捕获以下类型的变量:

  • 实例变量
  • 静态变量
  • 局部变量

对于实例变量和静态变量,Lambda表达式可以自由地读取和修改它们。但是,对于局部变量,Lambda表达式只能捕获那些 effectively final 的变量。

什么是 effectively final 的变量?简单来说,就是一个变量在初始化后,其值没有被改变过。换句话说,如果你可以将一个局部变量声明为 final 而不引起编译错误,那么这个变量就是 effectively final 的。

例如:

int count = 0; //effectively final
Runnable r1 = () -> System.out.println(count); // 可以捕获

int number = 1;
number = 2; //改变了值
Runnable r2 = ()-> System.out.println(number); //编译报错.

final String name = "Lambda";
Runnable r3 = () -> System.out.println("Hello, " + name); // 可以捕获

String greeting = "Hello";
// greeting = "Hi"; // 如果取消注释这一行,下面的Lambda表达式会报错
Runnable r4 = () -> System.out.println(greeting);

为什么Lambda表达式只能捕获 effectively final 的局部变量呢?

这背后涉及到Java的内存模型和Lambda表达式的实现方式。但从概念上,你可以这样理解:**Lambda表达式可能会在一个与定义它的线程不同的线程中执行。如果Lambda表达式可以修改局部变量的值,就可能会导致线程安全问题**。通过限制只能捕获 effectively final 的局部变量,Java避免了这种潜在的风险。

7. 方法引用:Lambda的快捷方式

Lambda表达式已经足够简洁,但Java 8还提供了另一种更简洁的表达方式:方法引用。尤其是在Lambda表达式仅仅是调用一个已经存在的方法时。

7.1 什么是方法引用?

方法引用可以被看作是Lambda表达式的一种“快捷方式”或“语法糖”。它允许你直接引用现有的方法,而无需重新编写Lambda表达式。方法引用使用::操作符,将方法名与类名或对象名分隔开。

有三种主要的方法引用类型:

  1. 静态方法引用: ClassName::staticMethodName

    • 例如:Integer::parseInt (等价于 s -> Integer.parseInt(s)
  2. 实例方法引用(指向特定对象): objectReference::instanceMethodName

    • 例如:System.out::println (等价于 s -> System.out.println(s))
    • 例如:假定一个局部变量expensiveTransaction用于存放Transaction类型的对象, 它支持实例 方法getValue,那么你就可以这么写expensiveTransaction::getValue
  3. 实例方法引用(指向任意对象): ClassName::instanceMethodName

    • 例如:String::length (等价于 (String s) -> s.length())
    • 这种方式有点特殊。它表示引用一个特定类型的所有对象的实例方法。在调用时,Lambda表达式的第一个参数会成为方法的调用者。

让我们通过一些例子来更好地理解这三种方法引用:

// 静态方法引用
Function<String, Integer> stringToInt = Integer::parseInt;

// 实例方法引用(指向特定对象)
Consumer<String> printString = System.out::println;

// 实例方法引用(指向任意对象)
Function<String, Integer> stringLength = String::length;
BiPredicate<List<String>, String> contains = List::contains;

这些方法引用都比对应的Lambda表达式更简洁、更易读。它们直接表达了“使用这个方法”的意图,而无需重复方法的实现细节。

7.2 构造函数引用

除了普通方法,方法引用还可以用于构造函数。你可以使用ClassName::new的形式来引用一个类的构造函数。

例如:

// 无参构造函数引用
Supplier<List<String>> newListSupplier = ArrayList::new;
List<String> list = newListSupplier.get(); // 创建一个新的ArrayList

// 有参构造函数引用
Function<Integer, String[]> stringArrayCreator = String[]::new;
String[] stringArray = stringArrayCreator.apply(10); //创建一个长度为10的String类型数组。

构造函数引用可以让你像使用普通方法一样使用构造函数,从而可以将其传递给函数式接口,实现延迟创建对象等功能。

方法引用提供了一种比Lambda表达式更简洁、更直接的方式来引用已有的方法或构造函数。在合适的场景下使用方法引用,可以进一步提高代码的可读性和表达力。

8. Lambda和方法引用实战:排序案例研究

为了更好地理解Lambda表达式和方法引用的实际应用,我们将通过一个经典的例子——排序——来展示如何逐步简化代码,从传统的写法过渡到Lambda表达式,最终使用方法引用。

假设我们有一个Apple类:

public class Apple {
    private String color;
    private int weight;

    public Apple(String color, int weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
        return color;
    }

    public int getWeight() {
        return weight;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "color='" + color + '\'' +
                ", weight=" + weight +
                '}';
    }
}

我们有一个List,现在需要对这个列表按照苹果的重量进行排序。

8.1 传统方式:Comparator接口

在Java 8之前,我们通常会使用Comparator接口和匿名内部类来实现排序:

List<Apple> apples = Arrays.asList(
        new Apple("green", 150),
        new Apple("red", 120),
        new Apple("yellow", 180)
);

apples.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple a1, Apple a2) {
        return Integer.compare(a1.getWeight(), a2.getWeight());
    }
});

System.out.println(apples);

这种写法虽然能够实现功能,但代码显得冗长,不够直观。核心的比较逻辑被淹没在大量的模板代码中。

8.2 进化一:Lambda表达式

使用Lambda表达式,我们可以将上面的代码简化为:

List<Apple> apples = Arrays.asList(
        new Apple("green", 150),
        new Apple("red", 120),
        new Apple("yellow", 180)
);

apples.sort((a1, a2) -> Integer.compare(a1.getWeight(), a2.getWeight()));

System.out.println(apples);

Lambda表达式使代码更简洁,更易读。我们不再需要编写匿名内部类的模板代码,只需要关注比较逻辑即可。

8.3 进化二:方法引用

利用Java 8提供的Comparator.comparingInt方法和方法引用,我们可以将代码进一步简化:

List<Apple> apples = Arrays.asList(
        new Apple("green", 150),
        new Apple("red", 120),
        new Apple("yellow", 180)
);

apples.sort(Comparator.comparingInt(Apple::getWeight));

System.out.println(apples);

这段代码是目前为止最简洁、最易读的版本。Comparator.comparingInt方法接受一个Function(用于提取排序键),并返回一个ComparatorApple::getWeight是一个方法引用,它指向Apple类的getWeight方法。

通过这个排序的例子,我们看到了Lambda表达式和方法引用如何逐步简化代码,提高代码的可读性和可维护性。它们不仅减少了模板代码,还使代码更接近于问题本身的描述。我们不再需要纠结于如何实现比较逻辑的细节,只需要声明“按照苹果的重量排序”即可。

9. 复合Lambda表达式:链式编程的艺术

Java 8不仅提供了丰富的预定义函数式接口,还为其中一些接口增加了实用的默认方法。这些默认方法允许我们将多个Lambda表达式组合起来,形成更复杂的行为,就像搭积木一样。这种“链式”的编程风格可以极大地提高代码的可读性和表达力。

9.1 比较器复合

在前面的排序例子中,我们使用Comparator.comparingInt(Apple::getWeight)按苹果的重量对列表进行排序。如果现在有两个苹果的重量相同,我们希望再按照颜色对它们进行排序,该怎么办呢?

Comparator接口提供了一个名为thenComparing的默认方法,它允许我们创建比较器链。我们可以这样写:

List<Apple> apples = Arrays.asList(
        new Apple("green", 150),
        new Apple("red", 120),
        new Apple("yellow", 150),
        new Apple("green",120)
);

apples.sort(
    Comparator.comparingInt(Apple::getWeight) // 首先按重量排序
             .thenComparing(Apple::getColor)   // 然后按颜色排序
);

System.out.println(apples);
//输出:
//[Apple{color='green', weight=120}, Apple{color='red', weight=120}, Apple{color='green', weight=150}, Apple{color='yellow', weight=150}]

这段代码首先按照苹果的重量进行排序,如果重量相同,则按照颜色进行排序。thenComparing方法接受另一个Comparator作为参数,用于在第一个比较器认为两个元素相等时进行进一步的比较。

我们还可以对比较器链进行更多的操作,例如使用reversed()方法进行降序排序:

apples.sort(
    Comparator.comparingInt(Apple::getWeight)
             .reversed() // 先按重量降序
             .thenComparing(Apple::getColor) // 然后按颜色升序
);

9.2 谓词复合

Predicate接口提供了三个默认方法:negateandor,它们分别对应逻辑非、逻辑与和逻辑或操作。我们可以使用这些方法来组合多个Predicate,构建更复杂的过滤条件。

例如,假设我们想要筛选出重量大于150或者颜色是红色的苹果:

Predicate<Apple> heavierThan150 = apple -> apple.getWeight() > 150;
Predicate<Apple> redApple = apple -> "red".equals(apple.getColor());

Predicate<Apple> heavyOrRed = heavierThan150.or(redApple); // 组合两个谓词

List<Apple> apples = Arrays.asList(
        new Apple("green", 150),
        new Apple("red", 120),
        new Apple("yellow", 180)
);

List<Apple> filteredApples = apples.stream()
                                   .filter(heavyOrRed)
                                   .toList();

System.out.println(filteredApples);
//输出: [Apple{color='red', weight=120}, Apple{color='yellow', weight=180}]

我们还可以使用and方法来组合谓词,表示同时满足两个条件:

Predicate<Apple> heavyAndRed = heavierThan150.and(redApple); // 重量大于150且颜色是红色

使用negate方法可以对谓词取反:

Predicate<Apple> notRedApple = redApple.negate(); // 颜色不是红色

9.3 函数复合

Function接口提供了两个默认方法:andThencompose,它们允许我们将多个Function组合起来,形成函数流水线。

  • andThen方法:先应用当前函数,然后将结果作为参数传递给下一个函数。
  • compose方法:先应用传入的函数,然后将结果作为参数传递给当前函数。

例如,假设我们有两个函数:

Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> multiplyByTwo = x -> x * 2;

我们可以使用andThen方法将它们组合起来,先加一,然后乘以二:

Function<Integer, Integer> addOneAndMultiplyByTwo = addOne.andThen(multiplyByTwo);
int result = addOneAndMultiplyByTwo.apply(5); // (5 + 1) * 2 = 12
System.out.println(result);

我们也可以使用compose方法将它们组合起来,先乘以二,然后加一:

Function<Integer, Integer> multiplyByTwoAndAddOne = addOne.compose(multiplyByTwo);
int result2 = multiplyByTwoAndAddOne.apply(5); // (5 * 2) + 1 = 11
System.out.println(result2);

通过andThencompose方法,我们可以将多个简单的函数组合成更复杂的函数,实现更强大的功能。

复合Lambda表达式的能力是函数式编程思想的一个重要体现。它允许我们将复杂的逻辑分解成一系列简单的、可重用的函数,然后将这些函数组合起来,构建出更强大的功能。这种编程风格不仅提高了代码的可读性和可维护性,还为并行化提供了便利。

10 . 总结与展望

经过前面一系列的深入探讨,我们已经全面了解了Java 8中Lambda表达式的方方面面。让我们回顾一下本次Lambda之旅的主要内容:

  • Lambda表达式的本质: 我们从Lambda表达式的定义入手,认识到它是一种简洁的匿名函数,可以作为参数传递。
  • 函数式接口: 我们理解了函数式接口是Lambda表达式的“载体”,只有符合函数式接口定义的Lambda表达式才能被正确使用。
  • 环绕执行模式: 通过一个实际案例,我们看到了Lambda表达式如何将“行为”参数化,从而简化代码,提高灵活性。
  • Java 8的函数式接口工具箱: 我们了解了java.util.function包中丰富的预定义函数式接口,以及它们的应用场景。
  • 类型检查、推断与约束: 我们深入探讨了Java编译器如何处理Lambda表达式的类型,以及Lambda表达式在使用上的一些限制(如变量捕获)。
  • 方法引用: 我们学习了方法引用这种更简洁的Lambda表达形式,以及如何使用方法引用来引用现有方法和构造函数。
  • 实战案例: 通过排序的例子,我们看到了Lambda表达式和方法引用如何协同工作,将代码简化到极致。
  • 复合Lambda: 我们掌握了如何使用ComparatorPredicateFunction接口的默认方法来组合Lambda表达式,构建更复杂的行为。

Lambda表达式的引入,是Java语言发展的一个重要里程碑。它不仅使Java代码更简洁、更易读,还为Java带来了函数式编程的强大能力。Lambda表达式与Stream API(后续章节将详细介绍)的结合,更是开启了Java集合处理的新篇章。

虽然本次Lambda之旅即将结束,但这仅仅是Java函数式编程的开始。在未来的Java版本中,我们有理由相信Lambda表达式和函数式编程的思想将会得到更广泛的应用和发展。希望这篇博客能为你理解和使用Lambda表达式提供帮助,让你在Java编程的道路上更上一层楼!

你可能感兴趣的:(java学习笔记,java)