Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
📌 先说结论
Lambda 表达式就是一个匿名函数的简写。
java
// 以前:要写一个完整的匿名内部类
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("干活");
}
};
// 现在:一行搞定
Runnable r = () -> System.out.println("干活");
它只能用在"函数式接口"上 ------就是只有一个抽象方法的接口。
原理一句话说完 :Java 8 之后,Lambda 编译后不再生成独立的匿名类,而是用 invokedynamic 指令在运行时动态生成。你不需要懂这个,会用就行。
一、Lambda 的写法(5 种形式)
1.1 五种写法一次看懂
java
// ① 无参数
Supplier<String> s = () -> "hello";
// ② 一个参数(括号可以省略)
Function<String, Integer> len = str -> str.length();
// ③ 多个参数
BinaryOperator<Integer> add = (a, b) -> a + b;
// ④ 带参数类型(一般省略,编译器能推断)
BiFunction<Integer, Integer, Integer> mul = (Integer a, Integer b) -> a * b;
// ⑤ 代码块(多条语句要用大括号 + return)
Function<Integer, String> block = n -> {
if (n > 10) return "大";
return "小";
};
实测:
无参: hello
单参: 5
多参: 7
带类型: 12
代码块: 大 / 小
1.2 语法速记
| 情况 | 写法 |
|---|---|
| 无参数 | () -> ... |
| 1 个参数 | x -> ...(括号可省) |
| 2 个以上参数 | (a, b) -> ...(括号必须有) |
| 单行表达式 | x -> x + 1(不用 return) |
| 多行代码 | x -> { ...; return ...; }(必须 return) |
💡 记住一个规则就够了 :只有一行 时,大括号和
return都能省;多行 时必须写大括号,有返回值就要写return。
二、函数式接口:Lambda 能用的前提
Lambda 不能随便赋给任何类型,只能是"函数式接口" ------有且仅有一个抽象方法的接口。
2.1 JDK 内置的四大接口(必须记住)
| 接口 | 签名 | 干什么 | 例子 |
|---|---|---|---|
Function<T,R> |
R apply(T) |
转换 T→R | s -> s.length() |
Consumer<T> |
void accept(T) |
消费,只进不出 | s -> System.out.println(s) |
Supplier<T> |
T get() |
供给,只出不进 | () -> "hello" |
Predicate<T> |
boolean test(T) |
判断,返回布尔 | n -> n > 0 |
按"有没有进、有没有出"记:
Function : 有进有出(转换)
Consumer : 有进没出(消费)
Supplier : 没进有出(供给)
Predicate: 有进有布尔(判断)
2.2 常用变体
| 接口 | 说明 |
|---|---|
BiFunction<T,U,R> |
两个参数的 Function |
UnaryOperator<T> |
T → T(同类型转换) |
BinaryOperator<T> |
(T,T) → T(两个同类型,返回同类型) |
IntFunction<R> / ToIntFunction<T> |
避免装箱的原始类型版本 |
BiConsumer<T,U> |
两个参数的 Consumer |
💡 为什么要有
IntFunction这些? 因为泛型不能是基本类型,Function<Integer,Integer>每次调用都要装箱拆箱。特化版直接操作int,没开销。这也是 Stream 里mapToInt存在的原因。
三、方法引用:Lambda 的进一步简写
当 Lambda 体只是"调用一个已有方法"时,可以再简化成 ::。
3.1 四种形式
java
// ① 静态方法引用:类::静态方法
Function<String, Integer> parse = Integer::parseInt;
// 等价于 s -> Integer.parseInt(s)
// ② 绑定对象的实例方法:对象::方法
String prefix = "用户-";
Function<String, String> concat = prefix::concat;
// 等价于 s -> prefix.concat(s)
// ③ 类::实例方法(第一个参数作为调用者)
Function<String, String> upper = String::toUpperCase;
// 等价于 s -> s.toUpperCase()
// ④ 构造器引用:类::new
Supplier<List<String>> listFactory = ArrayList::new;
// 等价于 () -> new ArrayList<>()
实测:
静态: Integer::parseInt("100") = 100
绑定对象: prefix::concat("A") = 用户-A
类::实例: String::toUpperCase("abc") = ABC
构造器: ArrayList::new 创建了 list, size=0
3.2 什么时候用方法引用
判断方法:Lambda 体是不是"直接把参数传给某个方法"?是的话就能换。
java
// ❌ 能用方法引用却写了 lambda
names.forEach(n -> System.out.println(n));
// ✅ 用方法引用
names.forEach(System.out::println);
| 写法 | 等价 |
|---|---|
s -> System.out.println(s) |
System.out::println |
s -> s.length() |
String::length |
() -> new User() |
User::new |
u -> u.getName() |
User::getName |
💡 不用强求 。方法引用只是更简洁,lambda 写法同样正确,团队里统一就行。
四、Stream 常用操作(Lambda 的主战场)
Lambda 最有价值的地方就是配合 Stream 处理集合。
4.1 准备数据
java
record User(String name, int age, String city) {}
List<User> users = List.of(
new User("张三", 28, "北京"),
new User("李四", 22, "上海"),
new User("王五", 35, "北京"),
new User("赵六", 19, "深圳")
);
4.2 八个最常用的操作
java
// ① filter:过滤
List<User> adults = users.stream()
.filter(u -> u.age() >= 25)
.toList();
// ② map:转换(取某个字段)
List<String> names = users.stream()
.map(User::name)
.toList();
// ③ sorted:排序
List<User> byAge = users.stream()
.sorted(Comparator.comparingInt(User::age))
.toList();
// ④ 统计:sum / max / min / average
int totalAge = users.stream().mapToInt(User::age).sum();
// ⑤ groupingBy:分组(相当于 SQL 的 GROUP BY)
Map<String, List<User>> byCity = users.stream()
.collect(Collectors.groupingBy(User::city));
// ⑥ distinct:去重
long cityCount = users.stream()
.map(User::city)
.distinct()
.count();
// ⑦ anyMatch / allMatch:判断
boolean hasYoung = users.stream().anyMatch(u -> u.age() < 20);
// ⑧ collect:收集成其他结构
Map<String, Integer> nameToAge = users.stream()
.collect(Collectors.toMap(User::name, User::age));
实测输出:
filter+map: 北京的用户 = [张三, 王五]
sorted(按年龄): [赵六, 李四, 张三, 王五]
sum=104, average=26.00
groupingBy: {上海=[李四], 北京=[张三, 王五], 深圳=[赵六]}
distinct: 不同城市数 = 3
4.3 Stream 的三个坑
坑一:没有终止操作,什么都不会执行
java
// ❌ 不打印任何东西!因为缺终止操作
users.stream().filter(u -> {
System.out.println("过滤 " + u.name());
return true;
});
// ✅ 加上 toList() / count() / forEach() 才会执行
users.stream().filter(...).toList();
Stream 是惰性的 :filter、map 这些只是"记下要做什么",必须有终止操作(toList、count、forEach、sum)才真正跑。
坑二:Collectors.toMap 遇到重复 key 会抛异常
java
// ❌ key 重复 → IllegalStateException: Duplicate key
.collect(Collectors.toMap(User::city, User::name));
// ✅ 提供冲突策略
.collect(Collectors.toMap(User::city, User::name, (oldV, newV) -> newV));
坑三:parallelStream() 不是万能加速
java
// ⚠️ 数据量小、有共享状态、有 IO 时别用
users.parallelStream().filter(...)
什么时候能用并行流 :数据量大(上万条)+ 纯计算 + 无共享状态。其他情况用了反而更慢。
五、一个必须知道的小坑:变量捕获
Lambda 里能用外部变量,但那个变量必须是 effectively final(事实不变)。
5.1 什么算 effectively final
java
int base = 100; // 声明后没改过 → 可以
IntUnaryOperator f = x -> x + base;
System.out.println(f.applyAsInt(5)); // 105
实测:
捕获 base=100,f(5)=105
但如果改了就不行:
java
int count = 0;
count++; // ❌ 改了
Runnable r = () -> System.out.println(count); // 编译报错!
报错信息 :Local variable count defined in an enclosing scope must be final or effectively final
5.2 为什么有这个限制
因为 Lambda 捕获的是变量的"值副本",不是变量本身。局部变量在方法结束后就没了,而 Lambda 可能在之后才执行,所以必须在创建时拷贝一份。
如果允许外部改,就会出现"副本和本体不一致"的混乱------所以 Java 直接禁止。
💡 解决办法 :要么别改,要么用
final数组 / 对象的字段(它们不是栈上的局部变量):
javaint[] counter = {0}; // 数组元素可以改 Runnable r = () -> counter[0]++;
六、面试常问的 6 个问题
1. Lambda 表达式是什么?
答:匿名函数的简写 ,用来实现函数式接口 。核心作用是把"行为"当参数传递 ,代码更简洁。原理上,Java 8 用 invokedynamic 在运行时动态生成实现,不产生独立的匿名类文件。
2. 什么是函数式接口?
答:有且仅有一个抽象方法 的接口,用 @FunctionalInterface 标注(该注解只做编译期检查)。default 方法、static 方法、Object 的 public 方法都不计入抽象方法数量。JDK 四大核心接口:Function / Consumer / Supplier / Predicate。
3. 方法引用有几种形式?
答:四种------① 静态方法 Integer::parseInt;② 绑定对象的实例方法 prefix::concat;③ 类的实例方法 String::toUpperCase(第一个参数作为调用者);④ 构造器 ArrayList::new。
4. Lambda 里的 this 指向什么?
答:指向外部类实例 (因为 Lambda 不引入新作用域,函数体逻辑上"长在"外部类里)。和匿名内部类不同 ------匿名内部类的 this 指向匿名类自己。
5. 为什么 Lambda 引用的外部变量必须是 effectively final?
答:因为 Lambda 捕获的是变量的值副本(不是变量本身)。局部变量存在栈上,方法返回后就销毁了,而 Lambda 可能之后才执行,所以必须创建时拷贝。若允许外部修改,副本和本体就会不一致,因此编译期强制要求"声明后未被修改过"。
6. Stream 的 map 和 flatMap 有什么区别?
答:map 是一对一转换 (Stream<T> → Stream<R>);flatMap 是一对多后展平 (Stream<T> → Stream<Stream<R>> → Stream<R>)。比如一篇文章拆成多个单词:map 会得到 List<List<String>>,flatMap 得到 List<String>。
七、总结
六件事记住就够了:
- 是什么 ------ 匿名函数的简写,只能用在函数式接口上。
- 怎么写 ------
() -> ...;单行省return,多行要写大括号。 - 四大接口 ------
Function(转换)、Consumer(消费)、Supplier(供给)、Predicate(判断)。 - 方法引用 ------
类::方法/对象::方法/类::new,s -> System.out.println(s)可换成System.out::println。 - 配合 Stream ------
filter/map/sorted/groupingBy/toList是最常用的五个;别忘了终止操作。 - 一个限制 ------ 引用的外部变量必须是 effectively final(没被改过)。
一句话:
Lambda 的价值是把行为变成参数 ------这也是 Stream 能做到
filter(u -> u.age() > 18)这种声明式写法的根本原因。
如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!如果疑问或补充,欢迎在评论区交流~
关注我,后续分享更多 Java 核心知识点和实战技巧!