序列化 | 谈一个不分手的对象

本贴最后更新于 1485 天前,其中的信息可能已经东海扬尘

找不到对象,那就 new 一个吧!什么?你怕她记不住你们的点点滴滴?怕分手?怕撒怕呀,有序列化啊!

序列化

序列化是将对象转换为可传输格式的过程。 是一种数据的持久化手段。

Java 中的序列化

在 java 中我们可以创建一个对象。这个对象跟随程序的启动而存在,程序的结束而消亡。即对象的生命周期不会比 jvm 的生命周期更长。但在很多应用中,会要求 jvm 停止以后能够保存指定的对象,并在需要的时候重新读取被保存的对象。java 对象序列化就能够实现这一功能。

如何对 Java 对象进行序列化和反序列化

在 Java 中,一个类能被序列化的前提是,实现了 java.io.Serializable 接口(可序列化类的所有子类型本身都是可序列化的)。代码示例如下:
GirlFriend 类,用于序列化和反序列化

package geektomya;

import java.io.Serializable;
import java.util.Date;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 16:01
 * @description
 */
public class GirlFriend implements Serializable {

    private String name;

    private Integer age;

    private transient Date birthday;

    private static final long serialVersionUID = 8639051849453497849L;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }
}

SerializationDemo 类,进行序列化

package geektomya;


import java.io.*;
import java.util.Date;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 16:07
 * @description
 */
public class SerializationDemo {

    public static void main(String[] args) {
        // 创建一个对象用于序列化
        GirlFriend girlFriend = new GirlFriend();
        girlFriend.setName("MyGirlFriend");
        girlFriend.setAge(18);
        girlFriend.setBirthday(new Date());
        GirlFriend.love = "geektomya";
        System.out.println("序列化前的对象:"+girlFriend+" static love :"+girlFriend.love);

        // 将girlFriend序列化
        try( ObjectOutputStream  oos = new ObjectOutputStream(new FileOutputStream("girlFriend"))) {
            oos.writeObject(girlFriend);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
    }
}
/* output:
* 序列化前的对象:GirlFriend{name='MyGirlFriend', age=18, birthday=Wed Mar 04 17:04:09 CST 2020}
* static love :geektomya
* */

SerializationDemo 类,进行反序列化

package geektomya;


import java.io.*;
import java.util.Date;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 16:07
 * @description
 */
public class SerializationDemo {

    public static void main(String[] args) {
        // 将girlFriend反序列化
        FileInputStream fis = null;
        try {
           fis = new FileInputStream("girlFriend");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        try(ObjectInputStream ois = new ObjectInputStream(fis)){
            GirlFriend myGirlFriend = (GirlFriend)ois.readObject();
            System.out.println("反序列化的对象:"+myGirlFriend+" static love :"+myGirlFriend.love);
        }catch (IOException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}
/* output
* 反序列化的对象:GirlFriend{name='MyGirlFriend', age=18, birthday=null} 
* static love :Me
* */

输出对比

girlFriend 对象被序列化的时候的值与反序列化得到的对象 myGirlFriend 的值进行对比可以发现:birthday 字段的值和 love 字段的值不一样。还有
之所以会出现这个原因是因为在 GirlFriend 类中 birthdaytransient 修饰,而 lovestatic 修饰。

  • Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  • static 关键字修饰的变量属于类变量,而序列化是将对象的状态进行序列化,所以序列化的时候是不会将 static 修饰的字段进行序列化。

总结

  • 在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化

  • 通过 ObjectOutputStreamObjectInputStream 对对象进行序列化及反序列化

  • 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID),如果反序列化时两个类的 serialVersionUID 不一致,那么就会失败。

  • 序列化并不保存静态变量。

  • Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中

  • 反序列化后对象的全类名(包名 + 类名)需要和序列化之前对象的全类名一致

如何实现自定义序列化

1.被序列化的类中增加 writeObject 和 readObject 方法

在序列化和反序列化过程中,如果被序列化的类中定义了 writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

ArrayList 的自定义序列化

在 ArrayList 中,他的定义如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;
    /*
     * 省略其他成员
     */
}

其中可以看到 elementData(就是用来保存列表中的元素的)被 transient 修饰,那么意味着在 ArrayList 中的元素不能被序列化。然而事实并不这样,ArrayList 中的元素能被序列化,因为在 ArrayList 中自定义了 writeObject 和 readObject 方法。

那么为什么 ArrayList 要 transient 后再自定义序列方式呢?

  • ArrayList 实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为 100,而实际只放了一个元素,那就会序列化 99 个 null 元素。为了保证在序列化的时候不会将这么多 null 同时进行序列化,ArrayList 把元素数组设置为 transient。
  • 为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList 使用 transient 来声明 elementData。 但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写 writeObjectreadObject 方法的方式把其中的元素保留下来。
writeObject 和 readObject 如何被调用的?

都是通过 ObjectXxxxxStream 来调用,以 ObjectInputStream 为例,图示如下:
20170927193034672.png

也就是调用栈为:
readObject ---> readObject0 --->readOrdinaryObject--->readSerialData--->invokeReadObject
其中在这里发挥作用的是:invokeReadObject,其中 readObjectMethod.invoke(obj, new Object[]{ in }); 是关键,通过反射的方式调用 readObjectMethod 方法。官方是这么解释这个 readObjectMethod 的:

class-defined readObject method, or null if none

结论:通过反射自动调用的。

2.被序列化的类中增加增加 writeReplace 和 readResolve 方法。

在序列化和反序列化过程中,如果被序列化的类中定义了 writeReplace 和 readResolve 方法,那么在序列化过程中就会调用 writeReplace 方法,实际序列化得到的对象将是 writeReplace 方法返回值的对象;在反序列化过程中就会调用 readResolve 方法,实际反序列化得到对象将是作为 readResolve 方法返回值的对象。

writeReplace

这里需要注意的是 writeReplace 中返回的对象如果不是本类对象,那么返回的对象那个类也应该实现 java.io.Serializable 接口,然后在反序列化的时候,需要用返回的对象那个类来接收

  • 如果返回的对象那个类没有实现 java.io.Serializable 接口,那么序列化的时候将会报 java.io.NotSerializableException
  • 如果反序列化的时候,没有用返回的对象那个类来接收,那么反序列化就会报 java.lang.ClassCastException
  • 代码示例如下:
    writeReplace 方法中返回其他类对象的 Demo1 类
package geektomya;

import java.io.Serializable;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 21:52
 * @description
 */
public class Demo1 implements Serializable {
    public String Demo1_Name;
    private static final long serialVersionUID = 4741659151754920854L;

    public Object writeReplace(){
        return new Demo2();
    }
   //  省略的getter and setter
}

writeReplace 方法中返回的 Demo2 类

package geektomya;

import java.io.Serializable;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 21:53
 * @description 实现了Serializable接口
 */
public class Demo2  implements Serializable {
}

测试类

package geektomya;

import java.io.*;

/**
 * @author yaoqiuhong
 * @create 2020-02-28 22:00
 * @description
 */
public class SerializableDemo2{
    //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
    //Exception直接抛出
    public static void main(String[] args)  throws FileNotFoundException, IOException {
        Demo1 demo1 = new Demo1();
        demo1.setDemo1_Name("Demo1");
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo1"));
        // 由于Demo1类中的writeReplace方法返回Demo2对象,如果Demo2没实现Serializable接口,将会抛出NotSerializableException
        oos.writeObject(demo1);

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("demo1"));

        try {
            /**
             * 此处会抛出ClassCastException,因为前面的序列化得到的对象是Demo2
             * 此处正确的写法是:Demo2 demo22 = (Demo2) ois.readObject();
             * */
            Demo1 demo11 = (Demo1) ois.readObject();
        } catch (ClassNotFoundException e) {
            System.out.println("异常错误java.lang.ClassNotFoundException");
        }
    }
}

readResolve

readResolve 注意这里如果返回的对象不是本类的对象,那么再反序列化的时候,也需要用与 readResolve 返回的对象一直的类对象来接受,否则同样会出现 java.lang.ClassCastException

3.通过实现 Externalizable 接口。

Externalizable 继承了 Serializable,该接口中定义了两个抽象方法:writeExternal()与 readExternal()。当使用 Externalizable 接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal()与 readExternal()方法。
所以可以在 writeExternal()与 readExternal()方法中进行自定义的序列化和反序列化。

  • 如果没有在这两个方法中定义序列化实现细节,那么序列化之后,对象内容为空。
  • 类必须存在一个 pulic 的无参数构造方法
  • 反序列化时的字段属性需要与序列化时一致,否则值顺序错乱。(Serializable 中如果定义了 serialVersionUID 且一直的话能顾自动兼容自动的增减)

为什么实现了 Serializable 接口就可以序列化

Serializable 明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?**
这个问题可以从 ObjectOutputStream 的 writeObject 的调用栈来回答:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
writeObject0 方法中有这么一段代码:

if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }

在进行序列化操作时,会判断要被序列化的类是否是 Enum、Array 和 Serializable 类型,如果不是则直接抛出 NotSerializableException


本文参考
深入分析 Java 的序列化与反序列化-HollisChuang's Blog
序列化与自定义序列化
JAVA 对象流序列化时的 readObject,writeObject,readResolve 是怎么被调用的

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖
  • Serializable
    2 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...