1、序列化概述
对象序列化可以使对象保存在磁盘或发送到网络中,为了让类支持序列化,该类应该实现Serializable或Externalizable接口。Java中很多类已经实现了Serializable,Serializable是一个标记接口,实现该接口无需实现它的方法,建议每个JavaBean类都实现Serializable。
JavaBean是遵循一定编程原则的Java类的统称,由属性、方法和事件3部分组成,使用它可以实现代码的可重用性,保持类功能的向后兼容。JavaBean遵循的约定有:
①、所有的JavaBean必须放在一个包(Package)中。
②、JavaBean应该生成public class类,文件名称应该与类名称一致。
③、所有属性必须封装为private,设置、获取属性值应该通过一组方法:setter()、getter()、geXxx()、setXxx()、isXxx(),这样当修改该类后对外提供的接口无需改变。
④、Java Bean 类必须有不带参数的构造函数,这个构造器通过调用各个属性的设置方法来设置属性的默认值。
我们可以使用lombok这个小工具来简化代码编写,,lombok是一个可以通过简单的注解的形式来自动生成代码的工具,比如如下所示,对一个方法的参数添加@NonNull注解后,在编译的时候会为该参数添加为空的判断。再比如下所示,对一个类添加注解后就自动为该类添加了getter和setter方法(编译源码的时候自动生成这些方法)。
2、序列化流程
序列化一个对象的步骤:
①、该对象所属类实现Serializable接口。
②、创建一个ObjectOutputStream处理流,该处理流建立在一个节点流基础上。
③、调用ObjectOutputStream的writeObject()方法保存可序列化的对象。
反序列化一个对象的步骤:
①、提供该对象所属类的class文件,否则会引发ClassNotFoundException异常。
②、创建一个ObjectInputStream处理流,该处理流建立在一个节点流基础上。
③、调用ObjectInputStream的readObject()方法保存可序列化的对象
//Person.java
public class Person implements java.io.Serializable
{
private String name;
private int age;
public Person(){}
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
//WriteObject.java
import java.io.*;
public class WriteObject
{
public static void main(String[] args)
{
try(
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")))
{
Person per = new Person("孙悟空", 500);
oos.writeObject(per);
Person p = (Person)ois.readObject();
int age = p.getAge();
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
3、序列化需要注意的地方
反序列化机制不会通过构造器来初始化Java对象。
当调用writeObject()方法序列化一个对象后,改变该对象的成员变量的值再次调用writeObject()方法序列化该对象只会输出序列化编号,不会再次序列化该对象。
使用transient声明的成员变量(又称瞬态实例变量)不会被序列化,在反序列化的时候会得到其为0或null。
使用序列化机制写入了多个Java对象后,反序列化的时候应该与写入顺序一致。
可序列化类的父类也应该是可序列化的,如果父类是不可序列化的,但是带有无参数的构造器,那么父类中成员变量的值不会被序列化。
序列化对象的实例变量也应该是可序列化的,而方法、类变量不会被实例化。
4、自定义序列化
可以重写以下方法来自定义序列化和反序列化,其中writeObject()为重写序列化方法,readObject()可以重写反序列化方法,readObjectNoData()重写序列化流不完整时(接收方使用的反序列化类的版本不同于发送方、接收方版本扩展的类不是发送方版本扩展的类、序列化流被篡改)的方法,writeReplace()方法中可以返回指定对象来替换序列化的对象,readResolve()中返回指定的对象会替换掉反序列化的对象:
private void writeObject(java.io.ObjectOutputStream out)throws IOException;
private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;
private void readObjectNoData()throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace()throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve()throws ObjectStreamException;
使用writeObject()和readObject():
import java.io.*;
public class Person
implements java.io.Serializable
{
private String name;
private int age;
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name实例变量的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
// 将int类型的age写入二进制流
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name实例变量
this.name = ((StringBuffer)in.readObject()).reverse().toString();
// 将读取的int型赋给age实例变量
this.age = in.readInt();
}
}
使用writeReplace:
public class Person implements java.io.Serializable
{
private String name;
private int age;
// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace()throws ObjectStreamException
{
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
5、Externalizable
类实现Serializable接口后就可以序列化该类的对象,可以重写其方法来自定义序列化。类实现Externalizable接口的话也可以实现其对象的序列化,但必须实现该接口,即必须重写readExternal()和writeExternal()方法,因为这两个方法是空方法:
import java.io.*;
public class Person
implements java.io.Externalizable
{
private String name;
private int age;
public void writeExternal(java.io.ObjectOutput out)
throws IOException
{
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in)
throws IOException, ClassNotFoundException
{
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
6、序列化版本
前面说过,反序列化时应该提供该对象的class文件,如果反序列化之前该类已经修改(即与序列化的时候不同了)的话,反序列化可能会失败。应该通过指定private static final long serialVersionUid = xxx;来指示该类与序列化时的类属于同一版本:如果仅是修改了该类的方法、类变量、瞬态实例变量,则反序列化不会受影响,serialVersionUid值无需修改,指示两个类为同一版本。如果修改了类的实例变量,比如将实例变量的类型由int改为String,则反序列化会失败,此时应该修改serialVersionUid为新的值,指示两个类版本已不同。