Lambda表达式基本理解及使用
- 背景
- Lambda 表达式语法
- 函数式接口
- 常见的函数式接口
- Function<T, R>
- Consumer<T>
- Supplier<T>
- Predicate<T>
- UnaryOperator<T>
- BinaryOperator<T>
- Lambda表达式的基本使用
- 有返回值函数式接口
- 无返回值函数式接口
- Lambda表达式和匿名内部类的区别
- Lambda表达式在使用时有一些重要的注意事项
背景
Lambda 表达式是Java 8引入的一个重要特性,它旨在简化函数对象的创建过程,使得Java语言更加简洁、功能更加强大,尤其是在处理集合、流和并发编程中表现突出。Lambda允许将功能作为方法参数或将其赋值给变量,这在函数式编程范式中尤为重要,它促进了行为参数化,即代码可以根据传入的行为(函数)动态变化。
Lambda 表达式(Lambda expression)可以看作是一个匿名函数
Lambda 表达式语法
Lambda表达式的语法结构一般为:
(parameters) -> expression
或者
(parameters) -> {
statements;
}
parameters
:一个或多个参数,参数之间用逗号分隔,无参数可留空。->
:箭头符号,表示“指向”,连接参数列表和主体。expression
:单个表达式,当Lambda体只有一条语句时可直接使用。{ statements; }
:当Lambda体包含多条语句时,需要用花括号包裹起来,并明确指定返回值(如果有的话)。
// 1. 不需要参数,返回值为 2
() -> 2
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的和
(x, y) -> x + y
// 4. 接收2个int型整数,返回他们的乘积
(int x, int y) -> x * y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
函数式接口
函数式接口是只有一个抽象方法的接口,它是Lambda表达式的基础。Java 8引入了@FunctionalInterface
注解来标记这类接口,但即使没有这个注解,只要满足条件的接口也可以作为Lambda的目标类型。
常见的函数式接口
- `Function<T, R>``:接收一个T类型的输入,产生一个R类型的输出。
Consumer<T>
:接收一个输入参数并执行操作,无返回值。Supplier<T>
:无参,产生一个结果。Predicate<T>
:接收一个输入参数,返回一个布尔值。UnaryOperator<T>
:一元操作符,输入和输出类型相同。BinaryOperator<T>
:二元操作符,输入和输出类型相同。
Function<T, R>
Function接口代表一个接受一个输入参数,产生一个输出的函数。它有一个抽象方法R apply(T t)。
示例:将字符串转换为大写。
Function<String, String> toUpperCase = String::toUpperCase;
String result = toUpperCase.apply("hello world");
System.out.println(result); // 输出: HELLO WORLD
Consumer
Consumer接口代表一个接收单个输入参数并且无返回的操作。它有一个抽象方法void accept(T t)。
示例:打印字符串。
Consumer<String> printer = System.out::println;
printer.accept("This is an example of using Consumer."); // 直接打印字符串
Supplier
Supplier接口不接受任何参数,但它可以提供一个结果。它有一个抽象方法T get()。
示例:生成随机数。
Supplier<Integer> randomSupplier = () -> new Random().nextInt(100);
System.out.println(randomSupplier.get()); // 输出一个0到99之间的随机数
Predicate
Predicate接口代表一个布尔值条件,它接受一个输入参数并返回一个布尔值结果。它有一个抽象方法boolean test(T t)。
示例:检查字符串是否为空。
Predicate<String> isEmpty = String::isEmpty;
System.out.println(isEmpty.test("")); // 输出: true
System.out.println(isEmpty.test("not empty")); // 输出: false
UnaryOperator
UnaryOperator是一个特殊的Function,它的输入和输出类型相同。
示例:自增操作。
UnaryOperator<Integer> increment = x -> x + 1;
System.out.println(increment.apply(10)); // 输出: 11
BinaryOperator
BinaryOperator也是一个特殊的Function,用于两个相同类型的操作数,并产生相同类型的结果。
示例:两个整数相加。
BinaryOperator<Integer> add = Integer::sum;
System.out.println(add.apply(5, 3)); // 输出: 8
Lambda表达式的基本使用
有返回值函数式接口
//无参
@FunctionalInterface
interface RandomSupplier {
int getRandom();
}
//一个参数
@FunctionalInterface
interface SquareFunction {
int square(int number);
}
//两个参数
@FunctionalInterface
interface MaxFinder {
int findMax(int a, int b);
}
public static void main(String[] args) {
RandomSupplier r = () -> new Random().nextInt(100);
System.out.println(r.getRandom()); // 输出一个0到99的随机数
SquareFunction square = number -> number * number;
System.out.println(square.square(5)); // 输出: 25
MaxFinder maxFinder = (a, b) -> Math.max(a, b);
System.out.println(maxFinder.findMax(10, 20)); // 输出: 20
}
无返回值函数式接口
//无参
@FunctionalInterface
interface NoReturnAction {
void performAction();
}
//一个参数
@FunctionalInterface
interface SingleParamAction<T> {
void execute(T param);
}
//两个参数
@FunctionalInterface
interface DualParamAction<T, U> {
void apply(T first, U second);
}
public static void main(String[] args) {
NoReturnAction greetUser = () -> System.out.println("欢迎来到我们的平台!");
greetUser.performAction(); // 执行动作
SingleParamAction<String> logMessage = message -> System.out.println("记录信息: " + message);
logMessage.execute("用户登录事件"); // 执行动作并传递参数
DualParamAction<String, Integer> processOrder = (item, quantity) ->
System.out.println("处理订单: " + item + ", 数量: " + quantity);
processOrder.apply("苹果", 10); // 执行动作并传递两个参数
}
Lambda表达式和匿名内部类的区别
1.所需类型
- Lambda表达式:适用于函数式接口(即只有一个抽象方法的接口)。Lambda表达式能够直接提供这个方法的实现。
- 匿名内部类:可以用于任何类型的类或接口,不限于函数式接口。对于有多个抽象方法的接口或抽象类,必须实现所有抽象方法。
2.使用限制不同
- Lambda表达式:由于其简洁性,通常用于简单操作。不支持复杂的多行语句或声明额外变量(除非在局部作用域内)。
- 匿名内部类:提供了更多的灵活性,可以在其中定义变量、实现复杂的逻辑,甚至包含构造方法。
3.使用原理不同
- Lambda表达式:编译器会自动推断出目标类型,并将其转换为一个函数式接口的实例。Lambda表达式本质上是一种编译器的语法糖,使得代码更加简洁。 编译之后,没有一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成
- 匿名内部类:在编译时会创建一个新的类(没有类名),这个类继承或实现了指定的父类或接口,并在运行时实例化这个新类的对象。
public class LambdaVsAnonymous {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 使用Lambda表达式遍历列表并打印元素
names.forEach(name -> System.out.println(name));
System.out.println("------------------------------");
// 使用匿名内部类遍历列表并打印元素
names.forEach(new Consumer<String>() {
@Override
public void accept(String name) {
System.out.println(name);
}
});
// 使用匿名内部类实例化抽象类Shape
Shape shape = new Shape() {
@Override
void draw() {
System.out.println("绘制形状");
}
};
// 调用抽象方法
shape.draw();
}
}
abstract class Shape {
abstract void draw();
}
Lambda表达式在使用时有一些重要的注意事项
- 类型推断:Java编译器能够根据上下文自动推断Lambda表达式的类型,因此通常不需要显式声明类型。但是,理解其背后的目标类型(通常是函数式接口)对于正确使用Lambda至关重要。
- 函数式接口:Lambda表达式只能用于实现具有一个抽象方法的接口(称为函数式接口)。如果试图将Lambda赋值给非函数式接口,将会导致编译错误。
- 简洁性:Lambda表达式旨在简化代码,通常用于简短的操作。复杂的逻辑应该考虑使用传统方法或分离成多个步骤。
- 访问外部变量:Lambda表达式可以访问其所在范围内的最终变量和有效final变量。这意味着外部变量在Lambda中要么是不可变的,要么在Lambda创建前已经赋值且不再改变。
- 捕获变量:在Java 8中,Lambda表达式通过值捕获外部变量,而在Java 9及以上版本中,也可以通过var关键字声明局部变量,但同样受到不可变性的限制。
- 并行处理:当Lambda表达式用于并行流(例如parallelStream())时,需要特别注意变量的并发访问问题,避免数据竞争和一致性问题。
- 异常处理:Lambda表达式中可以抛出和处理异常,但需要注意异常传播的规则,特别是在函数式接口方法声明了受检异常时。
- 可序列化:如果Lambda表达式所实现的函数式接口是可序列化的(如Serializable),那么Lambda表达式本身也必须是可序列化的。这可能影响到捕获的外部变量是否可序列化。