Java 基础里,泛型、IO、序列化常被分成三个章节。实际工程里它们经常连在一起:集合里装什么类型,怎么从文件或网络读出来,读出来的字节怎么还原成对象,跨版本和跨语言会不会出问题。
可以用一条线串起来:泛型解决编译期类型约束,IO 解决数据进出,序列化解决对象和字节之间的转换。
泛型解决编译期类型安全
泛型最直接的作用是让集合和方法有类型约束:
List<String> names = new ArrayList<>();
names.add("Tom");
这样编译器能阻止你把 Integer 塞进 List<String>。泛型带来的好处是:
- 类型安全。
- 少写强制转换。
- API 更清晰。
- 同一套逻辑可以复用到不同类型。
常见形式有泛型类、泛型方法和泛型接口:
public class Cache<T> {
private T value;
}
public <T> T identity(T value) {
return value;
}
public interface Repository<T, ID> {
T findById(ID id);
}
注意泛型方法里的 <T> 是方法自己的类型参数,和类上的 <T> 可以不是同一个。
通配符看 PECS
通配符最容易混:
<?>:未知类型。<? extends T>:T 或 T 的子类。<? super T>:T 或 T 的父类。
记 PECS:
- Producer Extends:只从容器里读,用
extends。 - Consumer Super:要往容器里写,用
super。
例如:
public double sum(List<? extends Number> numbers) {
// 适合读取 Number
}
public void addInts(List<? super Integer> target) {
target.add(1);
}
extends 适合读,因为你知道取出来至少是 T。super 适合写,因为你知道放入 T 或 T 的子类是安全的。
类型擦除是泛型的运行期边界
Java 泛型主要存在于编译期。运行期会发生类型擦除:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
运行期它们都是 ArrayList.class。
无界泛型擦成 Object,有界泛型擦成上界类型。这也是为什么不能直接 new T(),不能 instanceof T,也不能创建具体参数化类型数组。
反射还能绕过编译期检查:
List<Integer> list = new ArrayList<>();
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, "not integer");
这说明泛型不是运行期强隔离,它主要帮你在编译期减少错误。
IO 先分字节流和字符流
IO 解决数据进出。Java 里最基础的两组是:
- 字节流:
InputStream/OutputStream - 字符流:
Reader/Writer
字节流按 byte 处理,适合图片、压缩包、二进制协议。字符流按 char 处理,适合文本,并涉及编码转换。
常见搭配:
try (InputStream in = new FileInputStream("a.bin")) {
byte[] buffer = new byte[8192];
}
文本读取要明确编码:
try (Reader reader = new InputStreamReader(
new FileInputStream("a.txt"), StandardCharsets.UTF_8)) {
}
不要把二进制文件当字符流读,也不要在字符流里忽略编码。乱码和数据损坏很多都来自这里。
节点流和处理流
节点流直接连接数据源,比如文件、网络连接、字节数组。处理流包装其他流,提供缓冲、转换、对象读写等增强能力。
例如:
try (BufferedInputStream in = new BufferedInputStream(
new FileInputStream("a.bin"))) {
}
BufferedInputStream 不直接代表文件,它包装了 FileInputStream,减少底层读取次数。
这种装饰器式结构在 IO 里很常见:一层负责数据源,一层负责缓冲,一层负责编码,一层负责对象转换。
序列化是对象和字节之间的转换
序列化解决“对象怎么变成可存储、可传输的字节”。Java 原生序列化依赖 Serializable,但工程里要谨慎使用。
原因包括:
- 字节体积大。
- 跨语言不友好。
- 类结构变更可能反序列化失败。
- 安全风险较多。
现在更常见的是 JSON、Protobuf、Kryo、Hessian 等方案。选型要看场景:
- 对外 API:JSON 可读性好。
- 内部高性能 RPC:Protobuf、Hessian 等更常见。
- 本地临时缓存:可以结合具体框架选择。
不管哪种方式,都要考虑版本兼容。字段新增、删除、改名都会影响反序列化。
三者怎么连起来
一个典型流程是:
- Java 对象放在泛型集合里,编译期保证类型。
- 通过序列化器把对象转成字节。
- 通过 OutputStream 写到文件、网络或缓存。
- 读取时通过 InputStream 拿到字节。
- 通过反序列化器还原对象。
- 再放回带泛型约束的数据结构。
泛型约束的是代码层类型,IO 处理的是字节流,序列化负责中间转换。不要把泛型当运行期安全边界,也不要把序列化当没有成本的对象复制。
最后记忆
泛型让代码在编译期更安全,IO 让数据能进出系统,序列化让对象能跨进程、跨文件或跨网络存在。
理解这条线,再看集合、文件上传、RPC、缓存、消息队列和接口协议,很多基础知识就能连成一张网。




