序列化基础

news2024/11/28 4:45:49

1、简介

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。它允许把内存中的 Java 对象转换成平台无关的二进制流(序列化,也称编码),并持久地保存在磁盘上或通过网络把这种二进制流传输到另一个网络节点,其它程序获得这种二进制流也可以把它恢复成原来的 Java 对象(反序列化,也称解码)。这样就使得对象可以脱离程序的运行而独立存在。

序列化的使用场景有:

  • 本地存储:将对象数据永久的保存在文件或者磁盘中。
  • 网络传输:将对象数据在网络上进行传输。由于网络传输是以字节流的方式对数据进行传输的,因此序列化的目的是将对象数据转换成字节流的形式。
  • 进程间通信:在 Android 中,对象数据在进程之间传递(如 Activity 之间传递数据),或者 Intent 之间传递复杂的数据类型时(基本数据类型直接传不用序列化)。

Android 中常用的序列化方案:Serializable,Parcelable,Json,Xml,Protocol buffer。

合理的选择序列化方案,可以从以下方面考虑:

  • 通用性:是否跨平台、跨语言。流行程度(很少人使用的协议往往意味着昂贵的学习成本)。
  • 健壮性:bug 要少。
  • 可调试性/可读性:Xml 可读性高。
  • 性能:时间、空间成本。
  • 可扩展性/兼容性
  • 安全性/访问限制:Android 的 Parcelable 曾有安全漏洞:漏洞预警 | Android系统序列化、反序列化不匹配漏洞

2、Serializable & Externalizable 接口

Serializable 是 Java 提供的一个标记接口,它只是表明该类的实例可以序列化,无须实现任何方法。Externalizable 接口是 Serializable 的子接口,其内部定义了 writeExternal(ObjectOutput) 和 readExternal(ObjectInput) 两个方法:

public interface Serializable { 
}

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

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

我们先关注 Serializable 接口。

2.1 Serializable 基本使用

序列化时需要用 ObjectOutputStream 的 writeObject(OutputStream),反序列化时用 ObjectInputStream 的 readObject():

    // 将 object 的序列化数据写入 path 表示的文件中
	public synchronized static boolean saveObject(Object object, String path) {
        if (object == null) return false;

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path))) {
            oos.writeObject(object);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

	// 从指定文件中读取数据进行反序列化
    public synchronized static <T> T readObject(String path) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

序列化时可以在构造 ObjectOutputStream 时传入不同的输出流实现不同的效果。如传入 ByteArrayOutputStream,可以直接得到序列化的二进制数据:

    public synchronized static byte[] getSerializedObject(Object object, String path) {
        if (object == null) return null;

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(object);
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

2.2 Serializable 实现类的创建

以实现了 Serializable 接口的 Person 类为例:

public class Person implements Serializable {

    // serialVersionUID 唯一标识了一个可序列化的类
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    // Food 也需要实现 Serializable 接口
    private List<Food> foods;
    /*
    用transient关键字标记的成员变量不参与序列化。
    在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象则为 null。
     */
    private transient Date createTime;
    /*
    静态成员变量属于类不属于对象,所以不会参与序列化。
    对象序列化保存的是对象的“状态”,也就是它的成员变量,因此序列化不会关注静态变量。
     */
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat();

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        foods = new ArrayList<>();
        createTime = new Date();
    }
    
    // getters and setters...
}

下面关注创建 Serializable 的实现类时需要注意的问题。

serialVersionUID

作用是表明类的不同版本间的兼容性,保证序列化类在其对象已经序列化之后做了修改,该对象依然可以被正确反序列化。举个例子,对于上面的 Person 类,先序列化一个对象保存到文件中。然后,假设业务需求变更需要把 age 字段修改成 String 类型的,那么在修改 age 字段的同时,也要修改 serialVersionUID:

public class Person implements Serializable {
    
    private static final long serialVersionUID = 2L;
    
    private String age;
    //...
}

这样的话,反序列化时根据 serialVersionUID 的值是可以正确执行的。倘若没有声明 serialVersionUID,修改前的已经被序列化的 Person 对象就会按照修改后的 Person 类进行反序列化,由于字段不匹配而抛出异常:

java.io.InvalidClassException: com.frank.serializable.Person; local class incompatible: stream classdesc serialVersionUID = -8357684236211496111, local class serialVersionUID = -657709351248511155
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	......

如果不显式定义 serialVersionUID,那么这个值会由 JVM 根据类的相关信息进行计算,通常修改后的计算结果与修改前不同,使得类版本不兼容,进而造成反序列化失败。说的直白一点,对象序列化数据中的 serialVersionUID 和反序列化时使用的 class 文件中的 serialVersionUID 不同,反序列化就会失败。

为了保证反序列化的正常执行,需要显式声明 serialVersionUID,最好是 private final 的,也可以使用 JDK bin 目录下的工具 serialver.exe 工具生成 serialVersionUID,当然 Android Studio 中的 GenerateSerialVersionUID 插件也可以帮我们生成。

不显式指定 serialVersionUID 的另一个坏处就是,即便类没有发生变化,但是两端使用不同的 JVM 时,由于编译器不同,计算策略不同,也可能会造成计算出来的 serialVersionUID 不同,导致反序列化失败。

那修改哪些程序单元时要更新 serialVersionUID 呢?需要看情况:

  • 修改方法、静态变量、瞬态实例变量(transient 修饰的变量),不需要修改 serialVersionUID(因为以上元素不会被序列化)。
  • 修改了非瞬态实例变量,可能会导致序列化版本不兼容。修改了变量类型(对象流中的对象和新类中包含同名实例变量,但实例变量类型不同)需要更新;删除了某个变量(流中的对象比新类中包含更多的实例变量,多出的被忽略),可以不更新;增加了某个变量(新类中包含的对象比流中包含的实例变量多),版本可以兼容,serialVersionUID 也可以不更新,但是反序列化时新对象中多出的实例变量值都是 null 或 0。

transient 关键字

transient 作用:非静态数据如不想被序列化,可以使用这个关键字修饰使之成为瞬态变量。

ObjectOutputStream 的 writeObject() 会将 Serializable 的实现类中非瞬态、非静态的成员变量都序列化,如果有非静态成员变量不想被序列化的话,就可以用 transient 修饰这个成员变量。例如 Person 类中的 Date 类型对象 createTime 被 transient 修饰,反序列化后该字段仍然存在,但是为 null。

具有类似情况的还有 static 修饰的静态变量,也不会被序列化。因为序列化的操作对象是实例对象,而 static 修饰的变量属于类,并不是序列化关注的内容。

transient 修饰的变量、static 修饰的变量和方法不会参与序列化。反序列化时这些变量的值为 0(基本数据类型)或者 null(引用数据类型)。

此外,Kotlin 中的 transient 不是一个关键字,而是一个注解 @Transient。

序列化的传递性

序列化具有一定的传递性:

1. 一个实现了序列化的类,它的子类也是可序列化的。
2. 要序列化的类中除了 transient、static 修饰的变量外,其它变量类型也要实现 Serializable 接口。
public class Student extends Person {

    // Course 要实现 Serializable 接口
    List<Course> courses;

    public Student(String name, String age) {
        super(name, age);
        courses = new ArrayList<>();
    }
}

Student 继承 Person,由于 Person 实现了序列化,那么 Student 也要实现序列化,这就要求 Course 类必须要实现 Serializable 接口,否则在序列化过程中会抛出 NotSerializableException。

此外,如果父类没有实现 Serializable,而子类实现了,需要父类中提供一个子类可访问到的空参构造方法,否则在反序列化调用 readObject() 时会抛出异常:

java.io.InvalidClassException: com.frank.serializable.Son; no valid constructor
	at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
	at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)

示例代码:

public class Father {

    private int age;

    // 应该打开,否则抛异常
    /*public Father() {
    }*/

    public Father(int age) {
        this.age = age;
    }
}

public class Son extends Father implements Serializable {

    private String name;

    public Son(String name) {
        super(10);
        this.name = name;
    }
}

2.3 序列化步骤与数据格式

序列化步骤:

  1. 将对象实例相关的类元数据输出。
  2. 递归地输出类的超类描述直到不再有超类。
  3. 类元数据输出完毕以后,从最顶层的超类开始输出对象实例的实际数据值。
  4. 从上至下递归输出实例的数据。

比如说对于一个 Person 对象,序列化到文件后,查看其十六进制数据(使用 Notepad++ -> 插件 -> HEX Editor -> View in Hex 查看):

序列化后16进制数据

这些十六进制数据的含义:

  • AC ED: STREAM_MAGIC 声明使用了序列化协议。
  • 00 05: STREAM_VERSION 序列化协议版本。
  • 0x73: TC_OBJECT 声明这是一个新的对象。
  • 0x72: TC_CLASSDESC 声明这里开始一个新的 Class。
  • 00 1d: Class 名字的长度。

2.4 源码流程

从 ObjectOutputStream 的 writeObject() 方法开始:

    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }

根据 enableOverride 的值决定调用 writeObjectOverride() 或 writeObject0()。enableOverride 仅在 ObjectOutputStream 的构造方法被赋值,如果调用的是空参构造方法,enableOverride 为 true,如果调用的是 ObjectOutputStream(OutputStream),则 enableOverride 为 false。

走一般流程,看 writeObject0() 的源码:

	/**
	* 写对象数据是一个递归流程。判断 obj 的类型是否可以直接写二进制数据,如 String、Array 或者
	* 枚举类型。如果不是,就调用 writeOrdinaryObject(),再去写入这个“复合”对象中的每个属性,
	* 此时会再调用到 writeObject0() 形成递归,直到 obj 的所有属性都可以直接写成二进制数据为止。
	*/
	private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        // ......
        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 {
                // 如果没有实现 Serializable 接口,抛出 NotSerializableException
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            // ......
        }
    }

根据 obj 的类型,调用相应的写入数据的方法。如果 obj 不是 String、数组或枚举类型,并且还实现了 Serializable,就会调用 writeOrdinaryObject():

    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            // 写 Externalizable 或者 Serializable 数据
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            ...
        }
    }

如果实现的是 Serializable,会调用 writeSerialData() 写数据:

    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;
            // 如果 Serializable 的实现类自定义了 writeObject() 就走 if,否则走 else。
            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);
                    // 调用自定义 writeObject()
                    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);
            }
        }
    }

看默认流程,走 defaultWriteFields():

    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }

        desc.checkDefaultSerialize();

        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        desc.getPrimFieldValues(obj, primVals);
        bout.write(primVals, 0, primDataSize, false);

        // 获取类中所有属性
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {
            ...
            try {
                // 又调用到 writeObject0() 写属性数据,形成递归。
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                ...
            }
        }
    }

递归直到要写入的是基本数据类型为止,整个流程图如下:

序列化方法调用流程图

2.5 自定义序列化

writeObject()、readObject()

源码流程中提到,在 writeSerialData() 中,如果 Serializable 的实现类自定义了 writeObject(),那么就通过反射调用到这个 writeObject(),否则就调用 defaultWriteFields() 执行一般的序列化过程。因此,重写 writeObject()、readObject() 可以实现自定义序列化。

自定义序列化机制可以让程序控制如何序列化实例变量,甚至完全不序列化。在要进行序列化的类中要提供以下方法:

自定义序列化方法

三个方法的用途:

  • writeObject(out) 默认调用 out.defaultWriteObject 来保存各个实例变量,我们可以在方法内自己决定如何对实例变量进行序列化。
  • readObject(in) 默认调用 in.defaultReadObject 来恢复对象的非瞬态实例变量。在这个方法内做与 writeObject(out) 操作相反的操作即可正确恢复该对象。
  • 当序列化流不完整时,readObjectNoData() 可以用来正确地初始化反序列化的对象。例如两端使用的序列化的类版本不同,或者序列化流被篡改,系统都会调用这个方法来初始化反序列化对象。

示例代码:

public class Person implements Serializable {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getters...

    private void writeObject(ObjectOutputStream outputStream) throws IOException {
        outputStream.writeObject(new StringBuffer(name).reverse());
        outputStream.writeInt(age);
    }

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        name = ((StringBuffer) inputStream.readObject()).reverse().toString();
        age = inputStream.readInt();
    }
}

在 writeObject() 将 name 属性封装到 StringBuffer 中并倒序,在 readObject() 中将 StringBuffer 倒序转换成 String 类型实现还原。

writeObject() 和 readObject() 处理变量的顺序应该一致,否则不能正常恢复该对象。

writeReplace()、readResolve()

还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其它对象。如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下方法:

    ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

仍以 Person 类为例:

public class Person implements Serializable {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /*private void writeObject(ObjectOutputStream outputStream) throws IOException {
        outputStream.writeObject(new StringBuffer(name).reverse());
        outputStream.writeInt(age);
    }

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        name = ((StringBuffer) inputStream.readObject()).reverse().toString();
        age = inputStream.readInt();
    }*/

    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> arrayList = new ArrayList<>();
        arrayList.add(name);
        arrayList.add(age);
        return arrayList;
    }
}

writeReplace() 把 Person 需要序列化的成员变量都封装在 ArrayList 中,并返回这个 ArrayList 对象,那么在反序列化时,我们拿到的就是一个 ArrayList 对象而不是原来的 Person 对象了:

	private void test3() {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.txt"))) {
            // 序列化
            Person person = new Person("name", 10);
            objectOutputStream.writeObject(person);

            // 反序列化,这里拿到的不是 Person 而是 ArrayList。
            //Person p = (Person) objectInputStream.readObject();
            ArrayList<Object> arrayList = (ArrayList<Object>) objectInputStream.readObject();
            System.out.println(arrayList.get(0) + "," +arrayList.get(1));
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
    }

执行序列化过程时,系统先调用 writeReplace(),如果它返回另一个对象,则系统会再去调用另一个对象的 writeReplace(),直到该方法不再返回另一个对象为止。最后再调用 writeObject() 保存该对象。

与 writeReplace() 对应的方法是:

    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readResolve() 会紧跟在 readObject() 之后被调用,其返回值会替代原来反序列化的对象,而 readObject() 反序列化的对象会被立即丢弃。

readResolve() 的特殊用途

readResolve() 在序列化单例类、枚举类时尤其有用。这么说是因为在 Java5 的 enum 出现之前,老式定义枚举类的方法不使用 readResolve() 会有问题。看代码示例:

public class Orientation implements Serializable {
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
    private int value;

    public Orientation(int value) {
        this.value = value;
    }
}

这个 Orientation 是 enum 出现之前的枚举类定义方式,先序列化然后再反序列化:

    private void test() {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("orientation.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("orientation.txt"))) {
            objectOutputStream.writeObject(Orientation.HORIZONTAL);
            Orientation orientation = (Orientation) objectInputStream.readObject();
            System.out.println(orientation == Orientation.HORIZONTAL); // false
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

发现反序列化得到的对象并不是枚举类中定义的枚举对象。为了解决这个问题,就要用 readResolve() 对反序列化的结果做进一步处理:

    private Object readResolve() throws ObjectStreamException {
        if (value == 1) {
            return HORIZONTAL;
        }

        if (value == 2) {
            return VERTICAL;
        }

        return null;
    }

在序列化类中加入这个方法后,再测试发现结果就为 true 了。

但是这个问题在使用 enum 定义的枚举类中是没有的:

public enum Orientation implements Serializable {
    HORIZONTAL,VERTICAL
}

测试反序列化的对象是否为枚举类中的枚举对象:

    private void test() {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("orientation.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("orientation.txt"))) {
            objectOutputStream.writeObject(Orientation.HORIZONTAL);
            Orientation orientation = (Orientation) objectInputStream.readObject();
            System.out.println(orientation == Orientation.HORIZONTAL); // true
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

类似的,在序列化单例类时,也会发生反序列化得到的对象与原对象不是同一个对象的问题:

public class Single implements Serializable {

    private static class SingleHolder {
        private static Single instance = new Single();
    }

    public static Single getInstance() {
        return SingleHolder.instance;
    }

    private Single() {
    }
}
---------------------------------------------------------------------------------------
    public static void main(String[] args) throws IOException, ClassNotFoundException {
    
        Single single = Single.getInstance();

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
        objectOutputStream.writeObject(single);
        byte[] bytes = out.toByteArray();

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Single single1 = (Single) objectInputStream.readObject();
        System.out.println(single == single1); // false
    }

这时就需要使用 readResolve() 进行处理了,在 Single 类中添加:

    private Object readResolve() {
        return SingleHolder.instance;
    }

writeReplace() 和 readResolve() 的隐患

writeReplace() 和 readResolve() 也有一些隐患。它们都可以被 public、protected 和 private 修饰,因此子类就可能会继承这些方法。对 readResolve() 来说,如果子类没有重写这个方法,反序列化时就会得到一个父类对象,这是一种不易发现的错误。但如果总让子类重写这个方法,又是一种负担。通常的建议是,对于 final 类重写 readResolve() 不会有任何问题,否则重写 readResolve() 时应尽量使用 private 修饰。

2.6 要注意的问题

对象变化

反序列化得到的对象不是序列化前的对象,测试代码:

    private static void test1() throws IOException, ClassNotFoundException {
        Person person = new Person("Test", 12);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
        objectOutputStream.writeObject(person);
        byte[] bytes = out.toByteArray();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Person person1 = (Person) ois.readObject();
        System.out.println(person == person1); // false
    }

序列化编号

这里的序列化编号不是之前说的那个 serialVersionUID,而是用来检查某个对象是否已经被序列化过的编号:

序列化算法内容

之所以要使用这个序列化编号,是为了避免多个对象对同一个对象引用进行重复序列化。比如说:

public class Teacher implements Serializable {
    private String name;
    private Person student;
}

已知 Person 类是可以序列化的,假设有如下特殊情形:

    Person person = new Person("name",1);
    Teacher t1 = new Teacher("teacher1",person);
    Teacher t2 = new Teacher("teacher2",person);

t1 和 t2 在进行序列化的时候都需要对它们内部持有的 person 对象进行序列化。假如没有序列化编号,那么 t1 和 t2 就会分别对 person 各进行一次序列化,产生两个 person 对象。而另一端从输入流中反序列化出来也是两个 person 对象了,而不是一个。这违背了 Java 序列化机制的初衷。

采用了序列化编号之后,只有第一次使用 writeObject() 时才会将该对象转换成字节序列并输出,后面再重复调用时输出的是序列化编号,这样可以避免一个对象被多次序列化的问题。但是这样做会有另一个隐患,就是如果一个对象在第一次调用 writeObject() 之后发生了改变,那么后面再调用 writeObject() 也仍然会输出序列化编号。

序列化可变对象时一定要注意,只有第一次调用 writeObject() 输出对象时才会将对象转换成字节序列,并写入到 ObjectOutputStream。后面如果对象实例发生了改变,再次调用 writeObject() 输出该对象时,实例变量也不会被输出,而是输出序列化编号。

多引用写入

在序列化输出过一次之后,对序列化对象进行了修改再输出一次,要注意,第二次输出要特殊处理:

    private static void test1() throws IOException, ClassNotFoundException {
        Person person = new Person("Test", "13");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);

        // 序列化输出一次
        objectOutputStream.writeObject(person);
        // 修改序列化对象
        person.setAge("14");
        // 将修改后的对象再输出一次
        writeObjectAgain(person, objectOutputStream);

        // 反序列化,观察 person1 和 person2 的结果
        byte[] bytes = out.toByteArray();
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Person person1 = (Person) in.readObject();
        System.out.println(person1);
        Person person2 = (Person) in.readObject();
        System.out.println(person2);
    }

如果还跟第一次一样只使用 objectOutputStream.writeObject(person),会发现反序列化得到的都是第一个对象(猜测原因就是上一节讲的序列化编号):

    private static void writeObjectAgain1(Person person, ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.writeObject(person); // {name='Test', age=13},Person{name='Test', age=13}
    }

	private static void writeObjectAgain2(Person person, ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.writeUnshared(person); // {name='Test', age=13},Person{name='Test', age=14}
    }

	private static void writeObjectAgain3(Person person, ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.reset();
        objectOutputStream.writeObject(person); // {name='Test', age=13},Person{name='Test', age=14}
    }

第二种和第三种是正确的处理方法,使用 writeUnshared() 或者在调用 writeObject() 之前先调用一次 reset()。

2.7 Externalizable

如果需要序列化的类实现的不是 Serializable 接口而其子接口是 Externalizable,自定义序列化方法也是和前者类似的。只不过 Externalizable 是要求强制自义定的,必须要实现 writeExternal() 和 readExternal() 两个方法,其作用和 Serializable 的 writeReplace() 和 readResolve() 非常相似,不再赘述。执行序列化和反序列化的方式与 Serializable 相同,都是调用 readObject() 和 writeObject():

public class Course implements Externalizable {

    private static final long serialVersionUID = 1L;

    private String name;
    private float score;

    /**
     * 序列化类型必须提供一个可访问的空参构造方法,否则反序列化时会抛出
     * InvalidClassException,原因是 no valid constructor
     */
    public Course() {
    }

    public Course(String name, float score) {
        this.name = name;
        this.score = score;
    }

    /**
     * @param out ObjectOutput 接口的实现类之一是 ObjectOutputStream,相当于
     *            还是调用的输出流去做序列化,只不过你可以自定义序列化的内容了。
     */
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("writeExternal");
        out.writeObject(name);
        out.writeFloat(score);
    }

    /**
     * @param in ObjectInput 接口的实现类之一是 ObjectInputStream,可以
     *           根据序列化的过程调整反序列化属性的顺序。
     */
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("readExternal");
        name = (String) in.readObject();
        score = in.readFloat();
    }

    public static void main(String[] args) {
        Course course = new Course("English", 85);
        // 还是用 ObjectOutputStream 和 ObjectInputStream 做序列化与反序列化
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(course);
            byte[] bytes = baos.toByteArray();

            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
            Course resultCourse = (Course) ois.readObject();
            System.out.println(resultCourse.name + ":" + resultCourse.score);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

虽然实现 Externalizable 接口能带来一定的性能提升,但是由于必须要实现 writeExternal() 和 readExternal() 两个方法导致编程复杂度增加,所以大部分时候采用实现 Serializable 接口来实现序列化。

2.8 总结

  1. 对象的类名、实例变量(包括基本类型、数组、对其它对象的引用)都会被序列化;方法、类变量、transient 实例变量(也称为瞬态实例变量)都不会被序列化。
  2. 实现 Serializable 接口的类如果需要让某个实例变量不被序列化,在该变量前加 transient 即可。虽然 static 也能达到这个效果,但是不能这样用。
  3. 可序列化类 A 需要保证所有实例变量类型也是可序列化的,如果有实例变量不可序列化可用 transient 修饰。否则,类 A 不可被序列化。
  4. 反序列化对象时必须有序列化对象的 class 文件。
  5. 通过文件、网络读取序列化后的对象时,必须按实际写入的顺序读取。

3、Parcelable

Parcelable 是 Android 为我们提供的序列化的接口,Parcelable 相对于 Serializable 的使用要复杂一些,但 Parcelable 的效率相对 Serializable 也高很多。Parcelable 是 Android SDK 提供的,它是基于内存的,由于内存读写速度高于硬盘,因此 Android 中的
跨进程对象的传递一般使用 Parcelable。

3.1 基本使用

public class Course implements Parcelable {

    private String name;
    private float score;

    /**
    * 反序列化创建对象时会用这个构造方法,读取属性,需要与 writeToParcel() 
    * 中写属性的顺序保持一致
    */
    protected Course(Parcel in) {
        name = in.readString();
        score = in.readFloat();
    }

    /**
     * 实现类必须有一个 Creator 属性,用于反序列化,将 Parcel 对象转换为 Parcelable
     */
    public static final Creator<Course> CREATOR = new Creator<Course>() {
        // 从序列化对象中,获取原始的对象;
        // 反序列化的方法,将Parcel还原成Java对象
        @Override
        public Course createFromParcel(Parcel in) {
            return new Course(in);
        }

        // 创建指定长度的原始对象数组,提供给外部类反序列化这个数组使用。
        @Override
        public Course[] newArray(int size) {
            return new Course[size];
        }
    };

    /**
     * 描述,返回的是内容的描述信息,只针对一些特殊的需要描述信息的对象,需要返回1,其他情况返回0就可以
     * 描述当前 Parcelable 实例的对象类型
     * 比如说,如果对象中有文件描述符,这个方法就会返回上面的 CONTENTS_FILE_DESCRIPTOR
     * 其他情况会返回一个位掩码
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 将对象转换成一个 Parcel 对象
     *
     * @param dest  表示要写入的 Parcel 对象
     * @param flags 表示这个对象将如何写入
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeFloat(score);
    }
}

Kotlin 实现 Parcelable 借口的方式更简洁,可以通过 Generator 中的插件自动生成,也可以通过 @Parcelize 注解生成:

@Parcelize
data class CourseTest(var name: String, var score: Float) : Parcelable

3.2 理论知识

底层是通过 Parcel 先包装要传输的数据,然后在 Binder 中传输,也就是用于跨进程传输数据
简单来说,Parcel 提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过 Parcel 可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型:

Parcel 可以包含原始数据类型(用各种对应的方法写入,比如 writeInt()、writeFloat() 等),可以包含 Parcelable 对象,它还包含了一个活动的 IBinder 对象的引用,这个引用导致另一端接收到一个指向这个 IBinder 的代理 IBinder。Parcelable 通过 Parcel 实现了 read 和 write 的方法,从而实现序列化和反序列化。

一个简单的 Parcel 的使用示例:

    private static void test1() {

        // 通过对象池拿到一个 Parcel 对象
        Parcel writeParcel = Parcel.obtain();
        // 填充数据
        writeParcel.writeInt(12);
        writeParcel.writeInt(19);

        // 序列化,得到一个字节数组,IPC 操作就围绕这个数组
        byte[] bytes = writeParcel.marshall();
        Log.d("Frank", "byteArray = " + bytes.length); // 8

        // 数据写入完成之后,需要将指针调整到最初的位置
        writeParcel.setDataPosition(0);
        // 释放 writeParcel
        writeParcel.recycle();

        Parcel readParcel = Parcel.obtain();
        // 反序列化操作
        readParcel.unmarshall(bytes, 0, bytes.length);

        int dataSize = readParcel.dataSize();
        Log.d("Frank", "readParcel size = " + dataSize); // 8
        for (int i = 0; i < dataSize; i = i + 4) {
            readParcel.setDataPosition(i);
            Log.d("Frank", "value = " + readParcel.readInt()); // 12 19
        }

        readParcel.recycle();
    }

3.3 Parcelable 与 Serializable 比较

  1. 在内存使用上,Serializable 在序列化过程中使用了反射机制会产生大量的临时变量导致频繁的 GC,而 Parcelable 以 IBinder 为信息载体,内存开销较小。
  2. 在读写数据上,Serializable 通过 IO 流的形式将数据写入到硬盘或者传输到网络上,而 Parcelable 是在内存中直接进行读写,所以速度较快。但是如果需要做数据持久化,还是要用 Serializable。(Parcelable 能转成 byte[],那么实际上也就是能持久化。但是 Parcelable 主要还是在内存中用作客户端通信用。)

参考:
Android中Parcelable的原理和使用方法
Android Parcel对象详解
Kotlin 一个好用的新功能:Parcelize
实用工具-JSON生成Java实体类

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1256922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年第十六届山东省职业院校技能大赛中职组“网络安全”赛项竞赛正式试题

第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题 目录 一、竞赛时间 二、竞赛阶段 三、竞赛任务书内容 &#xff08;一&#xff09;拓扑图 &#xff08;二&#xff09;A模块基础设施设置/安全加固&#xff08;200分&#xff09; &#xff08;三&#xf…

【学习记录】从0开始的Linux学习之旅——驱动模块编译与加载

一、概述 Linux操作系统通常是基于Linux内核&#xff0c;并结合GNU项目中的工具和应用程序而成。Linux操作系统支持多用户、多任务和多线程&#xff0c;具有强大的网络功能和良好的兼容性。本文主要讲述如何编译及加载linux驱动模块。 二、概念及原理 应用程序通过系统调用与内…

【C/C++】如何不使用 sizeof 求数据类型占用的字节数

实现代码&#xff1a; #include <stdio.h>#define GET_TYPE_SIZE(TYPE) ((char *)(&TYPE 1) - (char *) & TYPE)int main(void) {char a a;short b 0;int c 0;long d 0;long long e 0;float f 0.0;double g 0.0;long double h 0.0;char* i NULL;print…

STK Components 二次开发-创建地面站

1.地面站只需要知道地面站的经纬高。 // Define the location of the facility using cartographic coordinates.var location new Cartographic(Trig.DegreesToRadians(-75.596766667), Trig.DegreesToRadians(40.0388333333), 0.0); 2.创建地面站 创建方式和卫星一样生成对…

MUI框架从新手入门【webapp开发教程】

文章目录 MUI -最接近原生APP体验的高性能前端框架APP开发3.25 开发记录miu框架介绍头部/搜索框&#xff1a;身体>轮播图轮播图设置数据自动跳转&#xff1a;九宫格图片九宫格图文列表底部选项卡按钮选择器手机模拟器 心得与总结&#xff1a;MUI框架在移动应用开发中的应用M…

Linux shell编程学习笔记30:打造彩色的选项菜单

1 需求分析 在 Linux shell编程学习笔记21&#xff1a;用select in循环语句打造菜单https://blog.csdn.net/Purpleendurer/article/details/134212033?spm1001.2014.3001.5501 中&#xff0c;我们利用select in循环语句打造的菜单中&#xff0c;菜单项都是用系统设置的颜色配…

大屏可视化编辑器

前言&#xff1a; 乐吾乐Le5le大屏可视化设计器&#xff0c;零代码实现物联网、工业智能制造等领域的可视化大屏、触摸屏端UI以及工控可视化的解决方案。同时也是一个Web组态工具&#xff0c;支持2D、3D等多种形式&#xff0c;用于构建具有实时数据展示、监控预警、丰富交互的组…

一、Spring_IOCDI(1)

&#x1f33b;&#x1f33b; 目录 一、前提介绍1.1 为什么要学?1.2 学什么?1.3 怎么学? 二、Spring相关概念2.1 初始Spring2.1.1 Spring家族2.1.2 了解 Spring 发展史 2.2 Spring系统架构2.2.1 系统架构图2.2.2 课程学习路线 2.3 Spring核心概念2.3.1 目前项目中的问题2.3.2…

【古诗生成AI实战】之一——实战项目总览

[1] 总览 【古诗生成AI实战】系列共五篇文章&#xff1a; 【古诗生成AI实战】之一——实战项目总览   【古诗生成AI实战】之二——项目架构设计   【古诗生成AI实战】之三——任务加载器与预处理器   【古诗生成AI实战】之四——模型包装器与模型的训练   【古诗生成AI…

nodejs+vue+python+PHP+微信小程序-婚纱摄影预约系统的设计与实现-安卓-计算机毕业设计

本婚纱摄影预约系统主要包括个人中心、套系风格管理、用户管理、摄影师管理、婚纱套系管理、婚纱套系订单管理、客片欣赏管理、客户样片管理、摄影咨询管理、客户选片管理、系统管理等多个模块。它帮助婚纱摄影预约实现了信息化、网络化&#xff0c;通过测试&#xff0c;实现了…

阿里云,找回初心!

大数据产业创新服务媒体 ——聚焦数据 改变商业 近期&#xff0c;阿里巴巴发布了2023年Q3财报。其中&#xff0c;阿里云收入同比增长2%至276.48亿元&#xff0c;经调整EBITA利润从上个季度的3.87亿元&#xff0c;提升至14.09亿元&#xff0c;环比增幅达264%。 应该说&#xff…

自建CA实战之 《0x02 Nginx 配置 https双向认证》

自建CA实战之 《0x02 Nginx 配置 https双向认证》 上一章节我们已经实现了Nginx上配置https单向认证&#xff0c;主要场景为客户端验证服务端的身份&#xff0c;但是服务端不验证客户端的身份。 本章节我们将实现Nginx上配置https双向认证&#xff0c;主要场景为客户端验证服…

MySQL简单介绍

简单了解MySQL MySQL语句分类 SQL语句分类 DDL&#xff1a;数据定义语句 create表&#xff0c;库.….] DML&#xff1a;数据操作语句 [增加insert&#xff0c;修改 update&#xff0c;删除delete] DQL&#xff1a;数据查询语句 [select] DCL&#xff1a;数据控制语句 …

Linuxfork,写时拷贝

1.prinf隐藏的缓冲区 1.思考:为什么会有缓冲区的存在? 2.演示及思考? 1).演示缓存区没有存在感 那为什么我们感觉不到缓冲区的存在呢?我们要打印东西直接就打印了呢? 我们用代码演示一下: 比如打开一个main.c,输入内容如下: #include <stdio.h> int main() { …

【论文解读】Real-ESRGAN:使用纯合成数据训练真实世界的超分辨率图像

图一是4种超分方法的对比效果 。 0 摘要 尽管在盲超分辨率方面已经进行了许多尝试&#xff0c;以恢复具有未知和复杂退化的低分辨率图像&#xff0c;但它们仍然远远不能解决一般的真实世界退化图像。在这项工作中&#xff0c;我们将强大的 ESRGAN 扩展到一个实际的恢复应用程序…

计算机图形学-变换基础

坐标系转换历程模型坐标系 -> 世界坐标系 -> 摄像机坐标系 -> 视口&#xff08;屏幕&#xff09;坐标系 变换 仿射变换和线性变换线性&#xff1a;旋转 缩放 镜像 切变放射&#xff1a; 平移 平移 2D变换矩阵 3D变换矩阵 旋转 2D旋转矩阵 //2D 旋转private (float,…

案例026:基于微信的原创音乐小程序的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

7000字详解 动态代理(JDK动态代理 CGLIB动态代理)与静态代理

代理模式 1. 代理模式 概念2. 静态代理3. 动态代理3.1.JDK动态代理3.2.CGLIB动态代理3.3. JDK动态代理和CGLIB动态代理区别 4.静态代理和动态代理区别5.篇末 1. 代理模式 概念 代理模式是一种设计模式。 使用代理对象来替代真实对象&#xff0c;用代理对象去访问目标对象。这样…

ROS2智能小车基本原理图

我觉得这样意思已经表的很清楚了 这个图很重要&#xff0c;有了这个图&#xff0c;就可以积累每个部分的代码了&#xff0c;如果没有这个图&#xff0c;那么每次都只能是测试&#xff0c;以前的代码都会需要重新写一次。不过第一次训练也许更重要&#xff0c;这也是不可避免的…

使用STM32与MFRC522 IC进行RFID卡的读取与识别(含代码)

利用STM32与MFRC522 IC进行RFID卡的读取和识别&#xff0c;可以实现对RFID卡的读取和获取卡片标识信息。MFRC522 IC是一种高集成度的13.56MHz RFID芯片&#xff0c;常用于门禁系统、物流跟踪和智能支付等领域。下面将介绍如何使用STM32与MFRC522 IC进行RFID卡的读取和识别&…