原文: https://howtodoinjava.com/java/serialization/serialversionuid/

Java 序列化是将对象转换为字节流的过程,因此我们可以执行类似的操作,例如将其存储在磁盘上或通过网络发送。 反序列化是相反的过程 – 将字节流转换为内存中的对象。

在序列化期间,java 运行时将版本号与每个可序列化的类相关联。 称为serialVersionUID的数字,在反序列化期间用于验证序列化对象的发送者和接收者是否已加载了该对象的与序列化兼容的类。 如果接收者为对象加载的类serialVersionUID与相应发送者的类不同,则反序列化将导致InvalidClassException

1. Java serialVersionUID语法

可序列化的类可以通过声明一个名为“serialVersionUID”的字段来显式声明其自己的serialVersionUID,该字段必须是静态的,最终的且类型为long

  1. private static final long serialVersionUID = 4L;

在这里,serialVersionUID表示类的版本,如果您对类的当前版本进行了修改,以使其不再与先前的版本向后兼容,则应该对它进行递增。

Java `serialVersionUID` – 如何生成`serialVersionUID` - 图1

2. Java 序列化和反序列化示例

让我们看一个如何将类序列化然后反序列化的示例。

  1. package com.howtodoinjava.demo.serialization;
  2. import java.io.*;
  3. import java.util.logging.Logger;
  4. public class DemoClass implements java.io.Serializable {
  5. private static final long serialVersionUID = 4L; //Default serial version uid
  6. private static final String fileName = "DemoClassBytes.ser"; //Any random name
  7. private static final Logger logger = Logger.getLogger("");
  8. //Few data fields
  9. //Able to serialize
  10. private static String staticVariable;
  11. private int intVariable;
  12. //Not able to serialize
  13. transient private String transientVariable = "this is a transient instance field";
  14. private Thread threadClass;
  15. public static void main(String[] args) throws IOException, ClassNotFoundException
  16. {
  17. //Serialization
  18. DemoClass test = new DemoClass();
  19. test.intVariable = 1;
  20. staticVariable = "this is a static variable";
  21. writeOut(test);
  22. System.out.println("DemoClass to be saved: " + test);
  23. //De-serialization
  24. System.out.println("DemoClass deserialized: " + readIn());
  25. }
  26. private static Object readIn() throws IOException, ClassNotFoundException {
  27. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(fileName)));
  28. return ois.readObject();
  29. }
  30. private static void writeOut(java.io.Serializable obj) throws IOException {
  31. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(fileName)));
  32. oos.writeObject(obj);
  33. oos.close();
  34. }
  35. @Override public String toString() {
  36. return "DemoClass: final static fileName=" + fileName + ", final static logger=" + logger
  37. + ", non-final static staticVariable=" + staticVariable + ", instance intVariable=" + intVariable
  38. + ", transient instance transientVariable=" + transientVariable + ", non-serializable instance field threadClass:=" + threadClass;
  39. }
  40. }

程序输出。

  1. DemoClass to be saved: DemoClass:
  2. final static fileName=DemoClassBytes.ser,
  3. final static logger=java.util.logging.LogManager$RootLogger@1d99a4d,
  4. non-final static staticVariable=this is a static variable,
  5. instance intVariable=1,
  6. transient instance transientVariable=this is a transient instance field,
  7. non-serializable instance field threadClass:=null
  8. //Execute readIn() function from a separate main() method
  9. //to get given below output correctly. It will flush out the static fields.
  10. DemoClass deserialized: DemoClass:
  11. final static fileName=DemoClassBytes.ser,
  12. final static logger=java.util.logging.LogManager$RootLogger@cd2c3c,
  13. non-final static staticVariable=null,
  14. instance intVariable=1,
  15. transient instance transientVariable=null,
  16. non-serializable instance field threadClass:=null

如果可序列化的类未显式声明serialVersionUID,则序列化运行时将根据该类的各个方面计算该类的默认serialVersionUID值。

3. 如何生成serialVersionUID

Joshua Bloch 在《Effective Java》中说,自动生成的 UID 是基于类名称,已实现的接口以及所有公共和受保护成员生成的。 以任何方式更改其中任何一个都将更改serialVersionUID

但是,强烈建议所有可序列化的类显式声明serialVersionUID值,因为默认的serialVersionUID计算对类详细信息高度敏感,类详细信息可能会根据编译器的实现而有所不同,并且可以在不同的环境中产生不同的serialVersionUID。 这可能导致反序列化期间出现意外的InvalidClassException

因此,为了在不同的 Java 编译器实现中保证一致的serialVersionUID值,可序列化的类必须声明一个显式的serialVersionUID值。 强烈建议在可能的情况下,显式serialVersionUID声明在serialVersionUID中使用private修饰符,因为此类声明仅适用于立即声明的类。

另请注意,serialVersionUID字段不能用作继承成员。

基于我的短暂职业,我可以说长时间存储序列化数据(空间序列化)并不是很常见的用例。 使用序列化机制将数据临时写入(时间序列化)到例如高速缓存,或通过网络将其发送到另一个程序以利用信息,这是更为常见的。

在这种情况下,我们对保持向后兼容性不感兴趣。 我们只关心确保在网络上通信的代码库确实具有相同版本的相关类。 为了方便进行此类检查,我们必须保持serialVersionUID不变,并且不要对其进行更改。 另外,在网络上的两个应用程序上对类进行不兼容的更改时,请不要忘记更新它。

4. 没有serialVersionUID的 Java 类

这不是我们永远想要面对的情况。 但是,这是现实,有时甚至会发生(我应该很少说吗?)。 如果我们需要以不兼容的方式更改此类,但又想使用该类的旧版本维护序列化/反序列化特性,则可以使用 JDK 工具“serialver”。 该工具在旧类上生成serialVersionUID,并在新类上显式设置它。 不要忘记实现readObject()writeObject()方法,因为内置的反序列化机制(in.defaultReadObject())将拒绝从旧版本的数据中反序列化。

如果我们定义自己的readObject()函数,可以读取回旧数据。 此自定义代码应检查serialVersionUID,以便了解数据所在的版本并决定如何对其进行反序列化。 如果我们存储可以在您的代码的多个版本中保留的序列化数据,则此版本控制技术很有用。

阅读更多: Java 序列化兼容和不兼容的更改

5. Java serialVersionUID– 总结

  1. transientstatic字段在序列化中被忽略。 反序列化之后,transient字段和非最终静态字段将为null
    finalstatic字段仍具有值,因为它们是类数据的一部分。

  2. ObjectOutputStream.writeObject(obj)ObjectInputStream.readObject()用于序列化和反序列化。

  3. 在序列化期间,我们需要处理IOException; 在反序列化期间,我们需要处理IOExceptionClassNotFoundException。 因此,反序列化的类类型必须在类路径中。

  4. 允许使用未初始化的,不可序列化的,非瞬态的实例字段。
    添加“ private Thread th;”时,Serializable没有错误。 但是,“private Thread threadClass = new Thread();”将导致异常:

    1. Exception in thread "main" java.io.NotSerializableException: java.lang.Thread
    2. at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    3. at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source)
    4. at java.io.ObjectOutputStream.writeSerialData(Unknown Source)
    5. at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)
    6. at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    7. at java.io.ObjectOutputStream.writeObject(Unknown Source)
    8. at com.howtodoinjava.demo.serialization.DemoClass.writeOut(DemoClass.java:42)
    9. at com.howtodoinjava.demo.serialization.DemoClass.main(DemoClass.java:27)
  1. 序列化和反序列化可用于复制和克隆对象。 它比常规克隆慢,但是可以很容易地产生深拷贝

  2. 如果我需要序列化SerializableEmployee,但是其超类之一不是可序列化的,Employee类是否仍可以序列化和反序列化? 答案是肯定的,前提是不可序列化的超类具有无参构造器,在反序列化时调用该构造器以初始化该超类。

  3. 我们在修改实现java.io.Serializable的类时必须小心。 如果类不包含serialVersionUID字段,则其serialVersionUID将由编译器自动生成。
    不同的编译器或同一编译器的不同版本将生成潜在的不同值。

  4. serialVersionUID的计算不仅基于字段,而且还基于类的其他方面,例如Implement子句,构造器等。因此,最佳实践是显式声明serialVersionUID字段以保持向后兼容性。 如果我们需要实质性地修改可序列化的类,并希望它与以前的版本不兼容,则需要增加serialVersionUID以避免混合使用不同的版本。

学习愉快!