Java 中序列化机制的学习总结

本文详细介绍了Java中的序列化机制,包括序列化的概念、如何正确使用序列化机制、序列化过程中可能遇到的问题及解决方案。同时对比了实现Serializable接口与Externalizable接口在性能上的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、什么是序列化

java 中的序列化( serialization )机制能够将壹個实例对象的状态信息写入到壹個字节流中,使其可以通过 socket 进行传输、或者持久化 存储到数据库或文件系统中;然后在需要的时候,可以根据字节流中的信息来重构壹個相同的对象。序列化机制在 java 中有着广泛的应用,EJB、RMI 等技术都是以此为基础的。

2、正确使用序列化机制

壹般而言,要使得壹個类可以序列化,只需简单实现 java.io.Serializable 接口即可。该接口是壹個标记式接口,它本身不包含任何内容,实现了该接口则表示这個类准备支持序列化的功能。如下例定义了类 Person,并声明其可以序列化。

public class Person implements java.io.Serializable {}
序列化机制是通过 java.io.ObjectOutputStream 类和 java.io.ObjectInputStream 类来实现的。在序列化( serialize )壹個对象的时候,会先实例化壹個 ObjectOutputStream 对象,然后调用其 writeObject() 方法;在反序列化( deserialize )的时候,则会实例化壹個 ObjectInputStream 对象,然后调用其 readObject() 方法。下例说明了这壹过程。
//序列化 Person 类的实例对象
public void serializeObject(){
     String fileName = "ser.out";
     FileOutputStream fos = new FileOutputStream(fileName);
     ObjectOutputStream oos = new ObjectOutputStream(fos);
     oos.writeObject(new Person());
     oos.flush();
}

//反序列化 Person 类的实例对象
public void deserializeObject(){
     String fileName = "ser.out";
     FileInputStream fos = new FileInputStream(fileName);
     ObjectInputStream ois = new ObjectInputStream(fos);
     Person p = ois.readObject();
}
上例中我们对壹個 Person 对象定义了序列化和反序列化的操作。但如果 Person 类不能被序列化,即对不能序列化的类进行序列化操作,则会抛出 java.io.NotSerializableException 异常。
JVM中有壹個预定义的序列化实现机制,即默认调用 ObjectOutputStream.defaultWriteObject() 和 ObjectInputStream.defaultReadObject() 来执行序列化操作。如果想自定义序列化的实现,则必须在继承了接口 java.io.Serializable 的类中实现 writeObject() 和 readObject() 方法。
public class Person implements java.io.Serializable {
	public final void writeObject(Object obj) throws IOException {
		//...
	}
	
	public final Object readObject() throws IOException, ClassNotFoundException {
		//...
	}
}

3、三种使用序列化机制的情况介绍

壹般在序列化壹個类 A 的时候,有以下三种情况:
(1)类 A 没有父类,自己实现了 Serializable 接口;
(2)类 A 有父类 B,且父类实现了 Serializable 接口;
(3)类 A 有父类 B,但父类没有实现 Serializable 接口;
对于第壹种情况,直接实现 Serializable 接口即可。
对于第二种情况,因为父类 B 已经实现了 Serializable 接口,故类A无需实现此接口;如果父类实现了 writeObject() 和 readObject(),则使用此方法,否则直接使用默认的机制。
对于第三种情况,则必须在类A中显示实现 writeObject() 和 readObject() 方法来处理父类 B 的状态信息;还有壹点要特别注意,在父类B中壹定要有壹個无参的构造函数,这是因为在反序列化的过程中并不会使用声明为可序列化的类 A 的任何构造函数,而是会调用其没有申明为可序列化的父类 B 的无参构造函数。

4、传统序列化机制存在的壹些问题

(1)性能问题
为了序列化类 A 的壹個实例对象,所需保存的全部信息如下:
1. 与此实例对象相关的全部类的元数据(metadata)信息;因为继承关系,类A的实例对象也是其任一父类的对象。因而,需要将整个继承链上的每一个类的元数据信息,按照从父到子的顺序依次保存起来。
2. 类A的描述信息。此描述信息中可能包含有如下这些信息:类的版本ID(version ID)、表示是否自定义了序列化实现机制的标志、可序列化的属性的数目、每个属性的名字和值、及其可序列化的父类的描述信息。
3. 将实例对象作为其每一个超类的实例对象,并将这些数据信息都保存起来。
在RMI等远程调用的应用中,每调用一个方法,都需要传递如此多的信息量;久而久之,会对系统的性能照成很大的影响。

(2)版本信息
当用 readObject() 方法读取一个序列化对象的字节流信息时,会从中得到所有相关类的描述信息以及示例对象的状态数据;然后将此描述信息与其本地要构造的类的描述信息进行比较,如果相同则会创建壹個新的实例并恢复其状态,否则会抛出异常。这就是序列化对象的版本检测。Java 虚拟机默认的描述信息是使用壹個长整型的哈希码( hashcode )值来表示,这个值与类的各个方面的信息有关,如类名、类修饰符、所实现的接口名、方法和构造函数的信息、属性的信息等。因而,一个类作一些微小的变动都有可能导致不同的哈希码值。例如开始对一个实例对象进行了序列化,接着对类增加了一个方法,或者更改了某个属性的名称,当再想根据序列化信息来重构以前那个对象的时候,此时两个类的版本信息已经不匹配,不可能再恢复此对象的状态了。要解决这个问题,可能在类中显示定义一个值,如下所示:

private static final long serialVersionUID = ALongValue;

通过这样做,序列化机制会使用这个值来作为类的版本标识符,从而可以解决不兼容的问题。但是它却引入了壹個新的问题:如果壹個类作了实质性的改变,如增加或删除了壹些可序列化的属性,在这种机制下,JVM 仍然会认为这两个类是相等的。话说我们在 eclipse 集成开发环境下编码时,如果创建壹個新的类声明,经常会遇到与这個相关的警告信息,内容为:The serializable class * does not declare a static final serialVersionUID field of type long,自动提示中给出的解决的办法就是加上上面这句话。

5、实现序列化机制的更好选择

作为实现 java.io.Serializable 接口的壹种替代方案,实现 java.io.Externalizable 接口同样可以标识壹個类为可序列化。 java.io.Externalizable 接口中定义了以下两个方法:

public void readExternal(ObjectInput in);
public void writeExternal(ObjectOutput out);

这两个方法的功能与 readObject() 和 writeObject() 方法相同,任何实现了 Externalizable 接口的类都需要这实现两个函数来定义其序列化机制。
使用Externalizable比使用Serializable有着性能上的提高。前者序列化壹個对象,所需保存的信息比后者要小,对于后者所 需保存的第三个方面的信息,前者不需要访问每一个父类并使其保存相关的状态信息,而只需简单地调用类中实现的writeExternal() 方法即可。

转载于:https://my.oschina.net/bairrfhoinn/blog/93797

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值