Java序列化机制

1、什么是序列化

        Java允许我们在内存中创建可复用的Java对象,但只有当JVM运行时这些对象才可能存在,也就是这些对象的生命周期不会比JVM的生命周期更长。但在实际应用中,可能要求在JVM停止运行之后能够保存持久化保存对象的状态,并在将来重新读取被保存的对象,Java序列化就实现这样的功能。
        序列化时jvm会把对象的状态保存为一组字节,反序列化可以将这些字节还原成对象。需要注意对象序列化保存的是对象的"状态",即它的成员变量,不会关注类中的静态变量。Java接口Serializable和Externalizable为处理对象序列化提供了一个标准机制。

2、Serializable

        在Java中只要一个类实现了java.io.Serializable接口,那么它就可以被序列化和反序列化。举例如下:

public class Person implements Serializable {

    private String name = null;

    private Integer age = null;
    //每个枚举类型都会默认继承类java.lang.Enum,而该类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
    private Gender gender = null;

    public Person() {
        System.out.println("none-arg constructor");
    }

    public Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    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 Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "[" + name + ", " + age + ", " + gender + "]";
    }
}
public class SimpleSerial {

    public static void main(String[] args) throws Exception {
        File file = new File("person.out");

        ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
        Person person = new Person("John", 101, Gender.MALE);
        oout.writeObject(person);
        oout.close();

        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = oin.readObject(); // 没有强制转换到Person类型
        oin.close();
        System.out.println(newPerson);
    }
}

 测试程序的输出结果如下:

arg constructor
[John, 31, MALE]

        当Person对象被保存到person.out文件中之后,我们可以读取该文件以还原对象,当重新读取被保存的Person对象时,并没有调用Person的任何构造器。反序列化时必须确保程序的CLASSPATH中包含有Person.class,否则会抛出ClassNotFoundException。

2.1、可以被序列化的数据类型

 private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            
            // 源码简化后
            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());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }

        如果被序列化的对象类型是String、数组、Enum、Serializable可以进行序列化,否则将抛出NotSerializableException。

2.2、Serializable的序列化机制

2.2.1、深度序列化

        深度序列化如何理解?如果某个类仅仅实现了Serializable接口而没有其他别的特殊处理的话,那么就会使用默认序列化机制。默认机制在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,以此类推,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

2.2.2、transient-序列化时忽略某个字段

        当某个字段被声明为transient后,默认序列化机制就会忽略该字段。比如将Person类中的age字段声明为transient后的输出如下:

arg constructor
[John, null, MALE]

可见age字段并未参与序列化的过程。

2.2.3、writeObject()与readObject()-自定义序列化机制

可以在ObjectOutputStream的writeObject()和ObjectInputStream的readObject()中自定义序列化/反序列化机制。

public class Person implements Serializable {
    
    transient private Integer age = null;
    

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        age = in.readInt();
    }
}

输出结果如下:

arg constructor
[NULL, 30, NULL]

        要被序列化的字段在writeObject()中写入ObjectOutputStream中,同样反序列化时应该以同样的顺序读取字段,而且这种方式不受transient关键字的制约。那么自定义序列化机制是如何实现的呢?writeObject和readObject是如何被ObjectOutputStream和ObjectInputStream调用的呢?

        答案是反射机制。ObjectOutputStream和ObjectInputStream使用了反射来寻找是否声明了这两个方法,因为它们使用getPrivateMethod,所以这些方法不得不被声明为priate以至于供ObjectOutputStream和ObjectInputStream来使用。

 private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            //如果声明了WriteObject()方法使用自定义的序列化方法
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {//否者使用默认的序列化机制
                defaultWriteFields(obj, slotDesc);
            }
        }
    }
private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;

            if (slots[i].hasData) {
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } else if (slotDesc.hasReadObjectMethod()) {//有自定义的ReadObject()方法
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } else {//使用默认的反序列机制
                    defaultReadFields(obj, slotDesc);
                    }

                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else {
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }
            }
        }
            }

        一定要注意Write的顺序和read的顺序需要对应。譬如writeObject有多个字段都用writeInt写入流中,那么readint需要按照顺序将其赋值。

private void writeObject(ObjectOutputStream out) throws IOException {
            out.writeInt(age);
            out.writeObject(name);
}
 
private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
            this.name =(String) ins.readObject();
            this.age = ins.readInt();
}

报错如下:

java.io.OptionalDataException
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)

2.3.4、readResolve()-反序列化对象返回的入口

我们看下单例的实现:

public class Person implements Serializable {

    private static class InstanceHolder {
        private static final Person instatnce = new Person("John", 31, Gender.MALE);
    }

    public static Person getInstance() {
        return InstanceHolder.instatnce;
    }

    private String name = null;

    private Integer age = null;

    private Gender gender = null;

    private Person() {
        System.out.println("none-arg constructor");
    }

    private Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    
}

测试程序如下,我们观察下反序列化得到的对象和序列化的对象是不是同一个?

public class SimpleSerial {

    public static void main(String[] args) throws Exception {
        File file = new File("person.out");
        ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
        oout.writeObject(Person.getInstance()); // 保存单例对象
        oout.close();

        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = oin.readObject();
        oin.close();
        System.out.println(newPerson);

        System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较
    }
}

输出如下:

arg constructor
[John, 31, MALE]
false

可见反序列化得到了另外一个对象,这就违反了单例的设计原则,通过readResolve()可以解决。

public class Person implements Serializable {

    private static class InstanceHolder {
        private static final Person instatnce = new Person("John", 31, Gender.MALE);
    }

    public static Person getInstance() {
        return InstanceHolder.instatnce;
    }

    private String name = null;

    private Integer age = null;

    private Gender gender = null;

    private Person() {
        System.out.println("none-arg constructor");
    }

    private Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    private Object readResolve() throws ObjectStreamException {
        return InstanceHolder.instatnce;
    }
    
}

测试后输出:

arg constructor
[John, 31, MALE]
true

        当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。

源码如下:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            //反射找到ReadResolve方法,返回反序列化的对象
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

2.3.5、serialVersionUID作用

序​列​化​的​版​本​号​,凡是实现Serializable接口的类都应有一个表示序列化版本标识符的静态变量。

private static final long serialVersionUID

serialVersionUID有两种生成方式,一、自己指定,二、IDE根据类名,接口名,方法和属性自动生成,我们一定要指定serialVersionUID,两种方式都可以。如果没有指定serialVersionUID的话,IDE默默生成的serialVersionUID在反序列化时如果源代码有改动的话,比如增减变量、方法,反序列化会失败,抛出class 不兼容异常:

Exception in thread "main" java.io.InvalidClassException: Persion; 
2 local class incompatible: 
3 stream classdesc serialVersionUID = -88175599799432325, 
4 local class serialVersionUID = -5182532647273106745

        没有指定serialVersionUID的类,java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件多一个空格,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以添加了一个字段后,如果没有指定 serialVersionUID,编译器又为我们生成了一个UID,当然和前面保存在文件中的那个不会一样了,于是就出现了2个序列化版本号不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。

        为了提高serialVersionUID的独立性和确定性,强烈建议在一个序列化类中显示的定义serialVersionUID,为它赋予明确的值。

3、Externalizable

public interface Externalizable extends java.io.Serializable {
   
    void writeExternal(ObjectOutput out) throws IOException;

   
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

序列化在writeExternal中完成,反序列化在readExternal中完成。

public class Person implements Externalizable {

    private String name = null;

    transient private Integer age = null;

    private Gender gender = null;

    public Person() {
        System.out.println("none-arg constructor");
    }

    public Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
    
}

输出如下:

arg constructor
none-arg constructor
[null, null, null]

由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段,这也就是为什么输出结果中所有字段的值均为空。

使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。因此,实现Externalizable接口的类必须要提供一个无参的构造器,且访问权限为public。

public class Person implements Externalizable {

    private String name = null;

    transient private Integer age = null;

    private Gender gender = null;

    public Person() {
        System.out.println("none-arg constructor");
    }

    public Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
    
}

输出结果如下:

arg constructor
none-arg constructor
[John, 31, null]

要注意的是,反序列化时字段读取顺序要和序列化时的写入顺序一致。

4、Parcelable

        Serializable序列化用到了反射,反射会产生大量的临时对象,进而引起频繁的GC,序列化的过程较慢。Parcelable是Android为我们提供的序列化接口,Parcelable相对于Serializable的使用相对复杂一些,但Parcelable的效率相对Serializable也高很多,Google经过效率对比,Parcelable可比Serializable快10倍以上。

        Parcelable接口是通过Parcel实现序列化的,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。

 

下面介绍下Parcelable和Serializable的作用、效率、区别及选择。

1、作用
Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。

2、效率及选择
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化。

3、编程实现
对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可。Parcelable则需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。

5、Java序列化协议

举例如下:

class parent implements Serializable {  
 
     int parentVersion = 10;  
 
}

class contain implements Serializable{  
 
     int containVersion = 11;  
 
}

public class SerialTest extends parent implements Serializable {  
 
       int version = 66;  
 
       contain con = new contain();  
 
   
 
       public int getVersion() {  
 
              return version;  
 
       }  
 
       public static void main(String args[]) throws IOException {  
 
              FileOutputStream fos = new FileOutputStream("temp.out");  
 
              ObjectOutputStream oos = new ObjectOutputStream(fos);  
 
              SerialTest st = new SerialTest();  
 
              oos.writeObject(st);  
 
              oos.flush();  
 
              oos.close();  
 
       }  
 
} 

  

SerialTest类实现了Parent超类,内部还持有一个Container对象,以16进制形式打开temp.out,内容如下所示:

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65

73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07

76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09

4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72

65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00

0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70

00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74

61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00

0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78

70 00 00 00 0B

1、AC ED:

STREAM_MAGIC ,声明使用了序列化协议 

2、00 05

STREAM_VERSION,序列化协议版本

3、0x73

TC_OBJECT, 声明这是一个新的对象
4、0x72

TC_CLASSDESC,声明这里开始一个新 Class 

5、00 0A

 Class 名字的长度 
6、53 65 72 69 61 6c 54 65 73 74

 SerialTest,Class 类名 

7、05 52 81 5A AC 66 02 F6:

SerialVersionUID,  序列化 ID ,如果没有指定,则会由算法随机生成一个 8byte 的 ID.
8、0x02

标记号,该值声明该对象支持序列化。
9、00 02

该类所包含的域个数

    序列化按照上述协议格式,将Java对象序列化为二进制形式,二进制中保存类、对象、成员变量信息。反序列化也按照序列化协议,通过反射机制生成Java对象,读取字段值后赋值给该对象返回。
 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值