Flink用了大量泛型为什么?——Java泛型和类型擦除
本文目录
先说结论
Java泛型是编译时的类型检查机制,运行时会被擦除。这意味着ArrayList
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
写Flink代码的人都知道,到处都是泛型:DataStream
泛型的基本原理
// 泛型让代码更安全、更通用
// 没有泛型(JDK 1.4之前):
List list = new ArrayList();
list.add('hello');
list.add(123); // 编译通过!但运行时可能类型转换异常
String s = (String) list.get(1); // ClassCastException!
// 有泛型(JDK 5+):
List<String> list = new ArrayList<>();
list.add('hello');
// list.add(123); // 编译就报错!类型安全
String s = list.get(0); // 不需要强制转换
// 泛型方法(Flink的map就是泛型方法)
public <T, O> DataStream<O> map(MapFunction<T, O> func);
// T是输入类型,O是输出类型
// 编译器根据Lambda表达式自动推断T和O
类型擦除——泛型的致命缺陷
// 编译前(源代码中有泛型信息)
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// 编译后(泛型被擦除)
// 两者变成了完全相同的类型!
List strings = new ArrayList(); // 泛型信息消失
List integers = new ArrayList(); // 和上面一模一样
// 运行时验证
System.out.println(strings.getClass() == integers.getClass());
// 输出:true!运行时它们是同一个类
// 为什么Java要做类型擦除?
// 向后兼容——JDK 5之前没有泛型的代码必须能继续运行
// 擦除后泛型代码和非泛型代码在字节码层面一致
// 擦除带来的问题:
// 1. 运行时无法获取泛型参数类型
// 2. 不能 new T()(不知道T是什么类)
// 3. 不能 instanceof List<String>
Flink为什么需要TypeInformation——类型擦除的补偿
Flink需要在运行时知道DataStream的元素类型,用于:① 选择最优的序列化器(第4篇讲过);② 做Key的分区和比较;③ Schema推断(Table API/SQL需要知道字段名和类型)。但类型擦除让Flink在运行时拿不到泛型信息。
// Flink获取类型信息的三种方式
// 方式一:自动推断(大部分情况)
// Flink在编译时通过反射分析Lambda表达式推断类型
DataStream<String> stream = env.fromElements('a', 'b', 'c');
// Flink自动知道这是DataStream<String>
// 方式二:TypeHint(Lambda类型推断失败时手动指定)
DataStream<Tuple2<String, Integer>> result = stream
.map(s -> Tuple2.of(s, 1))
.returns(new TypeHint<Tuple2<String, Integer>>(){});
// TypeHint通过匿名子类保留了泛型信息(编译器会保留)
// 方式三:TypeInformation直接指定
DataStream<Tuple2<String, Integer>> result = stream
.map(s -> Tuple2.of(s, 1))
.returns(Types.TUPLE(Types.STRING, Types.INT));
// 最明确,不依赖任何推断
类型擦除导致的常见报错
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| 'could not determine type info' | Lambda类型推断失败 | 加.returns(TypeHint/TypeInfo) |
| 'using Kryo serialization' | Flink不认识的类型退化为Kryo | 用POJO或Tuple/Row替代 |
| ClassCastException | 泛型擦除后类型转换出错 | 检查数据流的实际类型 |
| InvalidTypesException | 泛型嵌套太深无法推断 | 手动指定TypeInformation |
实战踩坑:Flink Lambda推断失败导致Kryo退化
在某元宇宙社交公司写Flink DataStream时,有一个map操作返回一个复杂的嵌套类型Tuple3<String, Map<String, Integer>, List
症状:Flink日志中打印了WARNING:'Using Kryo serialization for type: ...'。Checkpoint从2秒涨到20秒。
解决:把复杂的Tuple3改成一个简单的POJO类(public字段+无参构造),Flink可以为POJO生成高效的内置序列化器。Checkpoint回到3秒。
// 错误:复杂嵌套泛型,Flink无法推断
stream.map(event -> Tuple3.of(event.name,
computeTagMap(event), // Map<String, Integer>
computeScores(event))); // List<Long>
// Flink: WARNING using Kryo...
// 正确:用POJO替代复杂Tuple
public class EventResult {
public String name;
public String tagsJson; // Map序列化为JSON字符串
public String scoresJson; // List序列化为JSON字符串
public EventResult() {} // 无参构造
}
stream.map(event -> {
EventResult r = new EventResult();
r.name = event.name;
r.tagsJson = JSON.toJSONString(computeTagMap(event));
r.scoresJson = JSON.toJSONString(computeScores(event));
return r;
}).returns(EventResult.class); // Flink可以高效序列化
面试高频追问
追问一:泛型的上界和下界(extends/super)是什么?
extends T>是上界通配符——只能读不能写(Producer),保证读出的是T或T的子类。 super T>是下界通配符——只能写不能读具体类型(Consumer),保证写入的是T或T的子类。记忆口诀:PECS(Producer Extends, Consumer Super)。Flink的Function接口经常用到:MapFunction