转载

Java对象的序列化和反序列化

我们在科幻电影中,经常能够看到“瞬间传送”这种神奇的科技。在生活中,尤其是在上下班和春节回家的时候,我也是真的想体验一下“瞬间传送”啊!从科学角度来讲,“瞬间传送”的本质应该是先将被传送的物质,分解为物质的最小单位夸克(已知最小),然后将这些夸克传送到目标位置,再根据夸克之前的排列顺序进行重组,最后被传送的物质就这样被重构出现在很遥远的另一个地方了。

在Java中,也有一种类似的“瞬间传送”,它就是对象的序列化和反序列化。之所以这么说,是因为序列化就像是传送前,对被序列化对象进行分解为字节单位、并传送至指定位置的过程(磁盘中指定的位置);而反序列化就是将距离很遥远的目标(磁盘中序列化对象文件的位置)进行分解传送到眼前,然后进行根据顺序重新排列组合字节码,将其恢复为该对象的过程。

目录:

  1. 序列化和反序列化的定义
  2. 序列化的意义
  3. 序列化和反序列化的用途
  4. 序列化和反序列化的使用场景
  5. 用于序列化的接口(Serializable和Externalizable)
  6. 同一对象多次序列化
  7. 序列化版本号SerializVersionUID

一.序列化和反序列化的定义:

  • 序列化:把对象转换为字节序列的过程(将对象写入到IO流中)。
  • 反序列化:把字节序列转换为对象的过程(从IO流中读取对象)。

二.序列化的意义:

将需要保存的对象持久化到磁盘上,使得对象可以脱离运行程序而独立存在,在需要的时候通过反序列化重新恢复为对象。

三.序列化和反序列化的用途:

1. 对象持久化:可通过对象序列化将对象永久保存到磁盘上,通常是保存到文件上

2. 网络传输:通过将对象转换为字节序列,可以将其从主机A传输到主机B上,然后在主机B上反序列化重构出对象

四.序列化和反序列化的使用场景:

所有可在网络上传输的对象都必须是可序列化的,如远程方法调用时,所有的参数对象都必须是可序列化的, 否则会出现异常 。所以我们在 创建Bean类的时候,最好都实现Serializable序列化接口

五.用于序列化的接口:

在Java中,想要 实现序列化有两种方式 ,分别是 实现Serializable接口(自动序列化)或实现Externalizable接口(手动强制序列化)

5.1.Serializable接口:

/**
 * 学生类 实现Serializable序列化接口
 * 用于验证在对象序列化时,瞬时变量和静态变量是否能够被持久化的问题
 * 瞬时变量:被transient修饰的变量,其生命周期在调用者内存中,无法被序列化(持久化),在序列化时其值会丢失
 * 静态变量:被static修饰的变量,其已经属于该类,不再属于类对象,所以即便是没有被transient修饰,其也无法被持久化
 */
public class Student implements Serializable {
    /**
     * 当类实现了Serializable接口时,类中所有的元素都将自动序列化,无需手动添加
     */

    private static transient String studentID;//编号  非瞬时变量(被static修饰的变量,无论是否被transient修饰,都不会被序列化;用于验证,不用在意)
    private String studentName;//姓名
    private String studentAge;//年龄
    private String studentAddress;//住址
    private transient String studentIDCard;//身份证号 瞬时变量(transient修饰),在序列化的时候不会被持久化到磁盘上
    private transient String studentPhone;//联系方式  瞬时变量一般用于用户敏感信息安全,避免在持久化的时候泄露
    private String studentAccount;//分数


    public Student(String name, String studentName, String studentAge, String studentAddress, String studentIDCard, String studentPhone, String studentAccount) {
        this.studentName = studentName;
        this.studentAge = studentAge;
        this.studentAddress = studentAddress;
        this.studentIDCard = studentIDCard;
        this.studentPhone = studentPhone;
        this.studentAccount = studentAccount;
    }

    //省略get()和set()方法

    @Override
    public String toString() {
        return "Student{" +
                "studentName='" + studentName + '/'' +
                ", studentAge='" + studentAge + '/'' +
                ", studentAddress='" + studentAddress + '/'' +
                ", studentIDCard='" + studentIDCard + '/'' +
                ", studentPhone='" + studentPhone + '/'' +
                ", studentAccount='" + studentAccount + '/'' +
                '}';
    }
}

实现Serializable接口的时候不用重写任何方法,即可直接自动序列化(无需手动指定需要序列化的元素,被transient修饰的 瞬态变量 和被static修饰的静态变量除外) 。在使用Serializable接口序列化的时候,先创建一个 ObjectOutputStream 输入流对象并指定文件路径,然后将需要序列化的类对象放入输入流对象的 writeObject()方法 即可。

/**
 * 序列化工具类
 * 用于类对象持久化
 */
public class SerializationUtil {
    /**
     * 序列化
     */
    public static void writeObject(String fileName, Serializable serializable){
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
            oos.writeObject(serializable);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 反序列化
     */
    public static Object readObject(String fileName){
        Object object = null;
        try {
            ObjectInput oi = new ObjectInputStream(new FileInputStream(fileName));
            object = oi.readObject();
            oi.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e){
            e.printStackTrace();
        }
        return object;
    }
}

编写测试类进行测试:

/**
 * 学生类测试类
 * 用于验证Serializable序列化时,瞬时变量和静态变量的取值问题
 */
public class SerializationTest {
    public static void main(String[] args) {
        Student student = new Student("1","张三","22",
                "上海市浦东新区","410804199803120341",
                "13763412233","89");
        //序列化
        SerializationUtil.writeObject("E:/programming/serializTest.txt",student);
        //反序列化
        Student sd = (Student) SerializationUtil.readObject("E:/programming/serializTest.txt");

        //studentID为静态变量,不可被持久化,studentIDCard和studentPhone为瞬时变量,不可持久化
        System.out.println(sd.toString());
    }
}

运行结果:

Java对象的序列化和反序列化

注意:

  • 虽然Serializable接口是自动序列化的,但是对于 被transient关键字修饰的瞬态变量 ,和 被static关键字修饰的静态变量是无法自动序列化的瞬态变量在序列化的时候会被忽略掉,而静态变量是无法序列化的(无论是否被transient关键字修饰)
  • 值得一提的是, 瞬态变量 会在序列化的时候被忽略掉,这样 会被完全隔离在序列化之外(因此不推荐使用transient关键字) ,为了能够自定义序列化,我们 可以提供无参构造函数,并重写writeObject(ObjectOutput out)方法和readObject (ObjectInput ins)方法 ,重写这两个方法的时候 不需要添加@Override注解(会报错) ,使用方法同Externalizable接口重写的两个方法。区别在于,Serializable接口是不强制重写这两个方法的。

创建一个Peoplo类,用于验证serializable接口中的自定义序列化

/**
 * 普通人类     实现Serializable序列化接口
 * 用于验证Serializable中的自定义序列化方式
 */
public class People implements Serializable {
    private static String peopleID;//编号(stataic纯粹用于验证自定义序列化时,静态变量的序列化,不用在意)
    private String peopleName;//姓名
    private int age;//年龄
    private String peopleAddress;//住址
    private transient String peoplePhone;//联系方式 瞬态变量

    //无参构造 用于反序列化时反射构造对象
    public People() {
    }
    //有参构造,用于初始化对象
    public People(String peopleName, int age, String peopleAddress, String peoplePhone) {
        this.peopleName = peopleName;
        this.age = age;
        this.peopleAddress = peopleAddress;
        this.peoplePhone = peoplePhone;
    }

    //省略get()方法和set()方法

    @Override
    public String toString() {
        return "People{" +
                "peopleID='" + peopleID + '/'' +
                ", peopleName='" + peopleName + '/'' +
                ", age=" + age +
                ", peopleAddress='" + peopleAddress + '/'' +
                ", peoplePhone='" + peoplePhone + '/'' +
                '}';
    }

    //重写writeObject(ObjectOutputStream out)和readObject(ObjectInputStream ins)方法
    private void writeObject(ObjectOutputStream out) throws IOException{
        out.writeObject(this.peopleName);
        out.writeInt(this.getAge());
        out.writeObject(this.peoplePhone);
    }
    
    private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
        this.peopleName = (String)ins.readObject();
        this.age = ins.readInt();
        this.peoplePhone = (String)ins.readObject();
    }
}

编写测试类PeopleTest

/**
 * 普通人类测试类  用于验证Serializable中的自定义序列化方式
 */
public class PeopleTest {
    public static void main(String[] args) {
        People people = new People("王二麻子",45,"上海市闵行区","15567341990");

        //序列化对象
        SerializationUtil.writeObject("E:/programming/serializTest.txt",people);
        //反序列化对象
        People pp = (People) SerializationUtil.readObject("E:/programming/serializTest.txt");

        System.out.println(pp.toString());
    }
}

运行结果:

Java对象的序列化和反序列化

解释:虽然peoplePhone属性是被transient关键字修饰的瞬态变量,但是因为我们在重写的writeObject(ObjectOutputStream out)和readObject(ObjectInputStream ins)方法中,指定了peoplePhone属性需要被强制序列化,所以在运行结果中,peoplePhone仍旧可以取到值。而没有被transient关键字修饰的peopleAddress属性,正常来说是能被序列化的,但是在重写的两个方法中没有定义该属性要被强制序列化和反序列化,所以我们在运行结果中取不到值(自动显示String类型的默认值)。

  • 如果想要 彻底自定义序列化 ,我们还 可以重写writeReplace()方法和readResolve()方法 ,因为在序列化和反序列化的时候, 这两个方法一个在最前调用,一个在最后调用writeReplace()方法是用于彻底替换对象的方法,被替换后原对象无法恢复!readResolve()方法则一般是用于保护性恢复数据 ,例如防止因为内存空间不同(从程序堆栈中读取,和从磁盘中读取的值可能不一样),导致 枚举类型和常量类型 的值发生错乱,从而避免程序出错。
//这个方法会在序列化时,调用writeObject()方法之前调用;
//该方法和writeObject()方法不能同时写,会报错
private Object writeReplace() throws ObjectStreamException{
    ArrayList<Object> list = new ArrayList<>(4);
    list.add(this.peopleName);
    list.add(this.age);
    list.add(this.peopleAddress);
    list.add(this.peoplePhone);
    return list;
}

//这个方法会在反序列化时,调用readObject()方法之前调用,用于替换反序列化的对象,原解析对象会被立刻丢弃;
// 该方法和readObject()方法不能同时写,会报错
private Object readResolve() throws ObjectStreamException {
    return new People("张三丰子",33,"上海市黄浦区","17734349989");
}


//注意:writeObject(ObjectOutput out)方法和writeReplace()方法不能同时重写,同样,
//      readObject (ObjectInput ins)方法和readReplace()方法也不能同时重写,编译时会报错。
  • 如果需要被序列化的对象中,有的元素(属性)的值是引用类型的(不是基本类型和String类型),那么被引用的类也必须是实现了序列化接口,否则会出现异常(NoSerializableException)。

5.2.Externalizable接口:

创建Teacher类并实现Externalizable接口

/**
 * 教师类 实现Externalizable序列化接口
 * 用于验证在对象序列化时,瞬时变量和静态变量是否能够被持久化的问题,同Student类
 */
public class Teacher implements Externalizable {
    /**
     * 当类实现了Externalizable接口后,类中所有元素均不会自动序列化,需要在writeExternal()方法中指定
     * 但是和Serializable接口不同,该序列化接口下,transient修饰符修饰的变量可被序列化
     */
    private static transient String teacherID;//编号 非瞬时变量(被static修饰的变量,无论是否被transient修饰,都不会被序列化)
    private String teacherName;//姓名
    private String teacherAge;//年龄
    private String teacherAddress;//住址
    private transient String teacherIDCard;//身份证号 瞬时变量(transient修饰),在序列化的时候不会被持久化到磁盘上
    private transient String teacherPhone;//联系方式  瞬时变量一般用于用户敏感信息安全,避免在持久化的时候泄露

    //必须有无参构造器(否则会报无可用的构造器异常),用于在反序列化的时候反射创建对象
    public Teacher() {}

    public Teacher(String teacherName, String teacherAge, String teacherAddress, String teacherIDCard, String teacherPhone) {
        this.teacherName = teacherName;
        this.teacherAge = teacherAge;
        this.teacherAddress = teacherAddress;
        this.teacherIDCard = teacherIDCard;
        this.teacherPhone = teacherPhone;
    }

    //省略get()和set()方法

    @Override
    public String toString() {
        return "Teacher{" +
                "teacherName='" + teacherName + '/'' +
                ", teacherAge='" + teacherAge + '/'' +
                ", teacherAddress='" + teacherAddress + '/'' +
                ", teacherIDCard='" + teacherIDCard + '/'' +
                ", teacherPhone='" + teacherPhone + '/'' +
                '}';
    }


    //实现Externalizable接口的时候必须重写该接口的writeExternal(ObjectOutput out)方法和readExternal(ObjectInput in)方法。
    
    //writeExternal(ObjectOutput out):方法用于指定需要强制序列化的元素;
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //在此指定需要序列化的元素,使用反转加密方式
        out.writeObject(new StringBuffer(teacherName).reverse());
        out.writeObject(new StringBuffer(teacherAge).reverse());
        out.writeObject(new StringBuffer(teacherAddress).reverse());
        out.writeObject(new StringBuffer(teacherIDCard).reverse());//虽被transient修饰,但因在此指定,所以仍可序列化
        out.writeObject(new StringBuffer(teacherPhone).reverse());//虽被transient修饰,但因在此指定,所以仍可序列化
    }
    //readExternal(ObjectInput in):方法用于指定需要反序列化的元素;
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //在此指定需要反序列化的数据,需反转后取出
        this.teacherName = ((StringBuffer)in.readObject()).reverse().toString();
        this.teacherAge = ((StringBuffer)in.readObject()).reverse().toString();
        this.teacherAddress = ((StringBuffer)in.readObject()).reverse().toString();
        this.teacherIDCard = ((StringBuffer)in.readObject()).reverse().toString();
        this.teacherPhone = ((StringBuffer)in.readObject()).reverse().toString();
    }
}

测试类:

/**
 * 教师类测试类
 * 用于验证Externalizable序列化时,瞬时变量和静态变量的取值问题
 */
public class ExternalizableTest {
    public static void main(String[] args) {
        Teacher teacher = new Teacher("语文老师","33","上海市松江区",
                "410804198402120445","13723324546");

        //序列化
        SerializationUtil.writeObject("E:/programming/serializTest.txt",teacher);
        //反序列化
        Teacher tc = (Teacher) SerializationUtil.readObject("E:/programming/serializTest.txt");

        //虽然teahcer类中也有静态变量和瞬时变量,但是静态变量不可被序列化(异常),瞬时变量只要被指定,即可被强制序列化
        System.out.println(tc.toString());
    }
}

运行结果:

Java对象的序列化和反序列化

解释:虽然Teacher类中,也有被transient修饰的瞬态变量,但是因为Externalizable接口中所有需要序列化和反序列化的属性都需要手动指定,所以transient关键字相当于无效,因此我们可以看到反序列化读取对象的时候,所有的属性都被打印了出来。

  • Externalizable接口 和Serializable接口不同, 必须在writeExternal(ObjectOutput out)方法和readExternal(ObjectInput in)方法中,手动指定需要序列化和反序列化的对象元素,否则部分数据会无法序列化或部分数据无法反序列化,造成数据缺失 。这两个方法除了可以指定元素之外,还可以 用于属性的加密等,如常见的反转加密等操作
  • 而且 实现了Externalizable接口 之后,还 必须提供类的无参构造方法 ,用于在反序列化的时候反射构造对象,如果没有无参构造函数会报异常(无有效构造函数异常)。
    异常截图:
    Java对象的序列化和反序列化
  • 但是从本质上来说,Externalizable接口就是强制序列化的Serializable接口,其底层调用的方法是一样的。

六.同一对象的多次序列化

在Java中,如果要对统一对象进行多次序列化操作,那么结果就是:该对象只会被序列化一次。这是因为,在Java的序列化机制中,对象在序列化之后,会保存一个序列化编码,如果这个对象已经被序列化过了,那么会直接调用该编码,而不是重新序列化。

这样的操作有其 优点:避免同一对象被多次序列化,提高了性能;

但是也有其 缺点:如果该对象在被序列化之后,做出了修改,则再次序列化很有可能无法更新磁盘中的对象。

七.序列化版本号serializVersionUID

我们在使用 部分开发工具 的时候(如MyEclipse等),会提示我们要 重写Serializable接口的serialVersionUID静态常量属性 (也可以不重写,不会出现异常)。SerializVersionUID的作用在于,当我们在升级项目的时候,反序列化使用的class文件肯定也要升级,为了保证升级后序列化的兼容性,我们可以使用序列化版本号 serializVersionUID来保证序列化和反序列化的正确性 ,只要serializVersionUID相同,那么即便是我们更改了序列化的属性,我们也可以正确的反序列化。

private static final long serialVersionUID = 8294180014912103005L;

SerializVersionUID的生成是Java运行时环境根据类的内部细节自动生成的。若是 在没有显式定义SerializVersionUID的情况下,对类进行修改,则在反序列化的时候会抛出异常 ,这是因为我们在 修改类的时候,产生了新的SerializVersionUID(生成细节已改变),新旧SerializVersionUID不相同,反序列化的时候SerializVersionUID对不上,所以自然就出现了异常

需要更改SerializVersionUID的情况:

  • 若修改了方法、或静态变量、或瞬态变量,则无需修改SerializVersionUID;
  • 若修改了非瞬态变量、或修改了变量类型,则可能导致反序列化失败,需要更改SerializVersionUID;
  • 若新增了变量,则反序列化的时候返回的是默认值(String:null、int:0、boolean:false);若删减了变量,则反序列化的时候自动忽略缺失的变量。

强烈建议在实现了序列化接口的类中,显式定义SerializVersionUID,这样在修改了类中的属性和方法之后,反序列化也不会出错。

原文  https://www.maiyewang.com/archives/93312
正文到此结束
Loading...