Java IO:序列化、打印流与 Properties
对象也能写进文件、跨网络传输——靠的就是序列化。本篇讲透序列化的规则与版本兼容陷阱,再补齐打印流、随机访问流和 Properties 这几块 IO 拼图。
本篇目标
- 理解序列化/反序列化的概念,掌握 Serializable、serialVersionUID、transient 三大规则
- 学会序列化流读写多个对象的两种技巧
- 会用打印流 PrintWriter、数据流和随机访问流 RandomAccessFile
- 用 Properties 读写配置文件
序列化是什么
序列化(Serialize)就是把内存中的对象转成字节序列写出去,持久化到文件或发往网络;反序列化则反过来,把字节序列还原成对象。JDK 提供的工具是一对”对象流”:ObjectOutputStream 用 writeObject() 把对象写出,ObjectInputStream 用 readObject() 把对象读回。
// 写出(序列化) |
反序列化还有一个特点:它不走构造方法,对象的字段值全部来自字节流,这一点在某些框架的底层源码里会遇到。
三大规则
1. Serializable 是个空接口
直接运行上面的代码会抛 NotSerializableException。要被序列化的类必须实现 Serializable 接口——打开源码你会发现它里面什么都没有,一个方法都没有。这类接口叫标记接口,作用只是给 JVM 打一个”允许序列化”的标记,序列化机制运行时靠检查这个标记来决定放行还是拒绝。类似的标记接口还有 Cloneable。
2. serialVersionUID:类的版本指纹
public class Person implements Serializable { |
serialVersionUID 是类的”版本指纹”。序列化时它会被一起写入文件;反序列化时 JVM 拿文件里的版本号和当前类的版本号对比,不一致就抛 InvalidClassException。
如果不显式声明,编译器会根据类的字段、方法等结构自动算一个。问题在于:只要类结构有任何改动(哪怕只是加个字段),自动算出的版本号就变了,旧文件全部读不回来。所以实际开发中必须显式写死 serialVersionUID,这样给类增加字段后旧数据仍能兼容读取,新字段用默认值补齐——这就是序列化的版本兼容问题及其标准解法,也是面试高频题。
3. transient:不想被序列化的字段
密码这类敏感字段不该落盘,用 transient 修饰即可让序列化机制跳过它:
private transient String password; // 反序列化后为 null |
另外注意:static 字段属于类而不属于对象,天然不参与序列化,所以也别指望靠它保存对象状态。
最后再提醒两点使用上的细节:一是序列化文件是人看不懂的二进制,别用文本编辑器去改;二是反序列化要求类路径上能找到对应的类定义,类被删了或包名改了都会抛 ClassNotFoundException,这也是实际项目中序列化文件”娇气”的原因——它存的不只是数据,还有对类结构的依赖。
多对象读写技巧
一个流连续 writeObject 多个对象没问题,但读取时有个麻烦:readObject() 读到流末尾不是返回 null,而是抛 EOFException。两种应对:
技巧一:用一个集合装所有对象(推荐)
List<Person> list = new ArrayList<>(); |
读一次拿到集合,循环遍历即可,不用关心对象个数。强转时的泛型警告用 @SuppressWarnings(“unchecked”) 压制,因为泛型在运行期会被擦除,JVM 看到的只是 ArrayList。
技巧二:捕获 EOFException,读到异常就认为读完了。能用,但拿异常当流程控制不优雅,知道即可。
打印流 PrintWriter
System.out 就是一个打印流(PrintStream)。打印流的特点:
- 可以 print/println 任意类型,底层自动调用对象的 toString() 转成字符串,传 null 就打印 “null”
- 只负责输出,永远不会抛 IOException(内部吞掉了),所以代码格外干净
- 构造时开启自动刷新(autoFlush = true)后,println 会自动 flush
PrintWriter pw = new PrintWriter(new FileWriter("demo/g.txt"), true); |
写日志、写简单文本报告时,打印流比 BufferedWriter 顺手。
标准输入输出流
System.in 是 InputStream 类型的标准输入流,默认从键盘读字节;System.out 是 PrintStream 类型的标准输出流,默认打向控制台。这两个”标准设备”可以用 System.setIn() / System.setOut() 重定向到文件:
System.setIn(new FileInputStream("demo/a.txt")); |
重定向之后,程序里所有 System.out.println 的输出都会写进 b.txt,做日志收集或批量测试输入时偶尔能派上用场。这个用法了解即可,重点是理解”控制台其实也是一种 IO 设备”。
数据流与随机访问流
DataInputStream / DataOutputStream 能按基本数据类型整块读写:writeInt() 固定写 4 个字节,readInt() 固定读 4 个字节,写读顺序必须严格对应,否则读出来全是错位的数据。它适合读写结构固定的二进制数据。
RandomAccessFile 不属于流体系,直接继承 Object,但它同时具备了读和写的能力。最大特点是内部维护一个文件指针,可以 seek() 到任意位置读写,不必从头开始:
RandomAccessFile raf = new RandomAccessFile("demo/h.txt", "rw"); |
典型应用是断点续传和多线程分段下载:每个线程 seek 到自己负责的区间写数据,互不干扰。模式参数 “r” 表示只读,”rw” 表示读写。
序列流与内存流(了解)
- SequenceInputStream:把多个字节输入流首尾相接拼成一个流,按顺序依次读。两个流直接传构造方法;更多流要装进 Vector,再用 elements() 得到 Enumeration 传入。这个 API 很老,了解原理即可。
- ByteArrayOutputStream:往内存里写的输出流,数据攒在内部数组里,最后 toByteArray() 或 toString() 一次性取出,适合”先把数据凑齐再统一处理”的场景,也能用来绕过 read(byte[]) 的分批拼接问题。
Properties:配置文件读写
Properties 是一个特殊的 Map:键和值都是字符串,并且可以直接和文件互转。作为集合用 setProperty/getProperty/stringPropertyNames;真正值钱的是 load 和 store 这对方法:
Properties prop = new Properties(); |
.properties 文件每行一条 key=value,# 开头是注释。它一直是 Java 世界最经典的配置载体,后来 Spring 的配置文件、Maven 的 properties 都是它的徒子徒孙。相比自己用字符流逐行解析配置文件,Properties 帮你处理了转义、注释和键值切分,代码量从几十行缩到几行。
经典练习
把上一篇的”试用版软件”升级为 Properties 版本:剩余次数存在 config.properties 里,每次运行 load 出来减一再 store 回去,归零时提示购买正版。体会 Properties 相比手写文本解析的便利。
小结
- 序列化三规则:实现 Serializable 标记接口、显式声明 serialVersionUID、敏感字段加 transient
- 多对象读写最稳的办法是”装进集合一次读写”
- PrintWriter 打印方便且可自动刷新;Data 流按类型整块读写;RandomAccessFile 靠 seek() 实现随机读写,是断点续传的基础
- Properties = 字符串版 Map + load/store 文件互转,是配置文件的标配
- 这些特殊流各有适用场景,记住”干什么事用什么流”,比死记 API 更重要
至此 IO 篇收尾。从字节流到序列化,这套 API 的骨架已经完整了。

