Java 泛型、IO 和序列化怎么串起来理解:类型擦除到字节流

3次阅读
没有评论

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 等更常见。
  • 本地临时缓存:可以结合具体框架选择。

不管哪种方式,都要考虑版本兼容。字段新增、删除、改名都会影响反序列化。

三者怎么连起来

一个典型流程是:

  1. Java 对象放在泛型集合里,编译期保证类型。
  2. 通过序列化器把对象转成字节。
  3. 通过 OutputStream 写到文件、网络或缓存。
  4. 读取时通过 InputStream 拿到字节。
  5. 通过反序列化器还原对象。
  6. 再放回带泛型约束的数据结构。

泛型约束的是代码层类型,IO 处理的是字节流,序列化负责中间转换。不要把泛型当运行期安全边界,也不要把序列化当没有成本的对象复制。

最后记忆

泛型让代码在编译期更安全,IO 让数据能进出系统,序列化让对象能跨进程、跨文件或跨网络存在。

理解这条线,再看集合、文件上传、RPC、缓存、消息队列和接口协议,很多基础知识就能连成一张网。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-06-30发表,共计2317字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)