Flink 学习需要具备的 Java 基础知识总结

Flink学习的核心在于掌握Java基础:函数式接口(Lambda)用于无状态算子,RichFunction与RuntimeContext处理有状态逻辑,泛型擦除通过TypeInformation/TypeHint补偿,POJO规则保障序列化性能,状态管理依赖ValueState等。


理解JVM机制、类加载、并发安全及Maven依赖隔离是工程落地关键。


Flink算子本质是分布式数据流上的组件,其设计源于Java运行时特性。


掌握"函数→算子→子任务"映射关系,结合前端类比(如useState、useEffect),可将复杂问题转化为可解释的Java契约。


Flink 的很多"难",其实是 Java 的"难";

Flink 的很多"设计",其实是 Java 运行时机制的延伸。


把 Flink 当成"用 Java 写的、运行在 JVM 上的、有状态的分布式流处理框架",很多问题会从"玄学"变成"可解释"。


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。

这就是为什么你写清洗时很顺,但一写状态就突然变复杂。


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 泛型擦除的"运行时补丁"。


Flink 对 POJO 有明确规则。

符合规则,用高效的 PojoSerializer;

不符合,退化成 KryoSerializer,性能差、状态大、易报错。

Flink POJO 规则:

  1. 类是 public。

  2. 有 public 无参构造。

  3. 字段是 public,或有 public getter/setter。

  4. 字段类型可序列化。

所以在银行场景写 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 契约。


RichMapFunction、RichFlatMapFunction、RichSinkFunction 等继承自 AbstractRichFunction,有:

  • open(...):初始化,类似 React useEffect 挂载。

  • 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。


Flink 一个算子子任务通常在一个线程中执行,但 TaskManager 是多线程 JVM。

所以:

  • 不要跨算子共享可变 Java 对象。

  • SimpleDateFormat、Random 等线程不安全对象,要在 open 中每子任务初始化。

  • 异步 I/O 用 AsyncFunction + CompletableFuture,类似前端 Promise/async-await。

Flink 概念 Java 机制 前端类比
AsyncFunction CompletableFuture、线程池 async/await
AsyncDataStream 异步算子 并发请求合并
背压 JVM 线程 + 网络缓冲 限流/队列积压
并行度 多线程/多实例 多 Worker

Flink 用户代码运行在 TaskManager JVM 中,所以你会遇到:

  • NotSerializableException:函数对象或字段不可序列化。

  • KryoException:POJO 不规范,退化成 Kryo。

  • ClassCastException:泛型擦除/类型推断错误,显式 returns。

  • NoSuchMethodError:依赖冲突,Maven provided 没配好。

  • 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 机制 前端类比
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 类,问自己:

  1. 它是函数式接口还是抽象类?

    接口 → 可 Lambda;抽象类 → 要继承,可能有生命周期。

  2. 它的泛型参数是什么?Flink 怎么知道运行时类型?

    会不会擦除?要不要 returns / TypeHint?

  3. 它需要序列化、状态、生命周期吗?

    是 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 算子(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() 里各打印一行日志。运行后观察:

  1. 日志打印几次?(提示:和并行度有关)

  2. 两个算子有没有被合并成算子链?

这个实验能让你直观感受到"函数 → 算子 → 子任务"的映射关系。


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(...) 把每个单词发送出去。

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 需要在运行时知道数据类型,用来:

  • 序列化(网络传输、状态存储)

  • 生成序列化器(TypeSerializer)

  • 状态后端存储

但 Java 擦除后,Flink 拿到的是 Object,不知道具体类型。于是 Flink 搞了一套 TypeInformation 来"补类型"。

java

复制代码
DataStream<Transaction> stream = env.fromElements(...);

Flink 怎么知道是 Transaction?靠:

  1. 反射 + 泛型签名:从方法签名、字段声明中提取。

  2. TypeHint:匿名子类保留泛型。

  3. returns():显式告诉 Flink。

java

复制代码
// 显式指定类型,防止擦除后推断失败
stream.returns(new TypeHint<Transaction>() {});

为什么 TypeHint 用匿名子类?

java

复制代码
new TypeHint<Transaction>() {}

匿名子类会保留父类的泛型签名,Flink 通过反射读这个签名,就能拿到 Transaction。这是擦除时代的标准"黑科技"。


因为 Flink 要靠反射重建类型信息:

  • public 类 → 反射能访问。

  • 无参构造 → 反射能创建实例。

  • public getter/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> 没了),所以反射还能干活。


十、你必须记住的三句话

  1. 泛型是编译期的"语法糖",运行时被擦成原始类型或上界。

  2. 擦除导致运行时拿不到泛型,所以需要 Class<T>、TypeHint、反射来补。

  3. 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:

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 有状态算子 ❌ ❌ ❌

十一、你必须记住的三句话

  1. Lambda = 匿名函数,用来实现函数式接口(SAM)。

  2. Lambda 是语法糖,底层靠 invokedynamic,不生成独立 class。

  3. 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 和算子生命周期彻底串起来了。

相关推荐
福建佰胜张工6 分钟前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop9 分钟前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x37 分钟前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事1 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客2 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1882 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su3 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123454 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
苜蓿muxu4 小时前
Java 字符串拼接:+、StringBuilder、StringBuffer的区别
java