序列化再研究

序列化ID的问题

虚拟机是否允许序列化,不仅取决于类路径(就是两个不同的端上的类的路径)和功能代码(属性字段、属性字段的类型等)是否一致;更为重要的一点是两个类的序列化ID是否一致,即serialVersionUID

如果ID不同,即使类路径和功能代码都一样,都是无法相互序列化和反序列化的。

序列化的生成策略有2种:

  • 固定的值,自己约定或默认1L
  • 随机生成一个不重复的long类型数据,给予JDK工具生成

默认情况下,没有特殊要求,建议就是使用默认的1L,这样可以保证反序列化的成功。

那在什么情况下使用随机的ID?

就是强制反序列端必须获取的是序列化端最新的序列化对象的这种情况。

外观模式

Client 端通过 Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通过网络将二进制对象数据传给 Client,Client 负责反序列化得到 Façade 对象。该模式可以使得 Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object 类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client 端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序。

静态变量

序列化保存的是对象的状态,静态变量属于类的状态,因此序列化不保存静态变量

父类的序列化与Transient关键字

要想父类对象也能序列化,则父类也必须实现Serializable接口,否则虚拟机是不会序列化父对象的,那反序列后的父类的属性就是空。

如果父类没有实现Serializable接口,可以提供默认无参构造函数来实现父类对象数据的初始化,这样不至于得到的是null。因为一个Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。

Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是null。

不用Transient如何实现字段不被序列化?

根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现Serializable接口,父类不实现,父类的字段数据将不被序列化。

对敏感字段加密

在序列化过程中,虚拟机会试图调用对象类里的writeObject()readObject()方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是ObjectOutputStreamdefaultWriteObject()方法以及ObjectInputStreamdefaultReadObject()方法。用户自定义的writeObject和readObject方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

  • writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。
  • readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中。

writeObject 和 readObject 方法到底是如何被调用的呢?

这里给出ObjectOutputStream的writeObject的调用栈:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

具体实现:

package com.example.panda.myapplication;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Created by panda on 2017/4/13.
 */
public class B1 extends A1 implements Serializable {
    public String b1;

    private void writeObject(ObjectOutputStream out) {
        try {
            ObjectOutputStream.PutField putFields = out.putFields();
            b1 = "encryption";//模拟加密
            putFields.put("b1", b1);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readObject(ObjectInputStream in) {
        try {
            ObjectInputStream.GetField readFields = in.readFields();
            Object object = readFields.get("b1", "");
            b1 = "pass";//模拟解密,需要获得本地的密钥
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

这样就对密码字段在序列化时进行加密,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

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;
}

ArrayList实现了java.io.Serializable接口,那么它可以进行序列化和反序列化操作,但是ArrayList的数据字段确实transient,表示这个成员变量在序列化时是无法被保留下来的。

那ArrayList的数据是怎么保留下来的呢?==> 原因是它重写了writeObject()readObject()方法,具体如下:

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
ArrayList为什么要这么做?

ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。

序列化存储规则

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.i = 1;
out.writeObject(test);
out.flush();
test.i = 2;
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);

目的是希望将 test 对象两次保存到 result.obj 文件中,写入一次以后修改对象属性值再次保存第二次,然后从 result.obj 中再依次读出两个对象,输出这两个对象的 i 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。

结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。

Serializable和Parcelable

Serializable:

  1. Serializable是java提供的可序列化接口
  2. Serializable的序列化与反序列化需要大量的IO操作,效率比较低,同时可能会产生大量的临时变量
  3. Serializable使用起来较为简单
  4. 适用于:对象序列化到存储设备中、网络传输中这样的场景

Parcelable:

  1. Parcelable是Android特有的可序列化接口
  2. Parcelable的效率比较高
  3. Parcleable实现起来比较复杂
  4. 适用于:内存中的序列化时效率较高

PS:然传送的对象和接收的对象内容相同,但是是不同的对象,他们的引用是不同的。

results matching ""

    No results matching ""