Java序列化

本文介绍了Java序列化的概念、基本用法及其内部实现机制。详细解释了如何通过实现Serializable接口来进行对象的序列化与反序列化,并探讨了序列化在处理复杂对象时的特点,包括对象引用和循环引用的处理。此外,还讨论了序列化的定制方式、版本问题及序列化格式的局限性。

序列化

什么事序列化?简单来说,序列化就是将对象转化为字节流,反序列化就是字节流转化为对象。在Java中具体是如何来使用的呢?他是如何实现的?有什么优缺点?

基本用法
Serializable

要让一个类支持序列化,只需要让这个类实现接口java.io.Serializable,Serializable没有定义任何方法,只是一个标记接口。

声明实现了Serializable接口后,保存/读取对象就可以使用ObjectOutputStream/ObjectInputStream了。

ObjectOutputStream是OutputStream的子类,当实现了ObjectOutput接口,ObjectOutput是DataOutput的子接口,增加了一个方法:

public void writeObject(Object obj) throws IOException

这个方法能够将对象object转化为字节,写到流中。
ObjectInputStream是InputStream的子类,它实现了ObjectInput接口,ObjectInput是DataInput的子接口,增加了一个方法:

public Object readObject() throws ClassNotFoundException, IOException

这个方法能够从流中读取字节,转化为一个对象。
是不是很神奇?只要将类声明实现Serializable接口,然后就可以使用ObjectOutputStream/ObjectInputStream直接读写对象了。我们之前介绍的各种类,如String, Date, Double, ArrayList, LinkedList, HashMap, TreeMap等,都实现了Serializable。

复杂对象

如果对象比较复杂,比如:

  • 如果a, b两个对象都引用同一个对象c,序列化后c是保存两份还是一份?在反序列化后还能让a, b指向同一个对象吗?
  • 如果a, b两个对象有循环引用呢?即a引用了b,而b也引用了a。

答案是肯定的。这就是Java序列化机制的神奇之处,他能自动处理这种引用同一个对象的情况。更神奇的是,他还能自动处理循环引用的情况。反序列化后还能保持原来的引用关系。

定制序列化

默认的序列化机制已经很强大了,他可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。
比如,对于某些字段,我们不想要缓存,比如默认的hashCode方法的返回值,当恢复对象后内存位置变了,基于原内存位置的值也就没有了意义。
那怎么办呢?java提供了多种定制序列化的几只,主要有两种,一种是transient管酱紫,另外一种是实现writeObject和readObject方法。
将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。如下所示:

transient int size = 0;

声明了transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了,可以实现writeObject/readObject方法来自己保存该字段。
可以在这个方法中,调用ObjectOutputStream的方法向流中写入对象的数据。比如,LinkedList使用如下代码序列化列表的逻辑数据:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out size
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (Node<E> x = first; x != null; x = x.next)
        s.writeObject(x.item);
}

需要注意的是第一行代码:

s.defaultWriteObject();

这一行是必须的,他会调用默认的序列化机制,默认机制会保存所有没声明为transient的字段,即使类中的所有字段都是transient,也应该写这一行,因为Java的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化之所以能够神奇的重要原因。
与writeObject对应的是readObject方法,通过它自定义反序列化过程。

序列化的基本原理

总结一下:
- 如果类的字段表示的就是类的逻辑信息,那就可以使用默认序列化机制,只要声明实现Serializable接口即可。
- 否则的话,那就可以使用transient关键字,实现writeObject和readObject来自定义序列化过程。
- Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。
但是,序列化到底是怎么发生的呢?关键在ObjectOutputStream的writeObject和ObjectInputStream的readObject方法内。他们的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇,我们简单介绍一下基本逻辑:
writeObject的基本逻辑:
- 如果对象没有实现Serializable,抛出异常NotSerializableException。
- 每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题。
- 如果对象实现了writeObject方法,调用他的自定义方法。
- 默认是使用反射机制,遍历对象的结构图,对每个没有标记为transient的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型,即完整类名、字段名、字段值等。
readObject的基本逻辑:
- 不调用任何构造方法
- 他自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制。
- 在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出ClassNotFoundException.

版本问题

上面的介绍,我们忽略了一个问题,那就是版本问题。我们知道,代码是不断演化的,而序列化的对象可能是持久保存在文件上的,如果类的定义发生了变化,那持久化的对象还能反序列化吗?
默认情况下,Java会给类定义一个版本号,这个版本号是根据类中一系列的信息自动生成的。在反序列化时,如果类的定义发生了变化,版本号就会变化,与流中的版本号对比就会不匹配,反序列化就会抛出InvalidClassException异常。
通常情况下,我们希望自定义这个版本号,而非让Java自动生成,一方面是为了更好的控制,另一方面是为了性能,因为Java自动生成的性能比较低,那怎么自定义呢?在类中定义如下变量:

private static final long serialVersionUID = 1L;

在Java的IDE中,如果声明实现了Serializable而没有定义该变量,IDE会提示自动生成,这个变量的值是任意的,代表该类的版本号。在序列化时,会将该值写入流,在反序列化时,会将流中的值与类定义中的值进行比较,如果不匹配,会抛出InvalidClassException。
那如果版本号一样,但实际字段不匹配呢?Java会分情况自动进行处理,以尽量保持兼容性,大概分为三种情况:
- 字段删掉了:即流中有该字段,而类中没有,该字段会被忽略
- 新增了字段:即类定义中有,而流中没有,该字段会被设为默认值
- 字段类型改变了:对于同名的字段,类型变了,会抛出InvalidClassException

序列化特点分析

序列化的主要用途有两个,一个是对象持久化,另一个是跨网络的数据交换、远程过程调用。

Java标准的序列化机制有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便的进行定制,处理版本问题等,但他还是有一些局限性:
- Java序列化格式是一种私有格式,是一种Java语言特有的技术,不能被其他语言识别,不能实现跨语言的数据交换。
- Java在序列化字节中保存了很多描述信息,使得序列化格式比较大
- Java的默认序列化使用反射分析遍历对象结构,性能比较低
- Java的序列化格式是二进制的,不方便查看和修改
对于这些局限性,实践中旺旺会使用一些替代方案。在跨语言的数据狡猾格式中,XML/JSON是被广泛采用的文本格式,各种语言都有对他们的支持,文件格式清晰已读,有很多查看和编辑工具,他们的不足之处是性能和序列化大小,在性能和大小敏感领域,往往会采用更为精简高效的二进制方式如ProtoBuf,Thrift,MessagePack等。

### 序列化的含义 序列化指将堆内存中的 Java 对象数据,通过某种方式存储到磁盘文件中,或者传递给其他网络节点(网络传输),即将对象转化为二进制,用于保存或网络传输。反序列化则是从 IO 流中恢复对象[^1][^4]。 ### 序列化的意义 序列化机制允许将实现序列化Java 对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在[^1]。 ### 序列化的使用场景 所有可在网络上传输的对象都必须是可序列化的,比如 RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的 Java 对象都必须是可序列化的。通常建议程序创建的每个 JavaBean 类都实现 Serializable 接口[^1]。 ### 序列化实现的方式 #### Serializable 接口 - **普通序列化**:实现 Serializable 接口后,可使用 ObjectOutputStream 将对象写入流,使用 ObjectInputStream 从流中读取对象。 ```java import java.io.*; class Person implements Serializable { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } } public class SerializationExample { public static void main(String[] args) { Person person = new Person("John", 30); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) { oos.writeObject(person); } catch (IOException e) { e.printStackTrace(); } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) { Person deserializedPerson = (Person) ois.readObject(); System.out.println(deserializedPerson.name + " " + deserializedPerson.age); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } ``` - **成员是引用的序列化**:若类的成员是引用类型,该引用类型也必须是可序列化的,否则当前类无法实现序列化。 - **同一对象序列化多次的机制**:Java 序列化算法会保证同一对象多次序列化时,不会重复序列化对象的内容,而是只记录对象的引用。 - **Java 序列化算法潜在的问题**:例如序列化版本号不匹配、性能问题等。 - **可选的自定义序列化**:可通过实现 `writeObject` 和 `readObject` 方法来自定义序列化和反序列化过程。 #### Externalizable 接口 Externalizable 接口强制自定义序列化,需要实现 `writeExternal` 和 `readExternal` 方法。 ```java import java.io.*; class Employee implements Externalizable { String name; int id; public Employee() {} public Employee(String name, int id) { this.name = name; this.id = id; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(id); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); id = in.readInt(); } } ``` ### 序列化版本号 serialVersionUID serialVersionUID 用于确保序列化和反序列化时类的版本一致性。若没有显式定义 serialVersionUID,Java 会根据类的结构自动生成一个。当类的结构发生变化时,可能会导致 serialVersionUID 改变,从而在反序列化时抛出 `InvalidClassException`。建议显式定义 serialVersionUID。 ```java import java.io.Serializable; class MyClass implements Serializable { private static final long serialVersionUID = 1L; // 类的成员和方法 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值