java
实现序列化必须要实现的接口。
参考资料
Java
序列化是指把 Java
对象转换为字节序列的过程,而 Java
反序列化是指把字节序列恢复为 Java
对象的过程。
实现序列化
- 直接实现
Serializable
接口(默认序列化机制),则可以按照以下方式进行序列化和反序列化
1 | ObjectOutputStream采用默认的序列化方式,对该类对象的非transient的实例变量进行序列化。 |
如果仅仅只是让某个类实现 Serializable接口
,而没有其它任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。
- 实现
Serializable
接口,并且类中定义了readObject(ObjectInputStream in)
和writeObject(ObjectOutputSteam out)
,则采用以下方式进行序列化与反序列化。
1 | ObjectOutputStream调用对象的writeObject(ObjectOutputStream out)的方法进行序列化。 |
- 若类实现了
Externalnalizable接口
,且类必须实现readExternal(ObjectInput in)
和writeExternal(ObjectOutput out)
方法,则按照以下方式进行序列化与反序列化。
1 | ObjectOutputStream调用对象的writeExternal(ObjectOutput out))的方法进行序列化。 |
案例
只要一个类实现 Serializable
接口,那么这个类就可以序列化了。
例如有一个 Person
类,实现了 Serializable
接口,那么这个类就可以被序列化了。
1 | class Person implements Serializable{ |
通过ObjectOutputStream 的writeObject()方法把这个类的对象写到一个地方(文件),再通过ObjectInputStream 的readObject()方法把这个对象读出来。
1 | File file = new File("file"+File.separator+"out.txt"); |
输出结果为:
1 | name:tom age:22 |
结果完全一样。如果我把 Person
类中的 implements Serializable
去掉,Person
类就不能序列化了。此时再运行上述程序,就会报 java.io.NotSerializableException
异常。
serialVersionUID
注意到上面程序中有一个 serialVersionUID
,实现了 Serializable
接口之后。
序列化 ID 提供了两种生成策略
- 一个是固定的 1L
- 一个是随机生成一个不重复的
long
类型数据(实际上是使用 JDK 工具,根据类名、接口名、成员方法及属性等来生成)
上面程序中,输出对象和读入对象使用的是同一个 Person类
。
如果是通过网络传输的话,如果 Person类
的 serialVersionUID
不一致,那么反序列化就不能正常进行。例如在 客户端A
中 Person类
的 serialVersionUID=1L
,而在 客户端B
中 Person类
的 serialVersionUID=2L
那么就不能重构这个 Person对象
。
客户端A
中的 Person
类:
1 | class Person implements Serializable{ |
客户端B
中的 Person类
:
1 | class Person implements Serializable{ |
试图重构就会报java.io.InvalidClassException
异常,因为这两个类的版本不一致,local class incompatible
,重构就会出现错误。
如果没有特殊需求的话,使用用默认的 1L
就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID
有什么作用呢,有些时候,通过改变序列化 ID
可以用来限制某些用户的使用。
静态变量序列化
串行化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符都不能保存。
如果把 Person类
中的 name
定义为 static
类型的话,试图重构,就不能得到原来的值,只能得到 null
。说明对静态成员变量值是不保存的。这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
transient关键字
经常在实现了 Serializable接口
的类中能看见 transient关键字
。这个关键字并不常见。
transient
关键字的作用是:阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
当某些变量不想被序列化,同是又不适合使用 static
关键字声明,那么此时就需要用 transient
关键字来声明该变量。
例如用 transient
关键字 修饰 name变量
1 | class Person implements Serializable{ |
在反序列化视图重构对象的时候,作用与 static
变量一样: 输出结果为:
name:null age:22
在被反序列化后,transient
变量的值被设为初始值,如 int
型的是 0
,对象型的是 null
。
注:对于某些类型的属性,其状态是瞬时的,这样的属性是无法保存其状态的。例如一个线程属性或需要访问IO、本地资源、网络资源等的属性,对于这些字段,我们必须用 transient
关键字标明,否则编译器将报措。
序列化中的继承问题
- 当一个父类实现序列化,子类自动实现序列化,不需要显式实现
Serializable
接口。 - 一个子类实现了
Serializable 接口
,它的父类都没有实现Serializable 接口
,要想将父类对象也序列化,就需要让父类也实现Serializable 接口
。
第二种情况中:如果父类不实现 Serializable接口
的话,就需要有默认的无参的构造函数。这是因为一个 Java
对象的构造必须先有父对象,才有子对象,反序列化也不例外。在反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。在这种情况下,在序列化时根据需要在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int
型的默认是 0
,string
型的默认是 null
。
例如:
1 | class People{ |
在一端写出对象的时候
Person person = new Person(10,"tom", 22); //调用带参数的构造函数num=10,name = "tim",age =22
System.out.println(person);
oos.writeObject(person); //写出对象
在另一端读出对象的时候
Person person = (Person)ois.readObject(); //反序列化,调用父类中的无参构函数。
System.out.println(person);
输出为
num:0 name:tom age:22
发现由于父类中无参构造函数并没有对num初始化,所以num使用默认值为0。