vlambda博客
学习文章列表

JDK源码阅读笔记-Serializable接口

JDK 版本:1.8
代码地址:https://github.com/chenminjie24/JDK-Note/blob/master/src/com/chenminjie/SerializableNote.java

1.序列化

序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

2.Serializable接口

在 Java 中通过实现Serializable接口来获得序列化的能力,但该接口只是语义上标识该类具有序列化能力,并未提供任何字段和方法。实现Serializable有一些注意事项如下:
2.1 serialVersionUID
可序列化类需要关联一个版本号,称为serialVersionUID。
在序列化和反序列化时根据serialVersionUID来判断是否使用了正确的类来加载,否则会抛出InvalidClassException。
建议的定义方式如下:
  
    
    
  
private static final long serialVersionUID = 8683452581122892189L; // 可以自己取值
如果不声明该字段则在序列化时会根据类信息计算得出一个默认serialVersionUID,但这个serialVersionUID与编译器有关,在不同编译器下计算出来结果可能不同,可能导致一些不必要的InvalidClassException。
2.2 可序列化类的子类都可序列化
写一段代码来实验下:
public class SerializableNote {
public static void main(String[] args) { Person jay = new Person("jay", 24); Student ming = new Student("ming", 12, "school"); try { writeObject2File(jay, "/tmp/jay.ser"); writeObject2File(ming, "/tmp/ming.ser");
Person fakeJay = (Person) readObjectFromFile("/tmp/jay.ser"); Student fakeMing = (Student) readObjectFromFile("/tmp/ming.ser"); System.out.println(fakeJay); System.out.println(fakeMing); } catch (Exception e) { e.printStackTrace(); } }
private static class Person implements java.io.Serializable { private final static long serialVersionUID = 77635273691834578L; private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
private static class Student extends Person { private String school;
public Student(String name, int age, String school) { super(name, age); this.school = school; }
@Override public String toString() { return "Student{" + "name='" + super.name + '\'' + ", age=" + super.age + ", school='" + school + '\'' + '}'; } }
private static void writeObject2File(Object object, String fileAddress) throws Exception { FileOutputStream outputStream = new FileOutputStream(fileAddress); ObjectOutputStream out = new ObjectOutputStream(outputStream); out.writeObject(object); out.close(); outputStream.close(); }
private static Object readObjectFromFile(String fileAddress) throws Exception { FileInputStream fileInputStream = new FileInputStream(fileAddress); ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); Object object = inputStream.readObject(); inputStream.close(); fileInputStream.close(); return object; }}

输出结果如下:
Person{name='jay', age=24}Student{name='ming', age=12, school='school'}
Person类实现了 Serializable 接口,Student 类继承了 Person 类,Student 可以进行反序列化。
2.3 不可序列化类的子类要实现 Serializable 接口父类必须有可访问的无参构造方法
测试代码:
public class SerializableNote {
public static void main(String[] args) { MiTelevision mi = new MiTelevision("mi", "Are you OK?"); try { writeObject2File(mi, "/tmp/mi.ser"); MiTelevision fakeMi = (MiTelevision) readObjectFromFile("/tmp/mi.ser"); System.out.println(fakeMi); } catch (Exception e) { e.printStackTrace(); } }
private static class Television { private String brand;
public Television(String brand) { this.brand = brand; } // 注掉该构造方法或改为 private 后,则子类无法序列化 public Television() {
}
@Override public String toString() { return "Television{" + "brand='" + brand + '\'' + '}'; } }
private static class MiTelevision extends Television implements Serializable { private String ad;
public MiTelevision(String brand, String ad) { super(brand); this.ad = ad; }
@Override public String toString() { return "MiTelevision{" + "brand='" + super.brand + '\'' + ", ad='" + ad + '\'' + '}'; } }}

输出结果:
  
    
    
  
MiTelevision{brand='null', ad='Are you OK?'}
可以看到虽然子类可以序列化了,但在父类中定义的字段信息丢失了,这是因为我们在不可序列化的父类的无参构造方法中没有对字段进行赋值。
不可序列化类的字段从无参构造方法中恢复,可序列化类的字段信息从数据流中恢复。
并且将父类中子类可访问无参构造方法改为私有或者注释掉之后,反序列化时会抛出InvalidClassException异常,提示:no valid constructor。
2.4 序列化与反序列化过程中的特殊处理
在序列化与反序列化过程可能有一些特殊的需求,比如有些字段需要加密写入等等。在类中实现下面方法可以操作序列化与反序列化过程。
注意调用defaultWriteObject()和defaultReadObject()方法。
// 序列化过程private void writeObject(ObjectOutputStream out) throws IOException;// 反序列化过程private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException ;

测试代码:
public class SerializableNote {
public static void main(String[] args) { Person jay = new Person("jay", 24); Student ming = new Student("ming", 12, "school"); try { writeObject2File(jay, "./jay.ser"); writeObject2File(ming, "./ming.ser");
Person fakeJay = (Person) readObjectFromFile("./jay.ser"); Student fakeMing = (Student) readObjectFromFile("./ming.ser"); System.out.println(fakeJay); System.out.println(fakeMing); } catch (Exception e) { e.printStackTrace(); } }
private static class Person implements Serializable { private final static long serialVersionUID = 77635273691834578L; private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
private void writeObject(ObjectOutputStream out) throws IOException { // 默认写入方法 age *= 2; out.defaultWriteObject(); out.writeUTF(name + "-fake"); }
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // 默认读出方法 in.defaultReadObject(); age /= 2; name = in.readUTF();// name = in.readUTF().replaceAll("-fake", ""); } }
private static class Student extends Person { private final static long serialVersionUID = 1267984759383L; private String school;
public Student(String name, int age, String school) { super(name, age); this.school = school; }
@Override public String toString() { return "Student{" + "name='" + super.name + '\'' + ", age=" + super.age + ", school='" + school + '\'' + '}'; } }}
输出结果:
  
    
    
  
Person{name='jay-fake', age=24} Student{name='ming-fake', age=12, school='school'}
反序列化后name 字段添加了一个“-fake”的后缀,以及在序列化过程中将 age乘 2 后又在反序列化过程中除2。
通过这两个方法,能操作序列化过程,注意调用默认方法。
2.5 readObjectNoData()方法
readObjectNoData()方法用于反序列化时用新的扩展过的类去加载旧的未扩展的反序列化结果。
例如:Student 类实现了Serializable接口未继承任何类,有一个 Student 实例被序列化到磁盘上。之后扩展了 Student 类,让其继承了 Person 类,此时让扩展后的 Student 的去加载磁盘上旧的序列化结果,则会调用 Person 中的readObjectNoData()方法。在该方法中,你可以执行对 Person 类中的字段赋值等操作。
参考:https://stackoverflow.com/questions/46481605/when-will-readobjectnodata-be-called
2.6 替换序列化对象
在序列化和反序列化的时候,提供了两个方法来替换被序列化和反序列化的对象。
// 替换序列化写入时对象private Object writeReplace() throws ObjectStreamException;// 替换反序列化后对象private Object readResolve() throws ObjectStreamException;

测试代码:
public class SerializableNote {
public static void main(String[] args) { Staff wang = new Staff("wang", 30, "996"); Shareholder chive = new Shareholder("chive", 20, "shell"); try { writeObject2File(wang, "./wang.ser"); writeObject2File(chive, "./chive.ser");
Staff fakeWang = (Staff) readObjectFromFile("./wang.ser"); Shareholder fakeChive = (Shareholder) readObjectFromFile("./chive.ser"); System.out.println(fakeWang); System.out.println(fakeChive); } catch (Exception e) { e.printStackTrace(); } }
private static class Person implements Serializable { private final static long serialVersionUID = 77635273691834578L; private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
private void writeObject(ObjectOutputStream out) throws IOException { // 默认写入方法 age *= 2; out.defaultWriteObject(); out.writeUTF(name + "-fake"); }
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // 默认读出方法 in.defaultReadObject(); age /= 2; name = in.readUTF();// name = in.readUTF().replaceAll("-fake", ""); }
private void readObjectNoData() throws ObjectStreamException { age = -1; name = "unnamed"; } }
private static class Staff extends Person { private final static long serialVersionUID = 984487567549383L; private String company;
public Staff(String name, int age, String company) { super(name, age); this.company = company; }
// 替换序列化对象 private Object writeReplace() throws ObjectStreamException { return new Staff("substitute", 996, "995"); }
@Override public String toString() { return "Staff{" + "name='" + super.name + '\'' + ", age=" + super.age + ", company='" + company + '\'' + '}'; } }
private static class Shareholder extends Person { private final static long serialVersionUID = 9999999999L; private String stock;
public Shareholder(String name, int age, String stock) { super(name, age); this.stock = stock; }
// 替换反序列化对象 private Object readResolve() throws ObjectStreamException { return new Shareholder("chives", 24, "Apple"); }
@Override public String toString() { return "Shareholder{" + "name='" + super.name + '\'' + ", age=" + super.age + ", stock='" + stock + '\'' + '}'; } }}

输出结果:
  
    
    
  
Staff{name='substitute-fake', age=996, company='995'} Shareholder{name='chives', age=24, stock='Apple'}
可以从输出结果看出,最后结果都已经被替换了。
writeReplace()是在写入时替换被写入的对象。
readResolve()是在读出后替换返回的对象。
2.7 transient
被transient修饰的字段,反序列化时不会加载其值,会使用类型对应的默认值。
测试代码:
public class SerializableNote {
public static void main(String[] args) { Coder ape = new Coder("ape", 30, (short) 1024); try { writeObject2File(ape, "./ape.ser");
Coder fakeApe = (Coder) readObjectFromFile("./ape.ser"); System.out.println(fakeApe); } catch (Exception e) { e.printStackTrace(); } }
private static class Person implements Serializable { private final static long serialVersionUID = 77635273691834578L; private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
private void writeObject(ObjectOutputStream out) throws IOException { // 默认写入方法 age *= 2; out.defaultWriteObject(); out.writeUTF(name + "-fake"); }
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // 默认读出方法 in.defaultReadObject(); age /= 2; name = in.readUTF();// name = in.readUTF().replaceAll("-fake", ""); }
private void readObjectNoData() throws ObjectStreamException { age = -1; name = "unnamed"; } }
private static class Coder extends Person { private transient short hair;
public Coder(String name, int age, short hair) { super(name, age); this.hair = hair; }
@Override public String toString() { return "Coder{" + "name='" + super.name + '\'' + ", age=" + super.age + ", hair=" + hair + '}'; } }}

输出结果:
  
    
    
  
Coder{name='ape-fake', age=30, hair=0}
从输出结果可以看出,被transient修饰的 hair 字段是默认值。
对于不想被序列化的值可以使用transient修饰。

3.注释文档及个人翻译

/** * Serializability of a class is enabled by the class implementing the * java.io.Serializable interface. Classes that do not implement this * interface will not have any of their state serialized or * deserialized. All subtypes of a serializable class are themselves * serializable. The serialization interface has no methods or fields * and serves only to identify the semantics of being serializable. <p> * 类通过实现Serializable接口来获得序列化能力。未实现该接口的类不能序列化与反序列化 * 类的状态。所有可序列化类的子类都是可序列化的。Serializable接口没有任何方法和字段, * 仅仅用来语义上标识该类可序列化。 *  * To allow subtypes of non-serializable classes to be serialized, the * subtype may assume responsibility for saving and restoring the * state of the supertype's public, protected, and (if accessible) * package fields. The subtype may assume this responsibility only if * the class it extends has an accessible no-arg constructor to * initialize the class's state. It is an error to declare a class * Serializable if this is not the case. The error will be detected at * runtime. <p> * 为了让不可序列化类的子类可被序列化,子类需要承担保存和恢复子类public,protecte * 和(可访问的)包字段的责任。子类承担该责任仅当该类的子类具有可访问的无参数构造函数 * 来初始化类的状态信息。如果不是上述情况下,声明一个类是可序列化的是错误的。运行时将 * 会检测出错误。 *  * During deserialization, the fields of non-serializable classes will * be initialized using the public or protected no-arg constructor of * the class. A no-arg constructor must be accessible to the subclass * that is serializable. The fields of serializable subclasses will * be restored from the stream. <p> * 在反序列化时,不可序列化类的字段将使用 public 或 protected 的无参构造函数来初 * 始化。无参构造函数必须对可序列化的子类是可访问的。可序列化子类的字段将从 stream  * 中恢复。 *  * When traversing a graph, an object may be encountered that does not * support the Serializable interface. In this case the * NotSerializableException will be thrown and will identify the class * of the non-serializable object. <p> * 当遍历图形时(感觉这句翻译有些有些问题,但也没找到更好的翻译,如果你有更好的翻译 * 请联系我更改),某些对象可能不支持序列化接口,这样的话会抛出NotSerializableException * 异常,并将这个类标志为不可序列化对象。 *  * Classes that require special handling during the serialization and * deserialization process must implement special methods with these exact * signatures: * 对于序列化和反序列过程中有特殊处理的类,必须实现这些指定的方法。 * * <PRE> * private void writeObject(java.io.ObjectOutputStream out) * throws IOException * private void readObject(java.io.ObjectInputStream in) * throws IOException, ClassNotFoundException; * private void readObjectNoData() * throws ObjectStreamException; * </PRE> * * <p>The writeObject method is responsible for writing the state of the * object for its particular class so that the corresponding * readObject method can restore it. The default mechanism for saving * the Object's fields can be invoked by calling * out.defaultWriteObject. The method does not need to concern * itself with the state belonging to its superclasses or subclasses. * State is saved by writing the individual fields to the * ObjectOutputStream using the writeObject method or by using the * methods for primitive data types supported by DataOutput. * writeObject方法负责写入对应类的状态信息,对应的readObject方法则负责恢复对象。 * 默认保存对象字段的机制是调用out.defaultWriteObject。该方法不需关心自身的状态 * 信息是属于超类或子类。通过使用writeObject方法将字段写入ObjectOutputStream * 或使用DataOutput支持的原始数据类型的方法来保存状态信息。 *  * <p>The readObject method is responsible for reading from the stream and * restoring the classes fields. It may call in.defaultReadObject to invoke * the default mechanism for restoring the object's non-static and * non-transient fields. The defaultReadObject method uses information in * the stream to assign the fields of the object saved in the stream with the * correspondingly named fields in the current object. This handles the case * when the class has evolved to add new fields. The method does not need to * concern itself with the state belonging to its superclasses or subclasses. * State is saved by writing the individual fields to the * ObjectOutputStream using the writeObject method or by using the * methods for primitive data types supported by DataOutput. * readObject方法用于负责从 stream 中读取数据并恢复类字段。它可以调用in.defaultReadObject * 来引入默认机制来恢复对象的非静态和非transient字段。defaultReadObject方法使用 stream 中 * 信息对应的字段来对当前对象赋值。当类新增字段时,将使用上述方法处理。这个方法不需要关心自身状 * 态信息是属于超类或子类。通过使用writeObject方法将字段写入ObjectOutputStream或使用 * DataOutput支持的原始数据类型的方法来保存状态信息。 *  * <p>The readObjectNoData method is responsible for initializing the state of * the object for its particular class in the event that the serialization * stream does not list the given class as a superclass of the object being * deserialized. This may occur in cases where the receiving party uses a * different version of the deserialized instance's class than the sending * party, and the receiver's version extends classes that are not extended by * the sender's version. This may also occur if the serialization stream has * been tampered; hence, readObjectNoData is useful for initializing * deserialized objects properly despite a "hostile" or incomplete source * stream. * 如果序列化流未将给定类列出为要反序列化的对象的超类,则readObjectNoData方法负责为其 * 特定类初始化对象的状态信息。这种情况可能发生在接收方和发送方使用了不同版本的反序列化 * 实例类,接收方版本扩展了类而发送方未扩展。也可能发生在序列化流被篡改的情形下,因此, * readObjectNoData方法对于初始化“不友善”或不完整的 stream 是非常有用的。 *  * <p>Serializable classes that need to designate an alternative object to be * used when writing an object to the stream should implement this * special method with the exact signature: * 当需要指定替代对象的可序列化类写入对象到 stream 中时应该实现下面指定的方法。 * <PRE> * ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException; * </PRE><p> * * This writeReplace method is invoked by serialization if the method * exists and it would be accessible from a method defined within the * class of the object being serialized. Thus, the method can have private, * protected and package-private access. Subclass access to this method * follows java accessibility rules. <p> * 如果writeReplace方法存在并且在被序列化的对象的类的方法中是可访问的,那么将会调用 * 该方法。因此,这个方法可以 private,protected 或者包内可访问的。对该方法的子类 * 访问遵循Java可访问性规则。 *  * Classes that need to designate a replacement when an instance of it * is read from the stream should implement this special method with the * exact signature. * 从流中读取实例时需要指定替换的类应该实现下面指定方法。 *  * <PRE> * ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; * </PRE><p> * * This readResolve method follows the same invocation rules and * accessibility rules as writeReplace.<p> * readResolve方法遵循与writeReplace相同的调用规则和可访问性规则 * * The serialization runtime associates with each serializable class a version * number, called a serialVersionUID, which is used during deserialization to * verify that the sender and receiver of a serialized object have loaded * classes for that object that are compatible with respect to serialization. * If the receiver has loaded a class for the object that has a different * serialVersionUID than that of the corresponding sender's class, then * deserialization will result in an {@link InvalidClassException}. A * serializable class can declare its own serialVersionUID explicitly by * declaring a field named <code>"serialVersionUID"</code> that must be static, * final, and of type <code>long</code>: * 序列化运行时与每个可序列化的类关联一个版本号,称为serialVersionUID,该序列号在反序列化 * 期间用于验证序列化对象的发送者和接收者是否已加载了与序列化对象兼容的类。如果接收方加载了 * 与发送方类不同serialVersionUID的类,将会抛出InvalidClassException异常。一个可序 * 列化类可以明确的声明自己的serialVersionUID通过一个名为serialVersionUID的 static, * final 并且类型为 long 的字段。 *  * <PRE> * ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L; * </PRE> * * If a serializable class does not explicitly declare a serialVersionUID, then * the serialization runtime will calculate a default serialVersionUID value * for that class based on various aspects of the class, as described in the * Java(TM) Object Serialization Specification. However, it is <em>strongly * recommended</em> that all serializable classes explicitly declare * serialVersionUID values, since the default serialVersionUID computation is * highly sensitive to class details that may vary depending on compiler * implementations, and can thus result in unexpected * <code>InvalidClassException</code>s during deserialization. Therefore, to * guarantee a consistent serialVersionUID value across different java compiler * implementations, a serializable class must declare an explicit * serialVersionUID value. It is also strongly advised that explicit * serialVersionUID declarations use the <code>private</code> modifier where * possible, since such declarations apply only to the immediately declaring * class--serialVersionUID fields are not useful as inherited members. Array * classes cannot declare an explicit serialVersionUID, so they always have * the default computed value, but the requirement for matching * serialVersionUID values is waived for array classes. * 如果可序列化的类未显式声明serialVersionUID,则序列化运行时将根据该类的各个方面为该类 * 计算默认的 serialVersionUID,如Java(TM)对象序列化规范中所述。然而,仍然强烈建议 * 所有的可序列化类都明确的声明serialVersionUID,因为默认值的 serialVersionUID  * 的计算对类的细节高度敏感的,然而类细节依赖于编译器的实现,这可能导致在反序列化时出现意 * 料外的InvalidClassException。因此,为了保证serialVersionUID 值在不同的 java  * 编译器中保持一致,可序列化类必须明确声明serialVersionUID。还强烈建议尽可能在serialVersionUID  * 声明时使用 private 修饰符,因为该字段声明应用于当前类,对继承者无用。数组类不能声明 * 显式的serialVersionUID,因此它们始终具有默认的计算值,但是对于数组类,无需匹配serialVersionUID值。 * @author unascribed * @see java.io.ObjectOutputStream * @see java.io.ObjectInputStream * @see java.io.ObjectOutput * @see java.io.ObjectInput * @see java.io.Externalizable * @since JDK1.1 */