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 的 mapfilterflatMapkeyByreduceaggregate 大量使用 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 → 必须继承 RichMapFunctionRichFlatMapFunction 等抽象类,不能用 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 契约。


RichMapFunctionRichFlatMapFunctionRichSinkFunction 等继承自 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 的 ValueStateListStateMapState 都是泛型接口,背后是 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 对象。

  • SimpleDateFormatRandom 等线程不安全对象,要在 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 providedRichFunctionopen/closeRuntimeContextValueStateKeyedProcessFunctionOutputTag

遇到再学

反射注解、类加载、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 RichFunctionopen/close 初始化/清理

关键差异 :前端 map 是同步、单机、一次性;Flink 算子是分布式、有状态、可并行、可容错的。


三、算子的两种分类方式

按"有没有状态"分

无状态算子:来一条处理一条,不记住任何东西。

  • mapfilterflatMap

  • 适合:清洗、格式转换、字段提取

有状态算子:需要记住历史数据。

  • keyBy + reduceaggregate

  • KeyedProcessFunctionwindow

  • 适合:累计金额、去重、风控规则

按"API 层级"分

DataStream API 算子

  • mapfilterflatMapkeyByreducewindowprocess

  • 底层、灵活、需要写 Java

Flink SQL 算子

  • SELECTWHEREGROUP BYJOINWINDOW

  • 上层、声明式、不用写 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 运行时的一个处理节点 MapOperatorFilterOperator
函数(Function) 你写的业务逻辑 MapFunctionFilterFunction
算子子任务(SubTask) 算子在某个并行度下的一个实例 map[0]map[1]

你写的是函数,Flink 把它包装成算子,再拆成子任务并行跑。


七、必须记住的算子分类表

类别 算子 有无状态 用途
转换 mapflatMapfilter 清洗、转换、过滤
分组 keyBy 按 key 分流
聚合 reduceaggregatesum 累计、求和、最大最小
窗口 windowwindowAll 时间/数量窗口
处理 processapply 底层、定时器、侧输出
合并 unionconnectjoin 视情况 多流合并
输出 printaddSink 视情况 控制台、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. implementsextends 的区别

  • 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() 返回 ObjectChild.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) ListObjectNumber StringIntegerT
Flink 运行时 TypeInformation 补回类型 原生泛型信息

九、前端类比:TS 擦除 vs Java 擦除

维度 TypeScript Java
类型何时存在 编译期 编译期
运行时类型 全部擦除 泛型擦除,原始类型保留
运行时类型检查 有(instanceof、反射)
补类型方式 类型守卫、Zod TypeHintClass<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 接收 MapFunctionfilter 接收 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 → 必须继承 RichFunctionProcessFunction

为什么?

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

相关推荐
宸津-代码粉碎机1 小时前
Spring AI企业级工程化落地手册|从Demo改造为生产级商用项目
java·人工智能·python·spring
Beyond_System|系统之外1 小时前
【学编程】Python基础编程题100道(1-20)
java·数据结构·算法
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之环境配置与多环境部署
java·开发语言·spring
小羊没烦恼!1 小时前
Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(完)
java·大数据·前端·网络·word·powerpoint·.net
估值探索者2 小时前
【AI+量化实战 #05】财报季的信息洪流:用LLM给业绩预告分类+算事件窗口收益
java·c语言·c++·人工智能·python·分类·数据挖掘
热爱编程的小李2 小时前
基于若以开发的新版情怀麻将后台
java
starzy19902 小时前
Flink高级之富函数类深度剖析:执行时序、状态注册与序列化陷阱
大数据·flink
zhanghaha13145 小时前
HTML系列教程:14_HTML 表单与输入框 <form>、<input> 零基础详解
java·前端·javascript
yxlalm10 小时前
Spring AI+RAG 01-项目背景与技术选型
java·人工智能·spring