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

找不到对象,那就 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 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中

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

  • 在对对象执行writeObject后,即使将对象进行修改,然后再执行writeObject。这时被修改后的对象不会被再次序列化。也就是说,只有第一次调用writeObject方法来输出对象时才会把对象转换成字节序列,并写入到ObjectOutputStream。

如何实现自定义序列化

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
image.png

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是怎么被调用的


标题:序列化 | 谈一个不分手的对象
作者:amore
地址:HTTPS://iamwaiting.cn/articles/2020/02/27/1582815765312.html
彧言:  正在加载今日诗词....

评论

取消