文章目录
-
- Lambda表达式的语法本质与核心概念
- Lambda表达式的用法
- 高级特性
Lambda表达式的语法本质与核心概念
什么是Lambda表达式
Lambda表达式本质上是一个匿名函数,它没有名称,但有参数列表、函数主体、返回类型,以及可能抛出的异常列表。它可以作为参数传递给方法,或者存储在变量中。
Lambda表达式的基本语法格式为:
java
(parameters) -> expression
// 或者
(parameters) -> { statements; }
其中:
parameters:参数列表,可以为空,也可以包含一个或多个参数->:Lambda操作符,也称为箭头操作符expression:单个表达式,其值将作为Lambda表达式的返回值{ statements; }:代码块,可以包含多条语句,需要使用return语句返回值(如果有返回值)
函数式接口:Lambda表达式的类型载体
Lambda表达式本身没有类型,它的类型由上下文推断得出。在Java中,Lambda表达式的类型必须是一个函数式接口。
函数式接口是指只包含一个抽象方法的接口。例如:
java
@FunctionalInterface
public interface Runnable {
void run();
}
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
@FunctionalInterface注解是可选的,但强烈建议使用。它会告诉编译器该接口是一个函数式接口,如果接口中包含多个抽象方法,编译器会报错。
Java 8在java.util.function包中提供了大量预定义的函数式接口,涵盖了大多数常见的函数类型:
| 接口名称 | 方法签名 | 用途 |
|---|---|---|
Predicate<T> |
boolean test(T t) |
表示一个布尔值判断 |
Function<T, R> |
R apply(T t) |
表示一个函数,接受一个参数并返回一个结果 |
Consumer<T> |
void accept(T t) |
表示一个消费操作,接受一个参数但不返回结果 |
Supplier<T> |
T get() |
表示一个供应操作,不接受参数但返回一个结果 |
UnaryOperator<T> |
T apply(T t) |
表示一个一元操作,接受一个参数并返回同类型的结果 |
BinaryOperator<T> |
T apply(T t1, T t2) |
表示一个二元操作,接受两个同类型参数并返回同类型的结果 |
Lambda表达式的用法
基本语法形式
Lambda表达式的语法非常灵活,可以根据上下文进行简化:
- 无参数,无返回值
java
// 完整形式
Runnable r1 = () -> { System.out.println("Hello Lambda!"); };
// 简化形式:当代码块只有一条语句时,可以省略大括号
Runnable r2 = () -> System.out.println("Hello Lambda!");
- 一个参数,无返回值
java
// 完整形式
Consumer<String> c1 = (String s) -> { System.out.println(s); };
// 简化形式1:省略参数类型(编译器可以根据上下文推断)
Consumer<String> c2 = (s) -> System.out.println(s);
// 简化形式2:当只有一个参数时,可以省略参数括号
Consumer<String> c3 = s -> System.out.println(s);
- 多个参数,有返回值
java
// 完整形式
Comparator<Integer> cmp1 = (Integer a, Integer b) -> { return a.compareTo(b); };
// 简化形式1:省略参数类型
Comparator<Integer> cmp2 = (a, b) -> { return a.compareTo(b); };
// 简化形式2:当代码块只有一条return语句时,可以省略大括号和return关键字
Comparator<Integer> cmp3 = (a, b) -> a.compareTo(b);
变量捕获
Lambda表达式可以访问其外部作用域中的变量,这被称为变量捕获。
捕获局部变量
Lambda表达式可以捕获外部方法中的局部变量,但这些变量必须是final 或者effectively final的(即变量在初始化后没有被重新赋值)。
java
public void testVariableCapture() {
int x = 10; // effectively final
Runnable r = () -> System.out.println(x); // 正确
int y = 20;
y = 30; // y不再是effectively final
// Runnable r2 = () -> System.out.println(y); // 编译错误
}
这一限制的原因是:Lambda表达式可能会在方法返回后仍然执行(例如,在另一个线程中),如果局部变量可以被修改,就会导致不可预测的结果。
捕获实例变量和静态变量
Lambda表达式可以自由地访问实例变量和静态变量,没有final限制:
java
public class LambdaTest {
private int instanceVar = 10;
private static int staticVar = 20;
public void test() {
Runnable r1 = () -> System.out.println(instanceVar); // 正确
Runnable r2 = () -> System.out.println(staticVar); // 正确
instanceVar = 30;
staticVar = 40;
r1.run(); // 输出30
r2.run(); // 输出40
}
}
方法引用
方法引用是Lambda表达式的一种简化形式,当Lambda表达式的主体只是调用一个已经存在的方法时,可以使用方法引用。
方法引用有四种形式:
- 静态方法引用 :
ClassName::staticMethod
java
// Lambda表达式
Function<String, Integer> f1 = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> f2 = Integer::parseInt;
- 实例方法引用(对象::实例方法) :
object::instanceMethod
java
String str = "Hello";
// Lambda表达式
Supplier<Integer> s1 = () -> str.length();
// 方法引用
Supplier<Integer> s2 = str::length;
- 实例方法引用(类::实例方法) :
ClassName::instanceMethod
java
// Lambda表达式
Function<String, Integer> f1 = s -> s.length();
// 方法引用
Function<String, Integer> f2 = String::length;
这种形式看起来有些特殊,它实际上是将第一个参数作为方法的调用者。例如,String::length等价于(String s) -> s.length()。
- 构造器引用 :
ClassName::new
java
// Lambda表达式
Supplier<List<String>> s1 = () -> new ArrayList<>();
// 构造器引用
Supplier<List<String>> s2 = ArrayList::new;
与Stream API结合
Lambda表达式最强大的应用之一是与Stream API结合,用于处理集合数据。Stream API提供了一套声明式的数据处理方式,允许开发者以流水线的方式对集合进行过滤、映射、排序、聚合等操作。
例如,从一个员工列表中筛选出年龄大于30岁的员工,按工资排序,然后提取他们的姓名:
java
List<Employee> employees = ...;
List<String> names = employees.stream()
.filter(e -> e.getAge() > 30)
.sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
.map(Employee::getName)
.collect(Collectors.toList());
这段代码比传统的for循环更加简洁、易读,并且可以轻松地转换为并行流来提高性能:
java
List<String> names = employees.parallelStream()
.filter(e -> e.getAge() > 30)
.sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
.map(Employee::getName)
.collect(Collectors.toList());
高级特性
复合Lambda表达式
许多函数式接口都提供了默认方法,可以用来复合Lambda表达式,构建更复杂的逻辑。
例如,Predicate接口提供了and()、or()、negate()方法:
java
Predicate<Employee> agePredicate = e -> e.getAge() > 30;
Predicate<Employee> salaryPredicate = e -> e.getSalary() > 50000;
// 年龄大于30且工资大于50000
Predicate<Employee> andPredicate = agePredicate.and(salaryPredicate);
// 年龄大于30或工资大于50000
Predicate<Employee> orPredicate = agePredicate.or(salaryPredicate);
// 年龄不大于30
Predicate<Employee> negatePredicate = agePredicate.negate();
Function接口提供了andThen()和compose()方法:
java
Function<Integer, Integer> add1 = x -> x + 1;
Function<Integer, Integer> multiply2 = x -> x * 2;
// 先加1,再乘2:(x + 1) * 2
Function<Integer, Integer> andThen = add1.andThen(multiply2);
// 先乘2,再加1:(x * 2) + 1
Function<Integer, Integer> compose = add1.compose(multiply2);
性能考量
虽然Lambda表达式使代码更加简洁,但在使用时也需要考虑性能问题:
-
Lambda表达式的创建成本:Lambda表达式的创建成本通常很低,但在性能敏感的场景中,频繁创建Lambda表达式可能会带来一定的开销。在这种情况下,可以将Lambda表达式缓存起来,重复使用。
-
自动装箱与拆箱 :当使用基本类型作为Lambda表达式的参数或返回值时,会发生自动装箱和拆箱操作,这会带来性能开销。Java 8提供了专门的基本类型函数式接口,如
IntPredicate、IntFunction、IntConsumer等,可以避免自动装箱和拆箱。
java
// 会发生自动装箱和拆箱
Predicate<Integer> p1 = i -> i > 10;
// 避免自动装箱和拆箱,性能更好
IntPredicate p2 = i -> i > 10;
- 并行流的使用:并行流可以利用多核CPU提高处理速度,但并不是所有场景都适合使用并行流。并行流有一定的启动开销,当数据量较小时,并行流的性能可能不如串行流。此外,并行流中的操作必须是线程安全的。
注意情况
-
过度使用Lambda表达式:虽然Lambda表达式很简洁,但过度使用会使代码变得难以理解。当Lambda表达式的逻辑比较复杂时,应该将其提取为一个命名方法,使用方法引用。
-
忽略异常处理:Lambda表达式不能抛出受检异常,如果Lambda表达式中可能抛出受检异常,必须在Lambda表达式内部进行捕获处理,或者使用能够抛出受检异常的函数式接口。
-
变量捕获的误解:Lambda表达式捕获的是变量的值,而不是变量的引用。当捕获的变量是effectively final时,这一点很容易被忽略。
-
并行流的线程安全问题:并行流使用的是Fork/Join线程池,其中的操作可能会在多个线程中同时执行。如果操作修改了共享状态,就会导致线程安全问题。
我的理解
-
保持Lambda表达式简短:Lambda表达式应该保持简短,通常不超过3行代码。如果逻辑比较复杂,应该提取为命名方法。
-
使用方法引用:当Lambda表达式的主体只是调用一个已经存在的方法时,优先使用方法引用,这样可以使代码更加清晰。
-
使用基本类型函数式接口:在处理基本类型时,优先使用基本类型函数式接口,避免自动装箱和拆箱的性能开销。
-
谨慎使用并行流:只有在数据量较大且操作是CPU密集型时,才考虑使用并行流。在使用并行流之前,应该进行性能测试,确保它确实能够提高性能。
-
为函数式接口添加文档:当自定义函数式接口时,应该为其添加详细的文档,说明该接口的用途、参数含义和返回值含义。