美文网首页
Java梳理之理解序列化

Java梳理之理解序列化

作者: _小二_ | 来源:发表于2017-10-19 22:07 被阅读0次

说起来对象序列化的文章已经非常多了,这里记录下自己学习理解的思路,如果文中有任何问题请告知。

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,可能需要我们将对象脱离JVM,保存在字节流中并通过网络传输或者保存到磁盘或文件中,并在之后将它重建为一个存活的对象。这其中涉及到两点,即,序列化和反序列化。

定义

序列化:将对象的表示转换为字节流的过程。
反序列化:从字节流中重新构建一个对象的过程。

1.前置分析:

在前面字节流中有说过ObjectIuputStreamObjectOutputStream,这两个类有方法readObject()writeObject()可以序列化和反序列化一个完整的对象。这里以ObjectOutputStream序列化一个对象为例,查看源码如下:

/**
**序列化对象 writeObject源码示例
**/
public final void writeObject(Object obj) throws IOException {
    //尽管是final方法,但是通过enableOverride字段实现重写该方法
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        //序列化对象的主线方法
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
        writeFatalException(ex);
        }
        throw ex;
    }
}

可以看到其中writeObject0()方法才是真的序列化方法,继续往下看:

/**
**writeObject0源码示例
**/
private void writeObject0(Object obj, boolean unshared) 
    throws IOException 
    {
            **代码有删减**
        for (;;) {
        // REMIND: skip this check for strings/arrays?
        Class repCl;
        desc = ObjectStreamClass.lookup(cl, true);
        //通过序列化对象的对应的ObjectStreamClass实例,判断是否有WriteReplace方法
        if (!desc.hasWriteReplaceMethod() ||
            (obj = desc.invokeWriteReplace(obj)) == null ||
            (repCl = obj.getClass()) == cl)
        {
            break;
        }
        cl = repCl;
        }
            **代码有删减**
        // remaining cases
        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());
        }    
        }
            **代码有删减**
    }

在这里可以看到其实支持的对象类型有这么几种,StringArray数组、EnumSerializable接口,先看Object类型的方法writeOrdinaryObject(),如下:

 private void writeOrdinaryObject(Object obj, 
                     ObjectStreamClass desc, 
                     boolean unshared) 
    throws IOException 
    {
        if (extendedDebugInfo) {
        debugInfoStack.push(
        (depth == 1 ? "root " : "") + "object (class \"" + 
        obj.getClass().getName() + "\", " + obj.toString() + ")");
        }
        try {
        desc.checkSerialize();

        bout.writeByte(TC_OBJECT);
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
        writeExternalData((Externalizable) obj);
        } else {
        writeSerialData(obj, desc);
        }
    } finally {
            if (extendedDebugInfo) {
        debugInfoStack.pop();
        }  
        }
    }

这里存在判断if (desc.isExternalizable() && !desc.isProxy()),点看看过之后,大体上通过ObjectOutputStream序列化对象的源码就看完了,如果感兴趣的话,可以看看类似的ObjectInputStream反序列化源码,在这里就不继续放上来了。下面可以开始细说序列化操作了。

2.序列化

2.1默认序列化

在Java里,我们通过Serializable接口实现序列化操作,在这个接口中没有任何东西,看过源码之后就知道这个接口只是一个标识,在writeObject0方法中,如果对象不是StringArray数组、Enum或者没有实现Serializable接口,就会抛出异常NotSerializableException。现在可以看一个简单的示例,如下:

/**
**序列化示例
**/
public class SerializableDemo implements Serializable{
    
    private String name;
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setName("小二");
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }

}
输出:
SerializableDemo [name=小二]

其中,如果不实现Serializable接口,就像在上面源码写的那样,报异常:java.io.NotSerializableException: Test.SerializableDemo,所以这个接口确实不能缺少。这时候,我们就简单的序列化并持久化了一个对象,并把字段的值持久化下来。这种默认的序列化可以实现,却会存在一些限制,如果有些字段是不能被序列化并保存的呢?比如密码等。那我们就需要定制按需求的序列化类。

2.2 影响序列化机制

2.2.1 transient关键字

Java平台提供了这样一个关键字transient,通过它可以将字段表示成瞬时状态,就不会将这个字段值序列化了,如下:

/**
**transient关键字示例
**/
public class SerializableDemo implements Serializable{
    
    private String name;
    private transient Integer age;
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setName("小二");
        demoOut.setAge(105);
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }

}
输出:
SerializableDemo [name=小二, age=null]

可以很清楚的看到,age字段是不会被序列化并保存的,这样就解决了上面说的保存数据的问题。

2.2.2 writeObject()&readObject()

仅仅是这样就够了么?我们序列化一个对象的时候,保存的仅仅是对象中的数据,那么如果我们确实想保存这个字段,但是要求加密呢?通过transient并不能满足我们的需求,但是在上面看的源码中,writeExternalData方法或writeSerialData方法中就包含有对对象的操作,有通过对象直接调用writeExternal(this)方法也有通过反射调用对象的writeObject方法完成需要的操作,所以,我们可以为类对象添加writeObject()readObject()方法实现对序列化对象的操作,如下所示:

/**
**writeObject()&readObject()方法示例
**/
public class SerializableDemo implements Serializable{
    
    private String name;
    private transient int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age-5);

    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt()+5;
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setName("小二");
        demoOut.setAge(105);
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }

}
输出:
SerializableDemo [name=小二, age=105]

虽然在age字段使用了transient关键字,但是在writeObject方法中,通过ObjectOutputStream对象写入了数据,所以还是存在数据的,在开始的源码中已经有写了,writeObject方法是在后面的if判断中,但是关键字的作用是在之前起作用的。

2.2.3 readResolve()

说到这里,还有一个很关键的问题,我们这样序列化,然后反序列化出来的对象并不是通过常规方法构造的,那么当一个类是单例的,那么反序列化的时候,会不会出现多实例的问题?因为这个实例是直接从字节流中直接构造出来的,我们常规的限制并不会起作用。这时在类ObjectInputStream中的方法readOrdinaryObject可以看到,当示例都已经构造好了,最后还有这样一个判断:

if (obj != null && 
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
        }
        if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
        }
    }

判断是否有readResolve方法,有的话就反射调用它,然后返回的对象就是反射调用后的对象,这里就可以看出,我们可以通过这个方法来操作了。

通过上述源码,我们知道可以通过readResolve来操作反序列化的单例实现问题,如下所示:

/**
**序列化单例问题
**/
public class SerializableDemo implements Serializable{
    private static SerializableDemo demo = new SerializableDemo("小二",100);
    private String name;
    private transient int age;
    private SerializableDemo(String name,int age){
            this.name = name;
            this.age = age;
    }
    public static SerializableDemo getInstance(){
        return demo;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age-5);

    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt()+5;
    }
    private Object readResolve(){
        return demo;
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = SerializableDemo.getInstance();
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println("demoIn==demoOut?"+(demoIn==demoOut));
    }
}
输出:
demoIn==demoOut?true

这样我们的单例就成功了,其实这里并不推荐使用readResolve()方法实现单例模式,在之前的源码中我们看到,ObjectOutputStream类中还支持一种类型Enmu,如果是单例的,可以直接使用枚举类型会更好一些,因为是Java来维护这个单例而不用我们来维护。

2.2.4 版本号

有一点需要注意的是,序列化对象的版本机制,默认情况下,序列化对象的版本是通过对象中各个字段方法计算出来的,如果修改了类,那么默认的版本号就会出现改变,所以,可以申明一个fianl的版本号来保证版本不会变化,如下所示:

/**
**Serializable版本号
**/
private static final long serialVersionUID = 21098328942;

说完了Serializable接口,接下来可以看一下它的子接口Externalizable接口。

2.3 Externalizable接口

可以看下它的源码,如下:

/**
**Externalizable接口源码
**/
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

在这个接口中只有两个方法,writeExternal()方法实现序列化,而readExternal方法实现反序列化。如果类实现了Externalizable接口,那么这两个方法在其中扮演什么角色呢?是不是像之前的readObjectwriteObject?下面测试一番,如下代码:

/**
**Externalizable测试用例
**/
public class SerializableDemo implements Externalizable{
    private String name;
    private transient int age;
    
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age-5);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt()+5;
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub
                //接口实现方法中不做任何操作
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        // TODO Auto-generated method stub
                //接口实现方法中不做任何操作
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setAge(100);
        demoOut.setName("小二");
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }
}
输出:
SerializableDemo [name=null, age=0]

可以看到在重写的两个方法中,如果没有写任何东西,那么序列化的就是字段类型的默认值,而不是现有值,而且其中的readObjectwriteObject都不起作用,其实在之前的源码方法writeExternalData()中就有写到,如下:

/**
**ObjectOutputStream中关于Externalizable序列化
**/
private void writeExternalData(Externalizable obj) throws IOException {
    Object oldObj = curObj;
    ObjectStreamClass oldDesc = curDesc;
    PutFieldImpl oldPut = curPut;
    curObj = obj;
    curDesc = null;
    curPut = null;
    
    if (extendedDebugInfo) {
        debugInfoStack.push("writeExternal data");
    }   
    try {
        if (protocol == PROTOCOL_VERSION_1) {
        obj.writeExternal(this);
        } else {
        bout.setBlockDataMode(true);
        obj.writeExternal(this);
        bout.setBlockDataMode(false);
        bout.writeByte(TC_ENDBLOCKDATA);
        }
    } finally {
        if (extendedDebugInfo) {
        debugInfoStack.pop();
        }     
    }   

    curObj = oldObj;
    curDesc = oldDesc;
    curPut = oldPut;
    }

在这里并没有反射调用其他方法,只是这个类型只过来调用了writeExternal(this)方法,根本不会有其他调用,所以如果在这个方法中没有操作就会值为默认值或null,那么修改一下,在这两个方法中来操做呢?如下所示:

/**
**Externalizable测试用例
**/
public class SerializableDemo implements Externalizable{
    private String name;
    private transient int age;
    
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub
          out.writeObject(name);
          out.writeInt(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        // TODO Auto-generated method stub
        name = (String)in.readObject();
        age = in.readInt();
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setAge(100);
        demoOut.setName("小二");
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }
}
输出:
SerializableDemo [name=小二, age=100]

这样就可以通过Externalizable接口正常的序列化这个对象了。

3 序列化代理

正常来说,通过上面介绍的方法可以正常序列化和反序列化一个对象,但是反序列化的时候毕竟不是常规方法构造出来的,总会留下各种不安全的因素,所以在《Effective Java》中有介绍一种序列化代理思想,通过代理来反序列化的时候,就可以正常通过构造方法构建对象,如下:

/**
**序列化代理 示例
**/
public class SerializableDemo implements Serializable{
    private String name;
    private int age;
    
    public SerializableDemo() {
        super();
        // TODO Auto-generated constructor stub
    }

    public SerializableDemo(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    private String getName(){
        return this.name;
    }
    private void setName(String name){
        this.name = name;
    }
    private Object writeReplace(){
        return new SerializableDemoProxy(this);
    }
    @Override
    public String toString() {
        return "SerializableDemo [name=" + name + ", age=" + age + "]";
    }
    private static class SerializableDemoProxy implements Serializable{
        private String name;
        private int age;
        
        public SerializableDemoProxy() {
            super();
            // TODO Auto-generated constructor stub
        }
        public SerializableDemoProxy(SerializableDemo demo){
            this.age = demo.age;
            this.name = demo.name;
        }
        private Object readResolve(){
            return new SerializableDemo(name,age);
        }
        
    }
    public static void main(String[] arg0) throws IOException, ClassNotFoundException{
        File f = new File("/test/test.txt");
        FileOutputStream fout = new FileOutputStream(f);
        ObjectOutputStream out = new ObjectOutputStream(fout);
        SerializableDemo demoOut = new SerializableDemo();
        demoOut.setAge(100);
        demoOut.setName("小二");
        out.writeObject(demoOut);
        out.flush();
            
        FileInputStream fin = new FileInputStream(f);
        ObjectInputStream in = new ObjectInputStream(fin);
        SerializableDemo demoIn = (SerializableDemo)in.readObject();
        System.out.println(demoIn);
    }
}
输出:
SerializableDemo [name=小二, age=100]

可以看到使用起来其实并没有区别,但是在序列化的时候,其实是使用代理的类SerializableDemoProxy,而不是直接用的当前类,而且反序列化的时候,也是反序列化当前这个代理类,然后通过内部的readObject()方法来通过构造器构造的,不会有直接构造的问题,而且使用的时候完全看不出来有任何区别。

说到这里,其实序列化的内容已经被说完了,当中如果有疑惑或者错误的地方,请帮忙指出,谢谢

文章参考:
《Java编程思想》
《Java程序设计语言》
《Effective Java》
博客:理解Java对象序列化

相关文章

网友评论

      本文标题:Java梳理之理解序列化

      本文链接:https://www.haomeiwen.com/subject/rfipuxtx.html