java 对象序列化中,如果字段值为 null,默认情况下该字段会被直接跳过,不会在字节流中写入任何数据。反序列化时,若目标类中存在该字段但流中无对应数据,该字段将保持其初始值(即 null)。这一行为由 objectoutputstream 的默认实现决定,无需额外配置即可处理。

ObjectOutputStream 如何标记和存储非空字段?
ObjectOutputStream 通过写入类型描述符和数据块来存储非空字段。对于每个非 null 的实例字段,它会先写入一个标识符表示字段存在,然后写入字段的实际值。如果字段是引用类型且不为 null,还会递归处理该对象的序列化。这种机制确保了只有实际有值的字段才会占用字节流空间。

import java.io.*;
public class NullFieldExample implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
public NullFieldExample(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "Name: " + name + ", Age: " + age + ", Address: " + address;
}
public static void main(String[] args) throws Exception {
// 创建对象,其中 address 为 null
NullFieldExample obj = new NullFieldExample("Alice", 30, null);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
byte[] bytes = baos.toByteArray();
System.out.println("Serialized size: " + bytes.length + " bytes");
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
NullFieldExample deserializedObj = (NullFieldExample) ois.readObject();
ois.close();
System.out.println("Deserialized object: " + deserializedObj);
System.out.println("Address is null: " + (deserializedObj.address == null));
}
}预期输出:
Serialized size: 85 bytes
Deserialized object: Name: Alice, Age: 30, Address: null
Address is null: true
立即学习“Java免费学习笔记(深入)”;
自定义序列化如何处理 null 字段?
当需要更精细地控制 null 字段的序列化行为时,可以实现 writeObject 和 readObject 方法。在这些方法中,可以显式检查字段是否为 null,并决定是否写入特定标记或默认值。例如,可以将 null 转换为一个特殊的占位符,以便在反序列化时区分“未设置”和“显式设置为 null”。这种方式适用于需要兼容不同版本或外部系统的场景。

import java.io.*;
public class CustomNullHandling implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
private static final String NULL_PLACEHOLDER = "__NULL__";
public CustomNullHandling(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// 显式处理 address 为 null 的情况
if (address == null) {
out.writeUTF(NULL_PLACEHOLDER);
} else {
out.writeUTF(address);
}
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 读取 address,如果是占位符则设为 null
String addr = in.readUTF();
this.address = NULL_PLACEHOLDER.equals(addr) ? null : addr;
}
@Override
public String toString() {
return "Name: " + name + ", Age: " + age + ", Address: " + address;
}
public static void main(String[] args) throws Exception {
CustomNullHandling obj = new CustomNullHandling("Bob", 25, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
byte[] bytes = baos.toByteArray();
System.out.println("Custom serialized size: " + bytes.length + " bytes");
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
CustomNullHandling deserializedObj = (CustomNullHandling) ois.readObject();
ois.close();
System.out.println("Custom deserialized object: " + deserializedObj);
System.out.println("Address is null: " + (deserializedObj.address == null));
}
}预期输出:
Custom serialized size: 92 bytes
Custom deserialized object: Name: Bob, Age: 25, Address: null
Address is null: true
使用 transient 关键字对 null 字段有何影响?
如果某个字段被声明为 transient,无论其值是否为 null,它都不会被序列化。这意味着在反序列化后,该字段将始终保留其默认初始值(对于引用类型是 null,对于基本类型是其零值)。这与字段本身为 null 但未标记为 transient 的行为不同:后者在流中不写入数据,但字段仍属于可序列化状态;而 transient 字段完全被排除在序列化过程之外。因此,transient 更适合用于那些不应持久化的敏感或临时数据,而非单纯处理 null 值。


















