在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表达式的方方面面。我们将从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表达式的形式非常灵活。你可以根据需要选择不同的写法。
在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();
对比这两段代码,我们可以发现:
new Runnable() { ... }
)。System.out.println("Hello from a thread!")
),而没有被淹没在冗余的代码中。这使得Lambda表达式版本的代码更易读、更易理解。
需要注意的是,Lambda表达式并非仅仅是匿名内部类的“语法糖”。它们之间存在一些重要的区别(例如,this
关键字的含义不同),但就目前而言,你可以将Lambda表达式视为一种更简洁、更强大的匿名内部类替代品。
Lambda表达式的简洁和强大令人印象深刻。但是,你可能会有一个疑问:Lambda表达式可以被用在哪里?我们如何知道一个Lambda表达式应该写成什么样子?答案就是:函数式接口。
函数式接口是只有一个抽象方法的接口。 这里的重点是"只有一个"和"抽象方法"。
Java 8引入了一个特殊的注解:@FunctionalInterface
。这个注解用于标记一个接口是函数式接口。虽然这个注解不是必需的(即使没有这个注解,只要接口满足函数式接口的定义,它仍然是函数式接口),但建议使用它。因为@FunctionalInterface
注解有两个好处:
@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中常见的函数式接口。它们都只有一个抽象方法。
现在,让我们来探讨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
)。() -> 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表达式是否可以用于某个特定的上下文。
理论知识固然重要,但将理论付诸实践才能真正掌握。下面,我们将通过一个经典的例子——环绕执行模式——来展示Lambda表达式如何简化代码、提高可读性和可维护性。
什么是环绕执行模式?简单来说,它是一种处理资源(如文件、数据库连接、网络连接等)的常见模式。这种模式通常包含三个步骤:
一个典型的例子是读取文件内容。在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
语句:确保资源被正确关闭(即使发生异常)。这段代码本身没有问题,但如果我们想要执行不同的文件处理操作(例如,读取两行、读取整个文件、过滤特定内容等),我们就需要编写不同的方法。这会导致代码重复,而且不够灵活。
现在,让我们看看如何使用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);
}
}
通过前面的学习,我们已经知道Lambda表达式的强大之处在于可以将行为参数化,而函数式接口则是Lambda表达式的“载体”。为了更好地支持Lambda表达式,Java 8在java.util.function
包中提供了大量预定义的函数式接口,它们涵盖了各种常见的应用场景。
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
的原始类型特化版本,如IntPredicate
、LongPredicate
、DoublePredicate
等。
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
也有原始类型特化版本,如IntConsumer
、LongConsumer
、DoubleConsumer
等。
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
值。为了方便大家查阅,我将常用的函数式接口总结在下表中:
接口 | 函数描述符 | 原始类型特化 | 用途 |
---|---|---|---|
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 类型的对象 |
除了上述表格中的函数式接口,java.util.function
包中还有很多其他有用的函数式接口。
这些函数式接口为我们提供了丰富的工具,可以满足各种不同的编程需求。通过熟练掌握这些接口,我们可以写出更简洁、更灵活、更强大的代码。掌握了这些内置的函数式接口,我们就可以在大多数情况下避免自己定义新的函数式接口。
前面的内容中,我们已经了解了Lambda表达式如何与函数式接口协同工作。现在,让我们更深入地了解Java编译器是如何处理Lambda表达式的类型,以及Lambda表达式在使用上的一些限制。
当你编写一个Lambda表达式时,Java编译器会进行类型检查,以确保Lambda表达式可以安全地用在上下文中。类型检查的过程大致如下:
如果类型检查通过,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表达式可以在不同的场景中重复使用,而无需进行修改。
为了进一步简化Lambda表达式的编写,Java编译器提供了类型推断的功能。这意味着在很多情况下,你可以省略Lambda表达式中参数的类型,编译器会根据上下文自动推断出这些类型。
例如,考虑以下Lambda表达式:
Comparator<String> c = (String s1, String s2) -> s1.compareTo(s2);
这里,我们显式地声明了参数s1
和s2
的类型为String
。但实际上,我们可以省略这些类型声明:
Comparator<String> c = (s1, s2) -> s1.compareTo(s2);
编译器会根据Comparator
接口的函数描述符推断出s1
和s2
的类型为String
。
类型推断可以使Lambda表达式更简洁,但它也依赖于编译器能够明确地推断出类型。如果上下文信息不足,导致编译器无法推断类型,你仍然需要显式地声明类型。
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避免了这种潜在的风险。
Lambda表达式已经足够简洁,但Java 8还提供了另一种更简洁的表达方式:方法引用。尤其是在Lambda表达式仅仅是调用一个已经存在的方法时。
方法引用可以被看作是Lambda表达式的一种“快捷方式”或“语法糖”。它允许你直接引用现有的方法,而无需重新编写Lambda表达式。方法引用使用::
操作符,将方法名与类名或对象名分隔开。
有三种主要的方法引用类型:
静态方法引用: ClassName::staticMethodName
Integer::parseInt
(等价于 s -> Integer.parseInt(s)
)实例方法引用(指向特定对象): objectReference::instanceMethodName
System.out::println
(等价于 s -> System.out.println(s)
)expensiveTransaction
用于存放Transaction
类型的对象, 它支持实例 方法getValue
,那么你就可以这么写expensiveTransaction::getValue
实例方法引用(指向任意对象): ClassName::instanceMethodName
String::length
(等价于 (String s) -> s.length()
)让我们通过一些例子来更好地理解这三种方法引用:
// 静态方法引用
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表达式更简洁、更易读。它们直接表达了“使用这个方法”的意图,而无需重复方法的实现细节。
除了普通方法,方法引用还可以用于构造函数。你可以使用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表达式更简洁、更直接的方式来引用已有的方法或构造函数。在合适的场景下使用方法引用,可以进一步提高代码的可读性和表达力。
为了更好地理解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
,现在需要对这个列表按照苹果的重量进行排序。
在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);
这种写法虽然能够实现功能,但代码显得冗长,不够直观。核心的比较逻辑被淹没在大量的模板代码中。
使用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表达式使代码更简洁,更易读。我们不再需要编写匿名内部类的模板代码,只需要关注比较逻辑即可。
利用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
(用于提取排序键),并返回一个Comparator
。Apple::getWeight
是一个方法引用,它指向Apple
类的getWeight
方法。
通过这个排序的例子,我们看到了Lambda表达式和方法引用如何逐步简化代码,提高代码的可读性和可维护性。它们不仅减少了模板代码,还使代码更接近于问题本身的描述。我们不再需要纠结于如何实现比较逻辑的细节,只需要声明“按照苹果的重量排序”即可。
Java 8不仅提供了丰富的预定义函数式接口,还为其中一些接口增加了实用的默认方法。这些默认方法允许我们将多个Lambda表达式组合起来,形成更复杂的行为,就像搭积木一样。这种“链式”的编程风格可以极大地提高代码的可读性和表达力。
在前面的排序例子中,我们使用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) // 然后按颜色升序
);
Predicate
接口提供了三个默认方法:negate
、and
和or
,它们分别对应逻辑非、逻辑与和逻辑或操作。我们可以使用这些方法来组合多个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(); // 颜色不是红色
Function
接口提供了两个默认方法:andThen
和compose
,它们允许我们将多个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);
通过andThen
和compose
方法,我们可以将多个简单的函数组合成更复杂的函数,实现更强大的功能。
复合Lambda表达式的能力是函数式编程思想的一个重要体现。它允许我们将复杂的逻辑分解成一系列简单的、可重用的函数,然后将这些函数组合起来,构建出更强大的功能。这种编程风格不仅提高了代码的可读性和可维护性,还为并行化提供了便利。
经过前面一系列的深入探讨,我们已经全面了解了Java 8中Lambda表达式的方方面面。让我们回顾一下本次Lambda之旅的主要内容:
java.util.function
包中丰富的预定义函数式接口,以及它们的应用场景。Comparator
、Predicate
和Function
接口的默认方法来组合Lambda表达式,构建更复杂的行为。Lambda表达式的引入,是Java语言发展的一个重要里程碑。它不仅使Java代码更简洁、更易读,还为Java带来了函数式编程的强大能力。Lambda表达式与Stream API(后续章节将详细介绍)的结合,更是开启了Java集合处理的新篇章。
虽然本次Lambda之旅即将结束,但这仅仅是Java函数式编程的开始。在未来的Java版本中,我们有理由相信Lambda表达式和函数式编程的思想将会得到更广泛的应用和发展。希望这篇博客能为你理解和使用Lambda表达式提供帮助,让你在Java编程的道路上更上一层楼!