一、为什么需要函数式编程
在传统面向对象编程中,我们习惯于描述"东西是什么":抽象数据
// OO:关注数据和对象
class Dog {
String name;
void bark() {}
}
而函数式编程关注的是"做什么动作":抽象动作
// FP:关注行为
list.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.forEach(System.out::println);
OO(面向对象) → 抽象数据
FP(函数式编程)→ 抽象行为
函数式编程还解决了并发编程中最棘手的问题之一:可变共享状态。
多个线程同时修改同一块内存 → 谁赢了?没人知道
函数式的解决方案:
函数永远不修改现有值
只生成新值返回
不对内存产生争用
天然线程安全
二、Lambda 表达式
什么是最小可能语法
Lambda 的核心思想就是能省的全省,只保留最核心的两件事:
1. 参数是什么
2. 拿参数做什么
对比普通方法和 Lambda:
// 普通方法:什么都要写
public int add(int a, int b) {
return a + b;
}
// Lambda:只留核心
(a, b) -> a + b
省略了什么:
访问修饰符 ← 编译器推断
返回值类型 ← 编译器推断
方法名 ← 不需要,不会复用
参数类型 ← 编译器推断
大括号+return ← 单行时省略
Lambda 的省略规律
编译器会帮我们推断s是什么类型
// 第一步:完整写法
(String s) -> {
return s.toUpperCase();
};
// 第二步:省略参数类型
(s) -> {
return s.toUpperCase();
};
// 第三步:单参数省略括号
s -> {
return s.toUpperCase();
};
// 第四步:单行省略大括号和return
s -> s.toUpperCase();
// 第五步:只是调用现有方法,换成方法引用
String::toUpperCase;
三、四大核心函数式接口
学会这四个,Lambda 才算真正入门。
1. Consumer 消费者
特征:有参数,无返回值
记忆:消费者只花钱不挣钱
Consumer<String> c = s -> System.out.println(s);
c.accept("hello"); // 打印 hello
// 最常见场景
list.forEach(s -> System.out.println(s));
list.forEach(System.out::println); // 方法引用简化
2. Supplier 供应者
特征:无参数,有返回值
记忆:供应商只产出不接收
Supplier<String> s = () -> "hello";
String result = s.get(); // 返回 hello
// 常见场景:懒加载
Supplier<List<String>> supplier = () -> new ArrayList<>();
// 需要的时候才创建,不是立刻创建
3. Function 函数
特征:有参数,有返回值
记忆:有输入有输出,负责转换数据
Function<String, Integer> f = s -> s.length();
Integer result = f.apply("hello"); // 返回 5
// 常见场景:数据转换
list.stream().map(s -> s.length());
list.stream().map(String::length); // 方法引用简化
4. Predicate 断言
特征:有参数,返回boolean
记忆:专门用来判断条件
Predicate<String> p = s -> s.length() > 3;
boolean result = p.test("hello"); // 返回 true
// 常见场景:过滤数据
list.stream().filter(s -> s.length() > 3);
四个接口对比总结
接口 参数 返回值 方法名
─────────────────────────────────────
Consumer 有 无 accept()
Supplier 无 有 get()
Function 有 有(任意) apply()
Predicate 有 boolean test()
如何选择接口
看方法的参数和返回值,对号入座:
有参数 无返回值 → Consumer
无参数 有返回值 → Supplier
有参数 有返回值 → Function
有参数 返回boolean → Predicate
无参数 无返回值 → Runnable
两个参数 → Bi开头的版本
BiConsumer
BiFunction
BiPredicate
实际例子:
static void sendMsg(String msg) { } // 有进无出 → Consumer
static String getName() { } // 无进有出 → Supplier
static Integer strToInt(String s) { } // 有进有出 → Function
static boolean isVip(String userId) { } // 有进返回判断 → Predicate
// 对应写法
Consumer<String> c = YourClass::sendMsg;
Supplier<String> s = YourClass::getName;
Function<String, Integer> f = YourClass::strToInt;
Predicate<String> p = YourClass::isVip;
Lambda 是一个函数式接口的匿名实现
// 你写的
Consumer<String> c = s -> System.out.println(s);
// 编译器理解的
Consumer<String> c = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
// Lambda本质是:
// 实现了函数式接口的一个匿名对象
普通方法:
有名字
挂在类上
不能单独传递
匿名内部类:
没有名字
是一个对象
可以传递[多态]
但写法啰嗦
Lambda:
没有名字
是一个对象
可以传递
写法极度简洁
Lambda 不只是没有名字的方法,而是一个实现了函数式接口的匿名对象,只不过编译器帮你把所有废话都省掉了,看起来像一个方法,但本质是个对象
四、方法引用
是什么
方法引用 = 把一个已经存在的方法直接当作Lambda来用
不用重新写逻辑,直接用 :: 指向已有方法
// Lambda:重新写了逻辑
list.forEach(s -> System.out.println(s));
// 方法引用:直接指向已有方法
list.forEach(System.out::println);
四种方法引用
1. 静态方法引用:类名::静态方法
// Lambda
list.stream().map(s -> Integer.parseInt(s));
// 方法引用
list.stream().map(Integer::parseInt);
2. 绑定方法引用:对象::普通方法
String prefix = "hello_";
// Lambda
list.stream().map(s -> prefix.concat(s));
// 方法引用(对象已经确定)
list.stream().map(prefix::concat);
3. 未绑定方法引用:类名::普通方法
// Lambda
list.stream().map(s -> s.toUpperCase());
// ↑ s是流里的每个元素,调用时才知道是谁
// 方法引用
list.stream().map(String::toUpperCase);
// ↑ 流里每来一个元素,那个元素就成为调用对象
未绑定 = 对象还不知道是谁
流里每来一个元素才完成绑定
4. 构造方法引用:类名::new
// Lambda
list.stream().map(s -> new StringBuilder(s));
// 方法引用
list.stream().map(StringBuilder::new);
四种对比
类型 语法 对象在哪里
────────────────────────────────────────────
静态方法引用 类名::静态方法 不需要对象
绑定方法引用 对象::普通方法 对象已经确定
未绑定方法引用 类名::普通方法 调用时传入
构造方法引用 类名::new new时创建
能用方法引用的条件
方法的参数和返回值要和函数式接口匹配
// Consumer:无参无返回值
void println(String s) 匹配
// Function<String,Integer>:接收String返回Integer
Integer parseInt(String s) 匹配
String toUpperCase() 不匹配
五、闭包
什么是闭包
Lambda 使用了函数作用域之外的变量
这个 Lambda 就形成了闭包
通俗说:Lambda 把外部变量"包"进去了
离开了原来的方法,外部变量依然活着
public class Closure1 {
int i; // 成员变量
IntSupplier makeFun(int x) {
return () -> x + i++;
// ↑ ↑
// x i
// 方法参数 成员变量
// Lambda把这两个外部变量都包进去了
}
}
成员变量 vs 局部变量
public class Closure1 {
int i; // 成员变量,在堆上
IntSupplier makeFun(int x) {
int local = 10; // 局部变量,在栈上
return () -> x + i++; // i可以改,local不行
}
}
为什么局部变量不能改:
局部变量在栈上
方法执行完,栈帧销毁,局部变量就没了
Lambda可能在方法结束后还被调用
所以Java把局部变量复制了一份给Lambda
如果外面改了局部变量:
外面的值变了
Lambda里的副本没变
两个值不一致,产生混乱
所以Java规定:局部变量不能被修改
为什么成员变量可以改:
成员变量在堆上
Lambda通过对象引用访问
不需要复制副本
改了就是改了,没有副本问题
所以随便改
成员变量(堆上) → Lambda里随便改
局部变量(栈上) → Lambda里不能改
等同 final 效果
// 明确写final
IntSupplier makeFun(final int x) {
final int local = 10;
return () -> x + local;
}
// 等同final效果:没写final,但从来没改过
IntSupplier makeFun(int x) {
int local = 10;
// local 和 x 从来没被重新赋值
// Java认为它们等同于final
return () -> x + local; // 可以用
}
// 改了就报错
IntSupplier makeFun(int x) {
int local = 10;
local = 20; // 改了!
return () -> x + local; // 编译报错
}
多个Lambda共享成员变量
Closure1 c = new Closure1();
c.i = 0;
IntSupplier f1 = c.makeFun(10); // x=10
IntSupplier f2 = c.makeFun(20); // x=20
// f1和f2的x不同,但共享同一个i
System.out.println(f1.getAsInt()); // 10+0=10,i变成1
System.out.println(f2.getAsInt()); // 20+1=21,i变成2
System.out.println(f1.getAsInt()); // 10+2=12,i变成3
// f1改了i,f2看到的i也变了
// 因为指向的是同一个对象的同一个i
六、串起来用
List<String> names = List.of("张三", "李四", "王五丰");
names.stream()
.filter(s -> s.length() > 2) // Predicate:筛选
.map(s -> s + "先生") // Function:转换
.forEach(System.out::println); // Consumer:消费
// 输出:王五丰先生
总结
Lambda → 用最少的语法写函数逻辑
方法引用 → 已有方法直接当Lambda用,更简洁
四大接口 → Consumer/Supplier/Function/Predicate
看签名选接口,签名决定一切
闭包 → Lambda捕获外部变量
成员变量随便改
局部变量必须等同final
函数式编程的核心思想:函数只接收数据,返回新数据,不修改任何外部状态。 代码更可预测,bug 更好找,天然线程安全。