理解 Serializable 文档

(本篇提到的源码均来自 AOSPXRef 的 Android 14 版本)

源文件地址

/libcore/ojluni/src/main/java/java/io/Serializable.java  源码链接

接口 Serializable 来自 java.io 包,也就是说,它是属于 JAVA 语言层面上的一种关于输入与输出处理的重要设计。(像 Parcelable 接口,就是属于 Android 系统的特色设计。)

 接口源码

public interface Serializable {
}

Serializable 接口的源码如上所示,可以看到这是一个空接口,其本身没有任何成员方法或变量。

文档与理解

(以下引用内容均翻译自源代码中的注释文档)

类的可序列化是通过实现 Serializable 接口实现的。

该接口没有任何方法或字段只被用来标识一种“可序列化”的语义——没有实现这个接口的类,就不能对类的任何状态进行序列化或反序列化;而可序列化的类的所有子类,也都是可序列化的。

· 标记接口


这里可以和 Cloneable 接口一起理解,它们都是一种起到“标识”作用的空接口,也被称作标记接口 Marker Interface

要想更细致地了解标记接口,可以参考【 java中的标记接口 】这篇博客。这里只简单地总结:标记接口是一种通用的设计模式,可被用作为描述类的元数据,并在运行时被获取。

注意“运行时”Cloneable 与 Serializable 这两个接口都是不会在编译时检查接口的实现情况,而只在运行时抛出相应的 Exception 的。

如果一个不可序列化的类的子类想实现可序列化,那这个子类就需要负责保存与恢复父类的 public、protected 与 package(当没有可见性修饰符时,就是 package 即同包内可见的) 字段的状态。而要做到这一点,这个父类就需要有一个可访问的无参构造函数,如果没有,那将这个子类声明为 Serializable 就可能会遇到运行时错误。

因为在去序列化的过程中,一个不可序列化的类的字段的初始化需要用到这个类的 public 或 protected 无参构造函数,因此它的子类要想可序列化,就必须得访问它的无参构造函数。而这个可序列化的子类的字段,就可以从 stream 流中恢复。

遍历时,可能会遇到不支持 Serializable 接口的对象,这时便会抛出一个异常 NotSerializableException,并识别出这个不可序列化的对象的类。

· 父类可访问的无参构造函数

文档首先提到,一个子类要想可序列化,那它的不可序列化的父类就需要有一个可访问的无参构造函数

这一点可以用一个代码示例帮助理解。(在代码示例之前,先介绍一下序列化机制中主要的两个“流”,ObjectOutputStream 和 ObjectInputStream,前者负责在序列化时向一个 OutputStream(例如 FileOutputStream)写入,后者负责在反序列化时从一个 InputStream(例如 FileInputStream)读出。

// 不可序列化的父类
public class Serial1 {
    public String str;          // 父类的 public、protected 或 package 字段

    private Serial1(){         // 父类的无参构造函数: private <-> public 或 protected
        str = "123";
    }
    public Serial1(int param){ // 父类的可访问的有参构造函数
        this();
    }
}

// 实现了 Serializable 接口的子类
public class Serial2 extends Serial1 implements Serializable {
    public Serial2(int param) {   
        super(param);
    }
}
val outS = ObjectOutputStream(FileOutputStream("serial.txt"))
outS.writeObject(Serial2(1))            // 序列化写入 Serial2 实例

val inS = ObjectInputStream(FileInputStream("serial.txt"))
val obj = inS.readObject() as Serial2   // 反序列化 
println(obj.str)

上述代码示例中,无论 Serial1 的无参构造函数是 private、public 还是 protected,编译都是没有问题的;当访问控制是 public 或 protected 时,也都可以成功反序列化并输出父类的可访问字段;但当修饰词是 private 时,运行时 readObject 便会抛出异常 InvalidClassException 并提示 “no valid constructor”。

可以查看 InvalidClassException 的几种错误场景,其中一个便是:

  • The class does not have an accessible no-arg constructor (类没有一个可访问的无参构造器)

 · 对象图 Object Graph

然后再理解一下序列化文档中经常会出现的“图 Graph”、“对象图 Object Graph”的概念。

因为序列化机制在对一个对象进行序列化时,会将它引用的对象也序列化,依次类推,一个个地遍历下去,就像是一个图的结构。因此,序列化一个目标对象可能会实际地序列化一组对象,而这组对象就可以看作是一个 Object Graph。(参考博客 【Object Graph in Java Serialization】)

只要在遍历这组对象的过程中遇到一个无法序列化的对象,那么便会抛出异常NotSerializableException

可以在上述示例代码的基础上,为 Serial2 新增一个引用了一个不可序列化的 Serial3 对象的字段,这时再次运行就会发现,writeObject 方法会抛出一个 NotSerializableException 异常并提示不可序列化的类是 Serial3。

public class Serial3 {
}

public class Serial2 extends Serial1 implements Serializable {
    public Serial3 prop = new Serial3();    // 引用对象也会被序列化
    ...
}
val outS = ObjectOutputStream(FileOutputStream("serial.txt"))
outS.writeObject(Serial2(1))               // 运行时报错
// java.io.NotSerializableException: com.example.juejin.Serial3

查看 NotSerializableException 文档,可以发现它的抛出场景就是实例没有实现 Serializable 接口, 它的参数则是类 Class 的名字

那些在序列化与反序列化过程中需要特殊处理的类,必须严格按照如下方法签名实现特定方法:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
  • private void readObjectNoData() throws ObjectStreamException

其中,writeObject 方法负责写指定类的对象的状态,而对应的 readObject 方法就负责恢复这个对象。要使用默认的机制来存储对象字段,可以调用 out.defaultWriteObject() 方法。该方法不需要考虑它的父类或子类的状态。要保存状态,可以使用 writeObject 方法把每个单独字段写入到 ObjectOutputStream,或者使用 DataOutput 提供的针对原生数据类型的方法。

 

readObject 方法负责从流中读取并恢复对象的字段。调用方法 in.defaultReadObject 可以使用默认机制恢复对象非静态非瞬态的字段。defaultReadObject 方法会根据流中的信息,把流中存储的字段值分配给当前对象对应命名的字段,如此可以应对那些类添加了新字段的场景。该方法也不需要考虑它的父类或子类的状态。同时要恢复状态,也可以从 ObjectInputStream 读取每个单独字段的数据并分配给对象相应的字段,或者借助 DataInput 的支持读取原生数据类型

readObjectNoData 方法则被用于一些特殊场景:例如,要初始化一个指定类的对象时,序列化流不能把这个给定的类看作是要被反序列化的对象的超类,这种情形可能是因为接收方使用了这个反序列化实例的类的不同版本,而且这个版本继承自与发送方版本不同的类;又或者,序列化流已经被篡改了。所以说,readObjectNoData 是负责在源流“敌对”或者不完整的情形下对反序列化对象初始化的。

以下讨论主要以 writeObject 为例。

 · 可序列化类的 writeObject 方法

个人理解,ObjectOutputStream.writeObject是序列化的入口,它的内部会在经过若干处理后,检查是否存在可序列化类的 writeObject 方法并调用,而这个可序列化类的 writeObject ,才是开发者可以自定义的。

也就是上述文档中提到的,如果当前类的序列化需要特殊处理,就需要自定义实现一个 private void writeObject(ObjectOutputStream out) 方法。  

如下是 ObjectOutputStream 源码的部分节选。

// ObjectOutputStream.writeObject 写入可序列化对象时,最终会调用到该方法
// 该方法针对给定对象的每个可序列化类写入实例数据,从父类到子类
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;
        if (slotDesc.hasWriteObjectMethod()) {       // 该方法实际检查两个条件:1、该类是否可序列化;2、该类是否实现了一个符合签名的 writeObject 方法
            ...
            slotDesc.invokeWriteObject(obj, this);
            ...
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

可以看到,在写入一个可序列化类的对象时,ObjectOutputStream 会从父类遍历到子类,依次检查这些类是否可序列化并持有自定义的 writeObject 方法,如果没有,就调用 ObjectOutputStream.defaultWriteFields 的默认方法。

因此也就可以理解,文档中为什么会强调类自定义的 writeObject 方法不需要考虑父类与子类的状态,因为序列化时会进行遍历检查并调用它们各自的自定义写方法默认写方法

而文档中提到的自定义写方法中可用的 out.defaultWriteObject()(也就是 ObjectOutputStream.defaultWriteFields()),它最后也是调用到 ObjectOutputStream.defaultWriteFields 方法。

部分源码节选如下,可以看到,out.defaultWriteObject() 只处理非静态非瞬态的字段(即 static 和 transient 修饰的字段),并额外对上下文 SerialCallbackContext 进行限制,即如果该方法不是从一个 writeObject 方法中被调用的,它会抛出一个异常 NotActiveException。

// 将当前类的非静态与非瞬态字段写入流中
public void defaultWriteObject() throws IOException {
    SerialCallbackContext ctx = curContext;
    if (ctx == null) {
        throw new NotActiveException("not in call to writeObject");
    }
    Object curObj = ctx.getObj();
    ...
    defaultWriteFields(curObj, curDesc);
    ...
}

除了默认的写方法之外,类自定义的 writeObject 还可以灵活调用 out 的一系列写方法,例如触发新一次的对象序列化 out.writeObject,或者使用 out.writeInt、out.writeUTF 等方法直接写入原生和 String 数据。

而之所以文档中会提到使用 DataOutput 处理原生数据类型,是因为 ObjectOutputStream 内部有一个实现了 DataOutput 接口的静态内部类 BlockDataOutputStream,和一个该类的属性 bout,并由 bout 来处理各类原生数据的写操作。部分源码节选如下:

...
private final BlockDataOutputStream bout;
...
public void writeInt(int val)  throws IOException {    // 提供给外部调用的写方法
    bout.writeInt(val);                                // 实际由 bout 处理 
}
...
private static class BlockDataOutputStream extends OutputStream implements DataOutput {
    // 实现了 DataOutput 接口要求的对原生类型和UTF(String) 的一系列写方法
    // 还实现并优化了相应的数组的写方法
    ...
}

也就是说,类自定义的 writeObject 方法可以直接调用 out 的写方法写入原生数据,而 out 的写方法最后是由它的一个实现了 DataOutput 接口的属性来实现。

 · writeObject、readObject 和 readObjectNoData

读方法 readObject 与 writeObject 类似,由 ObjectInputStream.readObject 在内部检查是否存在并调用,部分源码节选如下。可以看到,相较于 writeObject 的序列化过程,反序列化时会涉及到利用反射构建类的实例的 newInstance,和对象或数据布局为空时的特殊读取处理。

// 读一个常规的对象(“常规”是指非 String、Class、ObjectStreamClass、Array 或者 enum )
private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    ObjectStreamClass desc = readClassDesc(false); 
    ...
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
    ...
    }
    ...
    readSerialData(obj, desc);
    ...
}
// 读 (或尝试跳过)该对象的实例数据,从父类到子类
private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException {
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
            // 类似 writeObject 中的数组,该数组记录类的数据布局并按继承关系排序
    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() 方法
                ...
                slotDesc.invokeReadObject(obj, this);
                ...
            } else {
                ...
                defaultReadFields(obj, slotDesc);
                ...
            }
            ...
        } else {
            // 如果对象的数据布局的数据为空,便会检查并触发 readObjectNoData 
            if (obj != null && slotDesc.hasReadObjectNoDataMethod() && handles.lookupException(passHandle) == null) {
                slotDesc.invokeReadObjectNoData(obj);
            }
        }
    }
}

在之前的示例代码基础上进行修改,为可序列化类 Serial2 自定义实现 writeObject、readObject 和 readObjectNoData 这 3 个方法:

// 不可序列化父类 Serial1  有一个字段 str = "123"和一个可访问的无参构造函数
// 不可序列化类 Serial3
public class Serial3 {
    public String str;        // 新增一个字段 str
    public Serial3(String param) {
        str = param;
    }
}
// 可序列化类 Serial2
public class Serial2 extends Serial1 implements Serializable {
    
    public Serial3 prop = new Serial3("Serial3");    // 引用不可序列化对象

    public Serial2(int param) {  super(param);  }
    
    // 严格遵循方法签名 ; throws 灵活
    private void writeObject(ObjectOutputStream out){
        System.out.println("S2 OUT");
    }
    private void readObject(ObjectInputStream in){
        System.out.println("S2 IN");
    }
    private void readObjectNoData() {
        System.out.println("S2 IN NOData");
    }
}
val obj = Serial2(1)
val outS = ObjectOutputStream(FileOutputStream("serial.txt"))
println("Serialize Start")
outS.writeObject(obj)

val inS = ObjectInputStream(FileInputStream("serial.txt"))
println("DeSerialize Start")
val s2 = inS.readObject() as Serial2

println(s2.str)                // 父类字段
println(s2.prop.str)         // 不可序列化对象字段 

初始时,不在自定义的 writeObject 和 readObject 方法中进行任何处理,运行之后输出结果如下:

Serialize Start                           // 一次序列化开始
S2 OUT                                    // 类自定义的 writeObject
DeSerialize Start                      // 一次反序列化开始
Serial1 NO-ARG-CONS           // 父类的无参构造函数
S2 IN                                        // 类自定义的 readObject
123                                           // 父类可访问的字段
java.lang.NullPointerException: Cannot read field "str" because "s2.prop" is null   
                                                // 类没有自定义或默认处理的字段


可以看到:

  • out.writeObject 和 in.readObject 方法内部会自动调用 Serial2 类自定义的 writeObject 与 readObject 方法,且正常状态下不会调用 readObjectNoData
  • 自定义的读写方法内不需要考虑父类 Serial1 的状态,就可以正常读写父类的可访问字段 str,且反序列化时会自动调用父类的无参构造函数
  • Serial2 的自定义字段的序列化处理会由自定义读写方法实现,所以读写方法为空时,不会像之前那样抛出 Serial3 字段的 NotSerializableException 异常,但自然也丢失了该字段的数据。

如下代码示例列举了几种可能的读写方法实现方式,以帮助理解这部分内容:(因为不太了解怎么构建出 readObjectNoData 的场景,这里就不多介绍了,以后有机会再补充。)

// 1、使用默认的读写方法 out.defaultWriteObject() 和 in.defaultReadObject()
private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
}
// 1、使用默认方法直接对整个对象序列化与反序列化,因此仍旧会在写入 Serial3 字段时抛出 NotSerializableException

// 2、使用 out 与 in 的读写方法来单独处理某些字段 
private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeObject(prop);    // 直接使用对应的写方法写入 Serial3 字段 prop
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.readObject();          // 直接读
}
// 2、当然,因为 prop 不可序列化,这种方式依旧会抛出 NotSerializableException

// 3、只要读写方法制定好约定,readObject 就可以灵活地修改或新增字段
public String newAddedStr;                        // 反序列化端的类新增了一个字段
private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeUTF(prop.str);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    prop = new Serial3(in.readUTF() + " NEW");    // 根据 str 对字段赋值
    str = str + " NEW";                           // 修改父类的字段
    newAddedStr = "new added 123";                // 新增字段
}
// 3、只要约束好 serialVersionUID,就可以正常运行并输出各字段


· 序列化中的替换:writeReplace() 和 readResolve()

如果可序列化类希望在将一个对象写入流中时指定其它可选对象,它就需要严格按照如下方法签名实现特定方法:

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

只要存在 writeReplace() 方法,序列化便会调用它。因为该方法只需要在被序列化的对象的类内可访问即可,所以 private、protected 和 package (也就是没有修饰符)等访问修饰词都可以使用。

 如果可序列化类希望在从流中读取出一个实例后指定一个接替者,它就需要严格按照如下方法签名实现特定方法:

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

readResolve() 的调用和可访问性规则与 writeReplace() 方法一致。

在上述代码的基础上,为 Serial2 实现 writeReplace() 和 readResolve() 两个方法,运行之后输入如下。可以发现,writeReplace 在 writeObject 前调用,readResolve 在 readObject 后调用

Serialize Start
S2 OUT REPLACE            // writeReplace 
S2 OUT                              // writeObject
DeSerialize Start
S2 IN                                 // readObject
S2 IN RESOLVE               // readResolve 

查看 ObjectOutputStream 对应部分源码如下,可以看到,序列化最后实际上是将 writeReplace 的返回结果写到了流中。

// 由 writeObject(obj) 调用
private void writeObject0(Object obj, boolean unshared) throws IOException {
    ...
    if (desc.hasWriteReplaceMethod() && 
            (obj = desc.invokeWriteReplace(obj)) != null &&   // 把 obj 替换为 writeReplace 的返回结果
            (repCl = obj.getClass()) != cl) {
        ...
    }
    ...
    writeOrdinaryObject(obj, desc, unshared);   // 会继续调用到上面提到过的 writeSerialData 方法,而 writeSerialData 会检查并调用自定义的 writeObject
    ...
}

查看 ObjectInputStream 对应部分源码如下,可知反序列化返回的实际上是 readResolve 的返回结果。

private Object readOrdinaryObject(boolean unshared) throws IOException {
    ...
    readSerialData(obj, desc);    // 该方法会检查并调用自定义的 readObject()
    ... 
    if (obj != null && handles.lookupException(passHandle) == null && 
                desc.hasReadResolveMethod()) {
        Object rep = desc.invokeReadResolve(obj);
        ...
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);   // obj 替换为 readResolve 的返回结果
        }
    }
    return obj;
}

所以简单总结,这两个方法就是替换掉要被序列化的对象和反序列化出的对象

 · writeReplace 和 readResolve 的应用

关于 writeReplace 和 readResolve 方法的一些应用场景,可以参考这两篇博客:

这篇博客提到,虚拟机会自动为那些实现了 Serializable 接口的 lambda 表达式对应的匿名类对象添加一个 writeReplace 方法,且该方法可以返回一个包含了该 lambda 表达式具体信息的 SerializedLambda 对象。SerializedLambda 提供如 getImplMethodNamegetImplMethodSignature 等方法,来帮助在运行时获取 lambda 表达式信息。

这篇博客提到了反射和反序列化对构造函数的直接调用所隐含的对单例的破坏性(前面提到过反序列化过程中使用了 newInstance 反射)。而 readResolve 方法可以在单例类中将所有反序列化出的新对象都重新替换为单例对象,进而解决单例被破坏的问题。

 · 序列版本号 `serialVersionUID`

序列化运行时为每一个可序列化的类关联一个版本号,serialVersionUID,被用来在反序列化时验证序列化对象的发送者与接收者加载的对象的类是否兼容。如果接收者加载出的对象的类和发送者对应的类的 serialVersionUID 不一致,那么反序列化的过程就会抛出 InvalidClassException 异常。一个可序列化的类可以显式地声明它的 serialVersionUID 字段,该字段必须是 long 类型,同时必须修饰为 static 与 final。

  • ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L

如果没有显式声明,那序列化运行时就会根据这个类的各方面信息计算出一个默认的 serialVersionUID。但是强烈建议所有的可序列化类都显式声明一个 serialVersionUID 值,因为隐式的计算方式对类的细节非常敏感,而任何编译器实现上的不同都可能改变类的细节,进而造成反序列化过程中意料外的 InvalidClassException 异常。所以,要确保不同 JAVA 编译器实现之下的 serialVersionUID 值是一致的,就必须对其显式声明。同时,也强烈建议尽可能在显式声明时使用 private 修饰,因为这个声明只会作用于声明它的类中—— serialVersionUID 字段是不可以作为一个可继承成员使用的。至于数组类 Array Class,它们无法显式地声明一个 serialVersionUID,所以都只拥有隐式的计算值,但是序列化运行时放弃了对数组类的 serialVersionUID 匹配要求

此外,从 Android N 开始,一些类的 serialVersionUID 计算方式的实现发生了些微改变。为了保证兼容性,这些改变只在应用 target SDK 版本设置为 24 以上时Android N 对应 SDK 24)才启用。因此再次强烈建议,应采用显式声明的 serialVersionUID 来避免兼容性问题;

这部分文档中就已经介绍得比较清晰了。

代码示例尝试先为 Serial2 显式声明一个序列化版本 ID private final static long serialVersionUID = 1L,在序列化写入文件之后,修改版本 ID 为 2L,再对文件进行反序列化。此时会抛出异常 InvalidClassException,并提示:“local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2”。

如果定义一个类 Serial4 继承 Serial2,并且不为它显式声明一个序列化版本 ID,那么即便修改 Serial2 的 ID 访问控制为 public,序列化运行时也会为它默认计算一个 ID,也就是文档中提到的,serialVersionUID 字段不会作为一个可继承成员使用

查看 Android 14 源码中一些 JAVA 包内的可序列化类的实现方式:

public class Throwable implements Serializable {
    @java.io.Serial
    private static final long serialVersionUID = -3042686055658047285L;、
    ... 
    @java.io.Serial
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();       
        ...  
    }
    ...
    @java.io.Serial
    private synchronized void writeObject(ObjectOutputStream s) throws IOException {
        ...
        s.defaultWriteObject();
        ...
    }
}

public class Exception extends Throwable {
    @java.io.Serial
    static final long serialVersionUID = -3387516993124229948L;
    ...
}

public class IOException extends Exception {
    @java.io.Serial
    static final long serialVersionUID = 7818375828146090155L;
    ...
}

public abstract class ObjectStreamException extends IOException {
    @java.io.Serial
    private static final long serialVersionUID = 7260898174833392607L;
    ...
}

可以看到:

  • 可序列化类 Throwable 和它的子类们都显式地声明了各自的 serialVersionUID,并且访问控制基本上都设置为了 private 或 package;
  • 只有 Throwable 实现了自定义的 writeObject 和 readObject 方法,且 writeObject 方法使用了 synchronized 修饰。个人理解,任何一个异常类对象序列化时,都会从 Throwable 父类开始遍历,调用其自定义读写方法,和子类们的默认读写方法;
  • 使用了 @java.io.Serial 注解

· @Serial 注解

@Retention(RetentionPolicy.SOURCE) 
@Target({ElementType.METHOD,ElementType.FIELD}) 
public @interface Serial extends java. lang. annotation. Annotation


首先查看 @Serial 注解的定义,如上,它们各自的含义如下:

  1.  @Retention,用来指定注解保留多久,RetentionPolicy 策略主要有 3 种,SOURCE(保留在源文件中并由编译器丢弃),CLASS(保留在 class 文件中但运行时不会保留在 VM 中),和 RUNTIME(保留在 class 文件中且运行时会保留在 VM 中)。因此 @Serial 注解只作用在源文件中
  2. @Target,用来指定注解可以应用于的上下文,ElementType 类型包含 TYPE、FIELD、METHOD、PARAMETER 等。而 @Serial 注解可以修饰的目标为方法字段
  3. @interface的含义比较简单,就是用来定义一个注解的,就像 interface 定义一个接口一样。

再查看 @Serial 注解的文档:

该注解被用来指明字段或方法是序列化机制的一部分,因此可对序列化相关的声明进行编译时检查,就类似于 @Override 注解实现的对方法覆写的验证。可序列化类最好都使用该注解修饰那些序列化相关的字段与方法,来帮助编译器检查可能存在的错误声明。

序列化相关的 5 个方法:

  • private void writeObject(java. io. ObjectOutputStream stream) throws IOException
  • private void readObject(java. io. ObjectInputStream stream) throws IOException, ClassNotFoundException
  • private void readObjectNoData() throws ObjectStreamException
  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

序列化相关的 2 个字段:

  • private static final ObjectStreamField[] serialPersistentFields
  • private static final long serialVersionUID

编译器会对 @Serial 注解标记的上述方法或字段进行验证并发布警告。


 在如下情形中使用该注解会出现语义错误:

  • 非 Serializable 类中的方法或字段;
  • enum 类型的 serialVersionUID 固定为 0L,因此在 enum 类型中声明 serialVersionUID 会被忽略;enum 类型的 5 个方法也会被忽略;
  • Externalizable 类中的 writeObject、readObject、readObjectNoData 方法,和 serialPersistentFields 字段。

尽管 Externalizable 接口继承自 Serializable,但上面提到的 3 个方法和 1 个字段不被用于 Externalizable 类中。

注意:序列化机制通过反射访问这些字段与方法,所以它们可能会在类中显示为未使用

Since: 14

简单总结,因为序列化机制不对可序列化类进行编译时检查而只在运行时抛出异常,所以 @Serial 注解便被提出来,以对序列化机制涉及到的字段与方法进行一些编译时检查,例如命名是否正确、方法签名是否正确、字段访问控制是否恰当等等。

此外可以看到,该注解是在 “14” 之后提出,在 AOSPXRef 查看 Android 13 源码,可以发现该注解尚未出现,所以该注解应该是在 Android 14 及之后被使用的;同时也有相关资料指出,该注解作为 JAVA 语言的设计,文档中实际指的是 JAVA 14 。

所以,如果 Android Studio 使用的是 JAVA 1.8(8)、JAVA 1.7(7)这些较低版本,@Serial 注解是不会有提示的。需要将其更改为更高版本,本篇博客采用了 JAVA 17

如下,打开 Project Structure -> Module 面板,修改 Source Compatibility 与 Target Compatibility 为 JAVA 17;如果是 Kotlin 项目,那么就再在 Module:build.gradle 文件中修改 jvmTarget = '17'。 

此时尝试为 Serial2 类的序列化相关字段与方法添加 @Serial 注解,就可以看到相应提示。( 不过@Serial 基本都是 Warning 提示,不会影响编译与运行,例如即便方法不符合签名,代码依旧可以通过编译,只是运行时不会被调用而已。

 

 ·《Effective JAVA》: 审慎地实现 `Serializable`

审慎地实现 `Serializable

参考书籍 《Effective JAVA》。它解释了如何在不损害应用的可维护性的前提下使用 `Serializable` 接口。

此处可以在线阅读 《Effective JAVA》一书中的这一章节:

86. 非常谨慎地实现 Serializable - 《Effective Java (高效 Java) 第三版》 - 书栈网 · BookStack

它主要提到了序列化机制对类的演化的限制、反序列化时隐含的构造函数带来的安全风险、实现 Serializable 接口的工作负担等等问题。书中写得很详细,这里就不再赘述。

推荐备选方案

JSON 简洁、可读、高效。Android 提供了读写 JSON 的 streaming API(android.util.JsonReader)和 tree API(org.json.JSONObject)。使用像 GSON 这样的库就可以直接地读取 JAVA 对象

总结

  1. Serializable 接口是一个空的标记接口,被用作在运行时描述类为“可序列化的”;
  2. 可序列化的类的子类也是可序列化的,而不可序列化的类的子类要想实现可序列化,那它的父类就必须存在一个可访问的无参构造函数;
  3. ObjectOutputStream 和 ObjectInputStream 的读写方法是一次序列化和反序列化的入口,它们内部会检查并调用可序列化类自定义的 writeObject 和 readObject 方法;
  4. 可序列化的类也可自定义 writeReplace 和 readResolve 方法,用来替换要被序列化的对象和被反序列化出的对象;
  5. 每个可序列化类都会关联一个字段 serialVersionUID,用来验证发送方与接收方的类版本是否兼容,该字段可以显式指定(推荐),也可隐式计算,不可继承,针对 enum 或 array 等类型会有特殊处理;
  6. 序列化机制相关的方法与字段的声明形式均有严格限制, Android 14 与 JAVA 14 后,可使用 @Serial 注解来帮助对它们进行编译时检查。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值