Flink学习的核心在于掌握Java基础:函数式接口(Lambda)用于无状态算子,RichFunction与RuntimeContext处理有状态逻辑,泛型擦除通过TypeInformation/TypeHint补偿,POJO规则保障序列化性能,状态管理依赖ValueState等。
理解JVM机制、类加载、并发安全及Maven依赖隔离是工程落地关键。
Flink算子本质是分布式数据流上的组件,其设计源于Java运行时特性。
掌握"函数→算子→子任务"映射关系,结合前端类比(如useState、useEffect),可将复杂问题转化为可解释的Java契约。
Flink 学习需要具备的 Java 基础知识总结
Flink 的很多"难",其实是 Java 的"难";
Flink 的很多"设计",其实是 Java 运行时机制的延伸。
把 Flink 当成"用 Java 写的、运行在 JVM 上的、有状态的分布式流处理框架",很多问题会从"玄学"变成"可解释"。
一、函数式接口与 Lambda:Flink 算子的入口
Flink 的 map、filter、flatMap、keyBy、reduce、aggregate 大量使用 Java 的 SAM 接口(只有一个抽象方法的接口)。
| Flink API | Java 机制 | 前端类比 |
|---|---|---|
MapFunction<T,O> |
函数式接口,可用 Lambda | arr.map(x => ...) |
FilterFunction<T> |
函数式接口 | arr.filter(x => ...) |
FlatMapFunction<T,O> |
函数式接口,可输出 0~N 条 | arr.flatMap(x => ...) |
KeySelector<T,K> |
函数式接口,提取 key | groupBy 的 key 函数 |
ReduceFunction<T> |
函数式接口,两两聚合 | arr.reduce |
关键分水岭:
-
无状态、无生命周期 → 用 Lambda 写
map/filter/flatMap。 -
有状态、需要
open/close、需要RuntimeContext→ 必须继承RichMapFunction、RichFlatMapFunction等抽象类,不能用 Lambda。
这就是为什么你写清洗时很顺,但一写状态就突然变复杂。
二、泛型与类型信息:Flink 的"运行时类型系统"
Java 泛型会擦除 ,但 Flink 需要知道运行时类型来做序列化、网络传输、状态存储。
所以 Flink 搞了一套 TypeInformation / TypeHint。
| Flink 概念 | Java 机制 | 前端类比 |
|---|---|---|
TypeInformation<T> |
泛型擦除后的补偿 | TS 类型 + JSON Schema |
TypeHint<T> |
匿名子类保留泛型 | 类型声明 |
returns(...) |
显式告诉 Flink 输出类型 | 类型断言 |
Tuple2<String,Integer> |
Flink 自带元组 | TS 元组 |
典型坑:
java
java
DataStream<MyPojo> stream = env.fromElements(...);
// 如果不显式 returns,Flink 可能推断成 GenericType,退化成 Kryo
stream.returns(new TypeHint<MyPojo>() {});
记住 :Flink 的 TypeInformation 就是 Java 泛型擦除的"运行时补丁"。
三、POJO 与序列化:Flink 性能的隐形开关
Flink 对 POJO 有明确规则。
符合规则,用高效的 PojoSerializer;
不符合,退化成 KryoSerializer,性能差、状态大、易报错。
Flink POJO 规则:
-
类是
public。 -
有
public无参构造。 -
字段是
public,或有publicgetter/setter。 -
字段类型可序列化。
所以在银行场景写 Transaction 时:
java
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Transaction {
private String txId;
private BigDecimal amount;
private String riskLevel;
}
-
@Data:生成 getter/setter。 -
@NoArgsConstructor:必须加!否则 Flink 反射创建对象失败。 -
BigDecimal可序列化,但状态里频繁用要注意性能。
前端类比:TS 类型只存在于编译期,Flink 需要"运行时 schema",POJO 规则就是 schema 契约。
四、RichFunction 与 RuntimeContext:Flink 的"组件生命周期"
RichMapFunction、RichFlatMapFunction、RichSinkFunction 等继承自 AbstractRichFunction,有:
-
open(...):初始化,类似 ReactuseEffect挂载。 -
close():清理,类似卸载。 -
getRuntimeContext():获取状态、累加器、指标、并行度、任务名。
java
java
public class MyRichMap extends RichMapFunction<String, String> {
private transient SimpleDateFormat sdf;
@Override
public void open(Configuration parameters) {
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
@Override
public String map(String value) {
return sdf.format(new Date());
}
}
关键点:
-
SimpleDateFormat不可序列化,必须transient,并在open中初始化。 -
否则任务分发到 TaskManager 时直接
NotSerializableException。 -
RuntimeContext就像前端的Context,但它是 Flink 注入的运行时能力入口。
五、状态、定时器与 Checkpoint:Java 对象 + 框架托管
Flink 的 ValueState、ListState、MapState 都是泛型接口,背后是 Managed State。
| Flink 状态 API | Java 机制 | 前端类比 |
|---|---|---|
ValueState<T> |
泛型 + TypeSerializer | useState,但持久化 |
ListState<T> |
泛型 + 状态快照 | Redux list |
MapState<K,V> |
泛型 + 分布式状态 | Map/Redux |
KeyedProcessFunction |
抽象类 + Context + TimerService |
事件处理 + 定时器 |
CheckpointedFunction |
接口 + 生命周期 | 快照/回滚 |
在银行做"5 分钟累计交易金额"时,核心就是:
java
java
public class CardWindowFunction extends KeyedProcessFunction<String, Transaction, Alert> {
private transient ValueState<Double> sumState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Double> desc = new ValueStateDescriptor<>("sum", Double.class);
sumState = getRuntimeContext().getState(desc);
}
@Override
public void processElement(Transaction tx, Context ctx, Collector<Alert> out) throws Exception {
Double sum = sumState.value();
if (sum == null) sum = 0.0;
sumState.update(sum + tx.getAmount().doubleValue());
ctx.timerService().registerProcessingTimeTimer(
ctx.timerService().currentProcessingTime() + 300_000
);
}
}
Java:
-
泛型:
ValueState<Double>。 -
异常:
processElement可抛Exception。 -
生命周期:
open中获取状态句柄。 -
序列化:状态里的对象必须可序列化。
-
判空:状态第一次取出来是
null。
六、并发、异步与线程安全:JVM 多线程在 Flink 里的体现
Flink 一个算子子任务通常在一个线程中执行,但 TaskManager 是多线程 JVM。
所以:
-
不要跨算子共享可变 Java 对象。
-
SimpleDateFormat、Random等线程不安全对象,要在open中每子任务初始化。 -
异步 I/O 用
AsyncFunction+CompletableFuture,类似前端Promise/async-await。
| Flink 概念 | Java 机制 | 前端类比 |
|---|---|---|
AsyncFunction |
CompletableFuture、线程池 |
async/await |
AsyncDataStream |
异步算子 | 并发请求合并 |
| 背压 | JVM 线程 + 网络缓冲 | 限流/队列积压 |
| 并行度 | 多线程/多实例 | 多 Worker |
七、JVM、类加载与 Maven:Flink 工程化的硬骨头
Flink 用户代码运行在 TaskManager JVM 中,所以你会遇到:
-
NotSerializableException:函数对象或字段不可序列化。 -
KryoException:POJO 不规范,退化成 Kryo。 -
ClassCastException:泛型擦除/类型推断错误,显式returns。 -
NoSuchMethodError:依赖冲突,Mavenprovided没配好。 -
OOM/Metaspace:JVM 内存、类加载泄漏、依赖打得太肥。
| Flink 工程问题 | Java/Maven 机制 |
|---|---|
| 打包 | maven-shade-plugin |
| Flink 依赖 | provided scope |
| 日志 | SLF4J + Log4j2/Logback 冲突 |
| 类加载 | TaskManager 用户类加载器 |
| 内存调优 | 堆内存、堆外内存、RocksDB |
八、异常、容错与侧输出流:银行生产环境的保命技能
Flink 的容错靠 Checkpoint,但你的代码要配合:
-
try-catch吃掉脏数据,避免整个任务崩溃。 -
OutputTag侧输出流,把脏数据送死信队列。 -
CheckpointedFunction实现状态快照。 -
重启策略与 Checkpoint 配合。
java
java
OutputTag<Transaction> dirtyTag = new OutputTag<Transaction>("dirty") {};
OutputTag 用匿名子类,也是因为 Java 泛型擦除需要保留类型。
九、Flink 概念 ↔ Java 机制 ↔ 前端类比
| Flink 概念 | Java 机制 | 前端类比 |
|---|---|---|
MapFunction |
SAM + Lambda | Array.map |
RichMapFunction |
抽象类 + 生命周期 | 组件挂载/卸载 |
RuntimeContext |
运行时上下文 | React Context |
TypeInformation |
泛型擦除补丁 | TS 类型 + Schema |
| POJO 序列化 | Java Bean 规范 | 对象序列化 |
ValueState |
泛型 + 状态托管 | useState + 持久化 |
KeyedProcessFunction |
抽象类 + Timer | 事件 + 定时器 |
AsyncFunction |
CompletableFuture |
async/await |
Checkpoint |
序列化 + 快照 | 版本快照 |
Maven provided |
类加载 + 依赖隔离 | peerDependencies |
| JVM 内存/GC | 堆/堆外/GC | 无 |
十、学习建议:用三个问题看穿 Flink Java API
以后每看到一个 Flink Java 类,问自己:
-
它是函数式接口还是抽象类?
接口 → 可 Lambda;抽象类 → 要继承,可能有生命周期。
-
它的泛型参数是什么?Flink 怎么知道运行时类型?
会不会擦除?要不要
returns/TypeHint? -
它需要序列化、状态、生命周期吗?
是
RichFunction还是ProcessFunction?状态放哪里?open里初始化什么?
必须掌握 vs 可以后置
必须掌握 :
Lambda/SAM、泛型擦除与 TypeHint、POJO 规则、异常处理、Maven provided、RichFunction 的 open/close、RuntimeContext、ValueState、KeyedProcessFunction、OutputTag。
遇到再学 :
反射注解、类加载、JVM 调优、RocksDB、异步 I/O、CheckpointedFunction、Metrics。
暂时别碰 :
Spring、Java EE、Swing、JPA、复杂多线程锁。它们和 Flink 主线关系不大。
一句话总结:
Flink 是 Java 的分布式流处理运行时。你学 Java 的函数式接口、泛型、序列化、生命周期、JVM,就是在学 Flink 的底层契约。
Flink 算子
"算子"这个词,是你从"写业务代码"跨入"写大数据引擎代码"时,第一个必须彻底吃透的概念。它其实不神秘,用前端的话说,算子就是 Flink 里的"组件"或"中间件",是数据流水线上的一个个处理工位。
一、一句话定义
Flink 算子(Operator) = 对数据流进行某种转换操作的"计算单元"。
它接收上游数据,做点事情,再发给下游。多个算子串起来,就构成了一条 数据流水线(Dataflow)。
text
Source算子 → 清洗算子 → 打标算子 → 聚合算子 → Sink算子
二、用前端类比,一秒理解
| 前端概念 | Flink 算子 | 说明 |
|---|---|---|
Array.map |
MapOperator |
一对一转换 |
Array.filter |
FilterOperator |
过滤 |
Array.flatMap |
FlatMapOperator |
一对多/零 |
Array.reduce |
ReduceOperator |
两两聚合 |
keyBy + 分组 |
KeyedStream |
按 key 分流 |
setTimeout |
ProcessFunction + Timer |
定时触发 |
组件生命周期 useEffect |
RichFunction 的 open/close |
初始化/清理 |
关键差异 :前端 map 是同步、单机、一次性;Flink 算子是分布式、有状态、可并行、可容错的。
三、算子的两种分类方式
按"有没有状态"分
无状态算子:来一条处理一条,不记住任何东西。
-
map、filter、flatMap -
适合:清洗、格式转换、字段提取
有状态算子:需要记住历史数据。
-
keyBy+reduce、aggregate -
KeyedProcessFunction、window -
适合:累计金额、去重、风控规则
按"API 层级"分
DataStream API 算子:
-
map、filter、flatMap、keyBy、reduce、window、process -
底层、灵活、需要写 Java
Flink SQL 算子:
-
SELECT、WHERE、GROUP BY、JOIN、WINDOW -
上层、声明式、不用写 Java
四、银行场景里的算子实例
回到之前写的信用卡交易清洗:
java
java
sourceStream
.map(json -> JSON.parseObject(json, Transaction.class)) // 算子1:解析
.filter(tx -> "SUCCESS".equalsIgnoreCase(tx.getStatus())) // 算子2:过滤
.map(tx -> { // 算子3:清洗+打标
tx.setAmount(cleanAmount(tx.getRawAmount()));
tx.setRiskLevel(tagRisk(tx));
return tx;
})
.keyBy(Transaction::getCardNo) // 算子4:按卡号分组
.process(new CardWindowFunction()) // 算子5:有状态累计
.print(); // 算子6:输出
每一个 . 后面调用的方法,就是一个算子。
它们被 Flink 编译成一张 DAG(有向无环图),分发到不同 TaskManager 上并行执行。
五、算子链(Operator Chain)
Flink 会把能合并的算子"串"在一起,放在同一个线程里执行,减少网络传输和序列化开销。
text
map → filter → map (三个算子,但可能被优化成一个算子链)
好处 :减少线程切换、减少序列化。
代价:调试时堆栈变长,报错行号可能对不上。
用 .disableChaining() 禁止合并,方便调试;
用 .startNewChain() 手动断开。
六、算子 vs 函数:别混淆
| 概念 | 是什么 | 举例 |
|---|---|---|
| 算子(Operator) | Flink 运行时的一个处理节点 | MapOperator、FilterOperator |
| 函数(Function) | 你写的业务逻辑 | MapFunction、FilterFunction |
| 算子子任务(SubTask) | 算子在某个并行度下的一个实例 | map[0]、map[1] |
你写的是函数,Flink 把它包装成算子,再拆成子任务并行跑。
七、必须记住的算子分类表
| 类别 | 算子 | 有无状态 | 用途 |
|---|---|---|---|
| 转换 | map、flatMap、filter |
无 | 清洗、转换、过滤 |
| 分组 | keyBy |
无 | 按 key 分流 |
| 聚合 | reduce、aggregate、sum |
有 | 累计、求和、最大最小 |
| 窗口 | window、windowAll |
有 | 时间/数量窗口 |
| 处理 | process、apply |
有 | 底层、定时器、侧输出 |
| 合并 | union、connect、join |
视情况 | 多流合并 |
| 输出 | print、addSink |
视情况 | 控制台、Kafka、DB |
八、一句话总结
算子 = Flink 数据流水线上的工位。
map/filter/flatMap 是在定义工位做什么;
keyBy 是在决定数据去哪个工位;
process/window 是在给工位加"记忆"和"定时器";
sink 是把成品送出去。
你学的 Java 函数式接口、泛型、生命周期、状态,全都是在为"写算子"服务。
下一步,你可以做一个小实验:
把之前银行清洗 Demo 里的 .map() 拆成两个独立的 RichMapFunction,分别实现"金额清洗"和"风险打标",在 open() 里各打印一行日志。运行后观察:
-
日志打印几次?(提示:和并行度有关)
-
两个算子有没有被合并成算子链?
这个实验能让你直观感受到"函数 → 算子 → 子任务"的映射关系。
implements 是 Java 中的一个关键字
implements 是 Java 中的一个关键字,意思是 "实现" 。它专门用于类实现接口。
1. 接口是什么?
接口(interface)可以理解成一份契约 或规范。它里面定义了一些方法(只有方法签名,没有方法体),但不关心具体怎么实现。例如:
java
java
public interface FlatMapFunction<T, O> {
void flatMap(T value, Collector<O> out) throws Exception;
}
任何类只要声明 implements FlatMapFunction,就相当于签了这份契约 ,必须提供 flatMap 方法的具体代码。
2. 在你这段代码里
java
java
public class PKFlatMapFunction implements FlatMapFunction<String, String> {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
...
}
}
-
PKFlatMapFunction是一个类。 -
implements FlatMapFunction<String, String>表示:这个类实现了FlatMapFunction接口。 -
尖括号
<String, String>是泛型:第一个String是输入类型,第二个String是输出类型。 -
因为实现了接口,所以必须重写 接口里的
flatMap方法。@Override注解就是告诉编译器"我这是在重写父接口的方法"。 -
方法内部:把一行文本按逗号拆分,去掉空格、转小写,然后通过
out.collect(...)把每个单词发送出去。
3. 在 Flink 中的作用
Flink 的 FlatMapFunction 是一个函数接口。你写一个类实现它,Flink 就会在运行时调用你的 flatMap 方法,对每条输入数据执行你定义的逻辑。Collector 是 Flink 提供的收集器,用来输出结果(可以输出 0 个、1 个或多个)。
4. implements 和 extends 的区别
-
extends:继承类 (或接口继承接口)。一个类只能extends一个父类。 -
implements:实现接口 。一个类可以implements多个接口,用逗号隔开。 -
接口不能用来创建对象,必须由类来实现它。
简单说:implements 就是"我承诺按照这个接口的规定来写代码" 。在你的例子里,就是承诺实现 flatMap 方法,让 Flink 能够调用它来处理数据。
泛型擦除
"泛型擦除"是你从 Java 语法表面,深入到 JVM 运行机制时遇到的第一个"反直觉"概念。用一句话概括:
Java 泛型只存在于编译期,编译成字节码后,泛型类型信息会被"擦掉",运行时 JVM 看不到
List<String>和List<Integer>的区别。
这就像 TypeScript 的类型注解,编译成 JavaScript 后全没了。
但 Java 比 TS 更麻烦,因为 Flink 需要在运行时知道类型来做序列化和状态存储。
一、先看一个"见鬼"的实验
java
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true!
两个明明类型不同的 List,运行时 getClass() 返回的是同一个 ArrayList.class。
泛型信息在运行时被擦除了。
再看这个:
java
List<String> list = new ArrayList<>();
list.add("hello");
// 反射绕过编译检查,塞个 Integer 进去
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, 123); // 运行时居然成功了!
编译期 list.add(123) 会报错,但反射可以绕过,因为运行时根本不知道 list 应该只装 String。
二、擦除后,泛型变成了什么?
| 源码 | 擦除后(字节码里) |
|---|---|
List<String> |
List |
List<Integer> |
List |
T extends Number |
Number |
T(无界) |
Object |
Map<String, List<Integer>> |
Map |
规则:
-
无界泛型
T→ 擦成Object。 -
有界泛型
T extends Number→ 擦成Number。 -
泛型参数本身被擦掉,只剩原始类型(Raw Type)。
三、为什么 Java 要设计成擦除?
历史原因:兼容性。
Java 5 引入泛型时,必须保证旧代码(没有泛型的 List)和新代码(List<String>)能互相调用。如果泛型信息保留到运行时,JVM 规范、类文件格式、反射 API 全都要改,代价太大。
所以 Java 选择了 "编译期检查,运行时擦除" 的折中方案:
-
编译期:
javac帮你检查类型安全。 -
运行时:JVM 只看到原始类型,保证老代码能跑。
代价:运行时拿不到泛型信息,于是有了各种"补丁"。
四、擦除带来的四个经典坑
坑 1:不能 new T()
java
public <T> T create() {
return new T(); // 编译错误!运行时 T 是 Object,不知道构造哪个类
}
解决 :传入 Class<T> 对象。
java
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
坑 2:不能 instanceof T
java
if (obj instanceof T) { } // 编译错误
解决 :用 Class<T> 判断。
java
if (clazz.isInstance(obj)) { }
坑 3:不能创建泛型数组
java
T[] arr = new T[10]; // 编译错误
List<String>[] lists = new List<String>[10]; // 编译错误
解决 :用 (T[]) new Object[10] 强转,但会有 unchecked 警告。
坑 4:方法重载冲突
java
void method(List<String> list) { }
void method(List<Integer> list) { } // 编译错误!擦除后都是 List
原因:擦除后两个方法签名一样,JVM 无法区分。
五、擦除的"补丁":桥方法(Bridge Method)
擦除后,多态会出问题。看这个例子:
java
class Parent {
Object get() { return null; }
}
class Child extends Parent {
@Override
String get() { return "hello"; } // 看起来是重写
}
擦除后 Parent.get() 返回 Object,Child.get() 返回 String,签名不一致,怎么保证多态?
编译器偷偷生成一个桥方法:
java
class Child extends Parent {
String get() { return "hello"; }
// 编译器自动生成
Object get() { return get(); } // 桥方法,调用真正的 String get()
}
所以你反射看 Child.class.getDeclaredMethods() 会看到两个 get(),这就是桥方法。
六、Flink 为什么被泛型擦除"坑"得最惨?
Flink 需要在运行时知道数据类型,用来:
-
序列化(网络传输、状态存储)
-
生成序列化器(
TypeSerializer) -
状态后端存储
但 Java 擦除后,Flink 拿到的是 Object,不知道具体类型。于是 Flink 搞了一套 TypeInformation 来"补类型"。
java
DataStream<Transaction> stream = env.fromElements(...);
Flink 怎么知道是 Transaction?靠:
-
反射 + 泛型签名:从方法签名、字段声明中提取。
-
TypeHint:匿名子类保留泛型。 -
returns():显式告诉 Flink。
java
// 显式指定类型,防止擦除后推断失败
stream.returns(new TypeHint<Transaction>() {});
为什么 TypeHint 用匿名子类?
java
new TypeHint<Transaction>() {}
匿名子类会保留父类的泛型签名,Flink 通过反射读这个签名,就能拿到 Transaction。这是擦除时代的标准"黑科技"。
七、Flink POJO 规则为什么这么严格?
因为 Flink 要靠反射重建类型信息:
-
public类 → 反射能访问。 -
无参构造 → 反射能创建实例。
-
publicgetter/setter → 反射能读写字段。
不符合规则,Flink 只能退化成 KryoSerializer(通用但慢),甚至直接报错。
本质:泛型擦除后,Flink 只能靠 Java Bean 规范 + 反射,重新"猜"出类型结构。
八、一张表:编译期 vs 运行时
| 阶段 | 能看到什么 | 看不到什么 |
|---|---|---|
| 编译期(javac) | List<String>、T extends Number |
无 |
| 运行时(JVM) | List、Object、Number |
String、Integer、T |
| Flink 运行时 | 靠 TypeInformation 补回类型 |
原生泛型信息 |
九、前端类比:TS 擦除 vs Java 擦除
| 维度 | TypeScript | Java |
|---|---|---|
| 类型何时存在 | 编译期 | 编译期 |
| 运行时类型 | 全部擦除 | 泛型擦除,原始类型保留 |
| 运行时类型检查 | 无 | 有(instanceof、反射) |
| 补类型方式 | 类型守卫、Zod | TypeHint、Class<T>、反射 |
| 对框架的影响 | 运行时无类型 | Flink 需要 TypeInformation |
核心差异:
TS 擦除后 JavaScript 完全没有类型;
Java 擦除后,类的原始类型 还在(List 还在,只是 <String> 没了),所以反射还能干活。
十、你必须记住的三句话
-
泛型是编译期的"语法糖",运行时被擦成原始类型或上界。
-
擦除导致运行时拿不到泛型,所以需要
Class<T>、TypeHint、反射来补。 -
Flink 的
TypeInformation、POJO 规则、returns(),全是在对抗泛型擦除。
写一段代码验证擦除:
java
java
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // 输出什么?
// 再用反射往 a 里塞一个 Integer,看看能不能成功
思考:Flink 如果拿到的是 List,它怎么知道里面装的是 Transaction 还是 String? 想清楚这个问题,你就真正理解 TypeInformation 存在的意义了。
Lambda
Lambda 是你从"写命令式代码"跨入"写函数式代码"的那道门。
你在前端写 arr.map(x => x * 2) 时,已经天天在用它了。
Java 8 引入 Lambda,本质就是让 Java 也能写出这种"把函数当参数传"的代码。
一、一句话定义
Lambda = 一个匿名函数,用来简洁地实现"函数式接口"。
拆开看两个关键词:
-
匿名函数:没有名字、即写即用的函数。
-
函数式接口:只有一个抽象方法的接口(SAM,Single Abstract Method)。
二、从前端到 Java:同一个东西,两种写法
前端 JS:
js
const double = x => x * 2;
[1, 2, 3].map(double); // [2, 4, 6]
Java 8+:
java
Function<Integer, Integer> doubleIt = x -> x * 2;
前端:
js
arr.filter(x => x > 10);
Java:
java
list.stream().filter(x -> x > 10).collect(Collectors.toList());
你早就懂 Lambda 了,只是 Java 的语法更啰嗦一点。
三、Java Lambda 的三种写法
java
java
// 1. 完整写法:参数类型 + 括号 + 箭头 + 方法体
(int x, int y) -> { return x + y; }
// 2. 省略参数类型(编译器推断)
(x, y) -> { return x + y; }
// 3. 省略大括号和 return(单表达式)
(x, y) -> x + y
规则:
-
参数类型可省略,编译器从函数式接口推断。
-
单参数时括号可省略:
x -> x * 2。 -
单表达式时
{}和return可省略。 -
多行逻辑必须用
{}和return。
四、Lambda 的前提:函数式接口
Lambda 不能凭空存在,它必须赋值给一个函数式接口。
Java 标准库自带四大核心函数式接口:
| 接口 | 签名 | 前端类比 | 用途 |
|---|---|---|---|
Function<T,R> |
R apply(T t) |
x => y |
转换 |
Consumer<T> |
void accept(T t) |
x => { ... } |
消费,无返回 |
Supplier<T> |
T get() |
() => y |
生产,无输入 |
Predicate<T> |
boolean test(T t) |
x => true/false |
判断 |
Flink 的算子全是函数式接口:
-
MapFunction<T,O>→ 类似Function<T,O> -
FilterFunction<T>→ 类似Predicate<T> -
FlatMapFunction<T,O>→ 一对多
所以你写 Flink 时能直接:
java
.map(x -> x * 2)
.filter(x -> x > 10)
因为 map 接收 MapFunction,filter 接收 FilterFunction,都是 SAM。
五、Lambda 的底层:不是魔法,是语法糖
Lambda 编译后,不会生成一个独立的类文件 (不像匿名内部类会生成 Outer$1.class)。
JVM 用 invokedynamic 指令在运行时动态生成实现。
对比匿名内部类:
java
java
// 匿名内部类(Java 8 之前)
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("clicked");
}
});
// Lambda(Java 8+)
button.addActionListener(e -> System.out.println("clicked"));
匿名内部类:
-
会生成
Outer$1.class。 -
this指向匿名内部类实例。 -
可以有自己的字段。
Lambda:
-
不生成独立 class 文件。
-
this指向外部类实例。 -
不能有字段。
六、变量捕获:Lambda 里的"闭包"
Lambda 可以访问外部变量,但有严格限制:
java
int base = 10;
Function<Integer, Integer> add = x -> x + base; // 可以读
// base = 20; // 编译错误!不能修改
规则 :Lambda 捕获的外部变量必须是 final 或 effectively final(赋值后不再改变)。
为什么?
Lambda 可能在另一个线程执行,如果外部变量能被修改,会有线程安全问题。
Java 直接禁止,简单粗暴。
前端对比 :JS 的闭包可以自由修改外部变量(let),Java 不行。这是 Java 更保守的地方。
七、方法引用:Lambda 的进一步简化
当 Lambda 只是"调用一个已有方法"时,可以写成方法引用:
java
java
// Lambda
list.forEach(x -> System.out.println(x));
// 方法引用
list.forEach(System.out::println);
// Lambda
list.map(x -> x.getName());
// 方法引用
list.map(Transaction::getName);
四种形式:
| 形式 | 语法 | 例子 |
|---|---|---|
| 静态方法 | 类::静态方法 |
Integer::parseInt |
| 实例方法 | 对象::方法 |
System.out::println |
| 任意对象实例方法 | 类::实例方法 |
String::length |
| 构造器 | 类::new |
ArrayList::new |
Flink 里常见:
java
java
.keyBy(Transaction::getCardNo)
.map(Transaction::cleanAndTag)
八、Lambda 在 Flink 里的"能用"与"不能用"
能用 Lambda:
java
.map(x -> x * 2)
.filter(x -> x > 10)
.keyBy(Transaction::getCardNo)
不能用 Lambda:
java
// 有状态、需要 open/close、需要 RuntimeContext
public class CardWindowFunction extends KeyedProcessFunction<String, Transaction, Alert> {
@Override
public void open(Configuration parameters) { ... }
@Override
public void processElement(...) { ... }
}
分界线:
-
无状态、无生命周期 → Lambda。
-
有状态、需要
open/close→ 必须继承RichFunction或ProcessFunction。
为什么?
Lambda 没有 open() 方法,无法初始化状态和资源。
九、Lambda 与泛型擦除的交集
Lambda 本身也会被擦除,但 Flink 需要知道类型:
java
.map(x -> JSON.parseObject(x, Transaction.class))
这里 x 的类型、返回值类型,Flink 靠 类型推断 + returns() 来补。
如果推断失败:
java
.map(x -> JSON.parseObject(x, Transaction.class))
.returns(Transaction.class); // 显式告诉 Flink
Lambda 简洁,但代价是类型信息更模糊,Flink 更需要显式声明。
十、一张表:Lambda vs 匿名内部类 vs 方法引用
| 维度 | Lambda | 匿名内部类 | 方法引用 |
|---|---|---|---|
| 语法长度 | 短 | 长 | 最短 |
| 生成 class 文件 | 否 | 是 | 否 |
this 指向 |
外部类 | 自身 | 外部类 |
| 能有字段 | 否 | 是 | 否 |
| 适用场景 | SAM 实现 | 需要字段/多方法 | 已有方法可复用 |
| Flink 无状态算子 | ✅ | ✅ | ✅ |
| Flink 有状态算子 | ❌ | ❌ | ❌ |
十一、你必须记住的三句话
-
Lambda = 匿名函数,用来实现函数式接口(SAM)。
-
Lambda 是语法糖,底层靠
invokedynamic,不生成独立 class。 -
Flink 无状态操作用 Lambda,有状态操作必须用
RichFunction/ProcessFunction。
把这段匿名内部类改写成 Lambda,再改写成方法引用:
java
java
list.stream()
.filter(new Predicate<Transaction>() {
@Override
public boolean test(Transaction tx) {
return "SUCCESS".equalsIgnoreCase(tx.getStatus());
}
})
.map(new Function<Transaction, String>() {
@Override
public String apply(Transaction tx) {
return tx.getTxId();
}
})
.forEach(new Consumer<String>() {
@Override
public void accept(String id) {
System.out.println(id);
}
});
思考:为什么 Flink 的 KeyedProcessFunction 不能用 Lambda 写? 想清楚这个,你就把 Lambda 和算子生命周期彻底串起来了。