Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些

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 数组 / 对象的字段(它们不是栈上的局部变量):

java 复制代码
int[] 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>。


七、总结

六件事记住就够了:

  1. 是什么 ------ 匿名函数的简写,只能用在函数式接口上。
  2. 怎么写 ------ () -> ...;单行省 return,多行要写大括号。
  3. 四大接口 ------ Function(转换)、Consumer(消费)、Supplier(供给)、Predicate(判断)。
  4. 方法引用 ------ 类::方法 / 对象::方法 / 类::new,s -> System.out.println(s) 可换成 System.out::println。
  5. 配合 Stream ------ filter / map / sorted / groupingBy / toList 是最常用的五个;别忘了终止操作。
  6. 一个限制 ------ 引用的外部变量必须是 effectively final(没被改过)。

一句话:

Lambda 的价值是把行为变成参数 ------这也是 Stream 能做到 filter(u -> u.age() > 18) 这种声明式写法的根本原因。


如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!如果疑问或补充,欢迎在评论区交流~

关注我,后续分享更多 Java 核心知识点和实战技巧!

相关推荐
码农小河6633 分钟前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql
IT_陈寒1 小时前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
系统集成-鹰翔1 小时前
ERP图片改名后仍命中识别缓存:缓存键、十分钟到期与调用计数
javascript·经验分享·后端
用户8356290780511 小时前
使用 Python 在 Excel 中添加和自定义迷你图
后端·python
码事漫谈1 小时前
给AI看一颗❤️,它就开始写递归了
后端
用户8356290780511 小时前
使用 Python 在 Excel 图表中添加趋势线与回归方程
后端·python
你三大爷2 小时前
Netty / Kafka / Redis 保序问题的源码对比与 NIO 框架主流解法
后端·架构
SL_staff2 小时前
从配置失效到逻辑复用:JVS-Logic 如何构建可治理的业务编排中枢
java·spring·产品
特立独行的猫A2 小时前
一份 Markdown 发十个平台:cspub 一稿多投
java·后端·python