关于Java:什么是SerialValueUID,为什么我要使用它?

What is a serialVersionUID and why should I use it?

当一个serialVersionUID被遗忘的时候,日蚀问题警告。

The serializable class Foo does not declare a static final
serialVersionUID field of type long

为什么这很重要?请举例说明在哪儿失踪的serialVersionUIDWill造成了问题。


EDOCX1[0]的文档可能是一个很好的解释,因为您将得到:

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
InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field named serialVersionUID that must be static, final, and of type long:

1
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

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 strongly recommended 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 InvalidClassExceptions 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 private modifier where possible, since such declarations apply only to the immediately declaring class serialVersionUID fields are not useful as inherited members.


如果您只是因为必须为实现而序列化而序列化(例如,谁关心您是否为HTTPSession序列化),那么您可能不关心表单对象de-serializing,那么您可以忽略这一点。

如果您实际使用的是序列化,那么只有计划直接使用序列化存储和检索对象时,这才重要。serialVersionUID表示类版本,如果类的当前版本与以前的版本不向后兼容,则应增加该版本。

大多数情况下,您可能不会直接使用序列化。如果是这种情况,请单击"快速修复"选项生成默认的serialVersionUID,不要担心。


我不能错过这个机会来插入Josh Bloch的书《有效Java(第二版)》。第11章是Java序列化不可或缺的资源。

根据josh,自动生成的uid是基于类名、实现的接口以及所有公共和受保护的成员生成的。以任何方式改变其中的任何一个都会改变serialVersionUID。因此,只有当您确定类的任何一个版本都不会被序列化(跨进程或在以后从存储中检索)时,才不需要处理它们。

如果您现在忽略它们,稍后发现您需要以某种方式更改类,但需要与类的旧版本保持兼容性,那么您可以使用JDK工具serialver在旧类上生成serialVersionUID,并在新类上显式设置它。(根据您的更改,您可能还需要通过添加writeObjectreadObject方法来实现自定义序列化—请参阅Serializablejavadoc或前面的第11章。)


您可以告诉Eclipse忽略这些serialversionuid警告:

Window > Preferences > Java > Compiler > Errors / Warnings > Potential Programming Problems

如果您不知道,您可以在本节中启用许多其他警告(甚至有一些报告为错误),许多警告非常有用:

  • 潜在编程问题:可能的意外布尔赋值
  • 潜在的编程问题:空指针访问
  • 不必要的代码:从不读取局部变量
  • 不必要的代码:冗余的空检查
  • 不必要的代码:不必要的强制转换或"instanceof"

还有更多。


serialVersionUID有助于序列化数据的版本控制。它的值在序列化时与数据一起存储。在反序列化时,将检查相同的版本,以查看序列化数据如何与当前代码匹配。

如果要对数据进行版本化,通常从0的serialVersionUID开始,并在对类的每次结构更改中对其进行碰撞,从而更改序列化数据(添加或删除非瞬态字段)。

内置的反序列化机制(in.defaultReadObject()将拒绝从旧版本的数据反序列化。但是如果你想,你可以定义你自己的readObject()-函数,它可以读取旧数据。然后,该自定义代码可以检查serialVersionUID,以了解数据所在的版本,并决定如何对其进行反序列化。如果您存储序列化数据(这些数据在代码的多个版本中仍然存在),则此版本控制技术非常有用。

但是,在如此长的时间跨度内存储序列化数据并不常见。使用序列化机制将数据临时写入缓存或通过网络将其发送到具有相同版本代码库相关部分的另一个程序,这一点更为常见。

在这种情况下,您对保持向后兼容性不感兴趣。您只关心确保正在通信的代码基确实具有相关类的相同版本。为了方便这种检查,您必须像以前一样维护serialVersionUID,并且在对类进行更改时不要忘记更新它。

如果您确实忘记更新字段,那么您可能会得到具有不同结构但具有相同serialVersionUID的类的两个不同版本。如果发生这种情况,默认机制(in.defaultReadObject()将不会检测到任何差异,并尝试对不兼容的数据进行反序列化。现在,您可能会以一个神秘的运行时错误或无声故障(空字段)结束。这些类型的错误可能很难找到。

因此,为了帮助这个用例,Java平台为您提供了不手动设置EDCOX1 OR 0的选择。相反,类结构的散列将在编译时生成并用作ID。此机制将确保您从不具有具有具有相同ID的不同类结构,因此您不会得到上述难以跟踪的运行时序列化失败。

但是自动生成的ID策略有一个背面。也就是说,同一类的生成ID可能在编译器之间有所不同(如上面的jon skeet所提到的)。因此,如果在使用不同编译器编译的代码之间通信序列化数据,建议无论如何手动维护ID。

如果您像前面提到的第一个用例那样向后兼容您的数据,那么您可能也希望自己维护这个ID。这是为了获得可读的ID,并更好地控制它们的更改时间和方式。


What is a serialVersionUID and why should I use it?

SerialVersionUID是每个类的唯一标识符,JVM使用它来比较类的版本,确保在反序列化期间加载相同的类。

指定一个可以提供更多的控制,但如果不指定,则JVM会生成一个控制。生成的值在不同的编译器之间可能不同。此外,有时出于某种原因,您只希望禁止反序列化旧的序列化对象[backward incompatibility,在这种情况下,您只需更改serialversionID即可。

用于Serializable的javadocs说:

the default serialVersionUID computation is highly sensitive to class
details that may vary depending on compiler implementations, and can
thus result in unexpected InvalidClassExceptions during
deserialization.

因此,您必须声明serialversionID,因为它提供了更多的控制权。

本文在这个主题上有一些好的观点。


最初的问题是"为什么它很重要"和"示例",这个Serial Version ID在哪里有用。我找到了一个。

假设您创建了一个Car类,将其实例化,并将其写出一个对象流。扁平的汽车对象在文件系统中放置了一段时间。同时,如果通过添加新字段来修改Car类。稍后,当您试图读取(即反序列化)扁平的Car对象时,您会得到java.io.InvalidClassException,因为所有可序列化类都会自动获得唯一的标识符。当类的标识符不等于扁平对象的标识符时,会引发此异常。如果您真的考虑过它,那么会因为添加了新字段而引发异常。通过声明显式的serialversionID来控制自己的版本控制,可以避免引发此异常。在显式声明您的serialVersionUID时还有一点性能优势(因为不需要计算)。因此,最好的做法是在创建可序列化类后立即将自己的serialversionID添加到可序列化类中,如下所示:

1
2
3
public class Car {
    static final long serialVersionUID = 1L; //assign a long value
}


如果您永远不需要将对象序列化到字节数组并发送/存储它们,那么您不需要担心它。如果这样做,则必须考虑您的serialversionID,因为对象的反序列化程序将其与类加载器所具有的对象版本相匹配。在Java语言规范中阅读更多关于它的内容。


如果你在一个你从来没有想过要序列化的类上得到这个警告,并且你没有声明自己implements Serializable,那么这通常是因为你继承了一个实现可序列化的超类。通常情况下,最好委托给这样的对象,而不是使用继承。

所以,而不是

1
2
3
4
5
6
7
public class MyExample extends ArrayList<String> {

    public MyExample() {
        super();
    }
    ...
}

1
2
3
4
5
6
7
8
public class MyExample {
    private List<String> myList;

    public MyExample() {
         this.myList = new ArrayList<String>();
    }
    ...
}

在相关方法中,称为myList.foo(),而不是this.foo()(或super.foo())。(这并不适用于所有情况,但仍然非常常见。)

我经常看到人们扩展JFrame或类似的东西,当他们真的只需要授权给它的时候。(这也有助于在IDE中自动完成,因为JFrame有数百个方法,当您想在类中调用自定义方法时,不需要这些方法。)

警告(或serialversionID)不可避免的一种情况是,当您从抽象操作进行扩展时,通常是在匿名类中,只添加actionPerformed方法。我认为在这种情况下不应该出现警告(因为通常情况下,在类的不同版本中,您无法可靠地序列化和反序列化此类匿名类),但我不确定编译器如何识别这一点。


要理解字段serialversionID的重要性,应该了解序列化/反序列化是如何工作的。

当序列化类对象被序列化时,Java运行时将串行版本号(称为SerialValueUID)与序列化对象关联起来。在反序列化此序列化对象时,Java运行时将序列化对象的SerialValueUID与类的SerialValueUnID匹配。如果两者都相等,则只进行进一步的反序列化过程,否则将引发InvalidClassException。

因此,我们得出结论,要使序列化/反序列化过程成功,序列化对象的serialversionID必须与类的serialversionID等效。如果程序员在程序中显式地指定serialversionuid值,那么无论序列化和反序列化平台如何,相同的值都将与序列化对象和类关联(例如,可以在类似Windows的平台上使用Sun或MS JVM进行序列化,反序列化可能不同。使用zing-jvm租用平台linux)。

但是,如果在程序员没有指定SerialValueUID的情况下,在执行任何对象的序列化反序列化时,Java运行时使用它自己的算法来计算它。这种serialversionuid计算算法因JRE的不同而有所不同。也可能是,对象序列化的环境使用一个JRE(例如Sun JVM),而反序列化发生的环境使用Linux JVM(zing)。在这种情况下,与序列化对象关联的serialversionID将不同于在反序列化环境中计算的类的serialversionID。反过来,反序列化将不会成功。因此,为了避免这种情况/问题,程序员必须始终指定可序列化类的serialversionuid。


不用担心,默认的计算非常好,可以满足999999%的情况。如果你遇到了问题,你可以——正如前面所说的——在需求到达时引入uid(这是非常不可能的)


例如,缺少的serialversionID可能导致问题:

我正在研究一个JavaEE应用程序,它由一个使用EDCOX1×6模块的Web模块组成。web模块远程调用EJB模块,并传递一个实现SerializablePOJO作为参数。

这个POJO's类被打包在EJB JAR中,并在Web模块的WEB-INF/lib中的自己的JAR中。它们实际上是相同的类,但是当我打包EJB模块时,我将这个pojo的jar解包,以便将它与EJB模块一起打包。

EJB的调用失败了,但下面有一个例外,因为我没有声明它的serialVersionUID

1
2
3
4
5
Caused by: java.io.IOException: Mismatched serialization UIDs : Source
 (Rep.
 IDRMI:com.hordine.pedra.softbudget.domain.Budget:5CF7CE11E6810A36:04A3FEBED5DA4588)
 = 04A3FEBED5DA4588 whereas Target (Rep. ID RMI:com.hordine.pedra.softbudget.domain.Budget:7AF5ED7A7CFDFF31:6227F23FA74A9A52)
 = 6227F23FA74A9A52

首先,我需要解释序列化。序列化允许将对象转换为流,以便通过网络发送该对象,或保存到文件或保存到数据库以供字母使用。

有一些序列化规则。

  • 对象只有在其类或其超类实现可序列化接口时才可序列化。

  • 对象是可序列化的(它本身实现了可序列化的接口),即使它的超类不是。但是,可序列化类层次结构中未实现可序列化接口的第一个超类必须具有无参数构造函数。如果违反了这一点,readObject()将在运行时生成java.io.InvalidClassException

  • 所有基元类型都是可序列化的。

  • 瞬态字段(带有瞬态修改器)未序列化(即未保存或还原)。实现可序列化的类必须标记不支持序列化的类(例如,文件流)的临时字段。

  • 静态字段(带有静态修饰符)未序列化。

当对象序列化时,Java运行时将串行版本号称为SerialValueNoID。

在反序列化过程中,我们需要serialversionid:以验证发送方和接收方是否与序列化兼容。如果接收方使用不同的serialversionid加载了类,则反序列化将以invalidClassCastException结束。可序列化类可以通过声明名为"serialversionuid"的字段来显式声明自己的serialversionuid,该字段必须是静态的、最终的且类型为long:。

让我们用一个例子来试试这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.Serializable;    
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String empname;
private byte empage;

public String getEmpName() {
    return name;
}
public void setEmpName(String empname) {
    this.empname = empname;
}
public byte getEmpAge() {
    return empage;
}
public void setEmpAge(byte empage) {
    this.empage = empage;
}

public String whoIsThis() {
    StringBuffer employee = new StringBuffer();
    employee.append(getEmpName()).append(" is ).append(getEmpAge()).append("
years old "));
    return employee.toString();
}
}

创建序列化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Writer {
public static void main(String[] args) throws IOException {
    Employee employee = new Employee();
    employee.setEmpName("Jagdish");
    employee.setEmpAge((byte) 30);

    FileOutputStream fout = new
FileOutputStream("/users/Jagdish.vala/employee.obj");
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(employee);
    oos.close();
    System.out.println("Process complete");
}
}

反序列化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Reader {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
    Employee employee = new Employee();
    FileInputStream fin = new
    FileInputStream("/users/Jagdish.vala/employee.obj");
    ObjectInputStream ois = new ObjectInputStream(fin);
    employee = (Employee) ois.readObject();
    ois.close();
    System.out.println(employee.whoIsThis());
 }
}

注意:现在更改Employee类的serialversionID并保存:

1
private static final long serialVersionUID = **4L**;

执行读卡器类。不执行编写器类,您将得到异常。

1
2
3
4
5
6
7
8
9
10
Exception in thread"main" java.io.InvalidClassException:
com.jagdish.vala.java.serialVersion.Employee; local class incompatible:
stream classdesc serialVersionUID = 1, local class serialVersionUID = 4
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at com.krishantha.sample.java.serialVersion.Reader.main(Reader.java:14)

我通常在一个上下文中使用EDCOX1 0,当我知道它将离开Java VM的上下文时。

当我将ObjectInputStreamObjectOutputStream用于我的应用程序时,或者如果我知道我使用的库/框架将使用它,我就会知道这一点。SerialValueNID确保不同版本或销售商的不同Java VM将正确地相互操作,或者如果它在VM之外存储和检索,例如EDCOX1〔7〕,会话数据甚至可以在应用服务器的重新启动和升级期间保留。

对于所有其他情况,我使用

1
@SuppressWarnings("serial")

因为大多数情况下,默认的SerialVersionUID就足够了。这包括ExceptionHttpServlet


字段数据表示类中存储的一些信息。类实现Serializable接口,因此Eclipse自动提供声明serialVersionUID字段。让我们从这里设置的值1开始。

如果您不希望该警告出现,请使用:

1
@SuppressWarnings("serial")

serialversionID用于对象的版本控制。也可以在类文件中指定serialversionuid。不指定serialversionuid的结果是,在类中添加或修改任何字段时,已经序列化的类将无法恢复,因为为新类和旧序列化对象生成的serialversionuid将不同。Java序列化过程依赖于正确的SerialValueUID来恢复序列化对象的状态,并且在SerialValueUID错配的情况下抛出JavaIO.ValueCaseExpRebug。

阅读更多:http://javarevisited.blogspot.com/2011/04/top-10-java-serialization-interview.html ixzz3vqxnpopz


如果checkStyle可以验证实现serializable的类上的serialversionID是否具有良好的值,即它是否与串行版本ID生成器生成的值匹配,那就更好了。例如,如果您有一个具有大量可序列化DTO的项目,记住要删除现有的serialversionID并重新生成它是一件痛苦的事情,而且目前唯一(我知道的)验证这一点的方法是为每个类重新生成并与旧类进行比较。这是非常痛苦的。


为什么在Java中使用EDOCX1 0内EDCOX1?1类?

在EDCOX1(2)中,Java运行时创建一个类的版本号,以便以后可以对其进行序列化。这个版本号在Java中被称为EDCOX1(0)。

SerialVersionUID用于版本化序列化数据。只有当类的SerialVersionUID与序列化实例匹配时,才能反序列化该类。当我们在我们的类中不声明EDCOX1×0时,Java运行时为我们生成它,但不推荐它。建议将SerialVersionUID声明为private static final long变量,以避免出现默认机制。

当您通过实现标记接口EDCOX1(10)来声明类为EDCOX1(1)时,Java运行库通过使用默认序列化机制将该类持久化到磁盘中,前提是您没有使用EDCOX1 OR 11接口来定制该进程。

请参见为什么在Java中使用SerialValueUID在可序列化类中

代码:javassist.serialversionuid


如果你想修改大量的没有serialversionuid设置的类,同时保持与旧类的兼容性,像intellij-idea、eclipse这样的工具就会出现问题,因为它们会生成随机数,并且不能一次性处理一堆文件。我想出了以下bash脚本(我对Windows用户感到抱歉,考虑购买Mac或转换为Linux),以便轻松地修改serialversionuid问题:

1
2
3
4
5
6
7
8
9
10
11
12
base_dir=$(pwd)                                                                  
src_dir=$base_dir/src/main/java                                                  
ic_api_cp=$base_dir/target/classes                                              

while read f                                                                    
do                                                                              
    clazz=${f//\//.}                                                            
    clazz=${clazz/%.java/}                                                      
    seruidstr=$(serialver -classpath $ic_api_cp $clazz | cut -d ':' -f 2 | sed -e 's/^\s\+//')
    perl -ni.bak -e"print $_; printf qq{%s
}, q{    private $seruidstr} if /public class/"
$src_dir/$f
done

如果保存了这个脚本,请说add_serialversionuid.sh to you~/bin。然后在Maven或Gradle项目的根目录中运行它,如下所示:

1
add_serialVersionUID.sh < myJavaToAmend.lst

LST包括以以下格式添加SerialValueUID的Java文件列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
com/abc/ic/api/model/domain/item/BizOrderTransDO.java
com/abc/ic/api/model/domain/item/CardPassFeature.java
com/abc/ic/api/model/domain/item/CategoryFeature.java
com/abc/ic/api/model/domain/item/GoodsFeature.java
com/abc/ic/api/model/domain/item/ItemFeature.java
com/abc/ic/api/model/domain/item/ItemPicUrls.java
com/abc/ic/api/model/domain/item/ItemSkuDO.java
com/abc/ic/api/model/domain/serve/ServeCategoryFeature.java
com/abc/ic/api/model/domain/serve/ServeFeature.java
com/abc/ic/api/model/param/depot/DepotItemDTO.java
com/abc/ic/api/model/param/depot/DepotItemQueryDTO.java
com/abc/ic/api/model/param/depot/InDepotDTO.java
com/abc/ic/api/model/param/depot/OutDepotDTO.java

这个脚本在hood下使用jdk serialver工具。所以确保您的$java_home/bin在路径中。


这个问题在Joshua Bloch的Java中得到了很好的证明。一本好书,一本必读的书。我将概述以下一些原因:

序列化运行时为每个可序列化类提供一个称为序列版本的数字。这个数字叫做serialversionuid。现在这个数字后面有一些数学,它是基于类中定义的字段/方法得出的。对于同一类,每次都会生成相同的版本。此数字在反序列化期间用于验证序列化对象的发送方和接收方是否已为该对象加载了与序列化兼容的类。如果接收器为与对应发送方类的serialversionID不同的对象加载了一个类,则反序列化将导致InvalidClassException。

如果类是可序列化的,则还可以通过声明名为"serialversionuid"的字段来显式声明自己的serialversionuid,该字段必须是静态、最终和long类型。大多数类似Eclipse的IDE都可以帮助您生成这么长的字符串。


每次序列化一个对象时,该对象都会被标记为该对象类的版本ID号。此ID称为serialversionID,它是根据有关类结构的信息计算的。假设您创建了一个Employee类,它的版本ID为333(由JVM分配),现在当您序列化该类的对象(假设Employee对象)时,JVM会将uid分配给它作为333。

考虑一种情况——将来您需要编辑或更改类,在这种情况下,当您修改它时,JVM将为它分配一个新的uid(假设444)。现在,当您尝试反序列化Employee对象时,jvm会将序列化对象(Employee对象)的版本ID(333)与类(即444)的版本ID(因为它被更改)进行比较。相比之下,jvm会发现两个版本的uid不同,因此反序列化将失败。因此,如果每个类的serialversionid是由程序员自己定义的。即使类是在将来演化的,它也将是相同的,因此,即使类是更改的,JVM也会始终发现类与序列化对象兼容。有关更多信息,您可以参考头14 Java的第四章。


简单解释:

  • 您正在序列化数据吗?

    序列化基本上是将类数据写入文件/流等。反序列化是将该数据读取回类。

  • 你打算投入生产吗?

    如果您只是使用不重要/假的数据测试某些东西,那么不要担心它(除非您直接测试序列化)。

  • 这是第一个版本吗?

    如果是,则设置serialVersionUID=1L

  • 这是第二、第三等产品版本吗?

    现在你需要担心serialVersionUID,应该深入研究它。

  • 基本上,如果在更新需要写入/读取的类时没有正确更新版本,则在尝试读取旧数据时会出错。


    长话短说,此字段用于检查序列化数据是否可以正确反序列化。序列化和反序列化通常由程序的不同副本进行,例如服务器将对象转换为字符串,客户端将接收到的字符串转换为对象。这个字段告诉我们,这两个操作对于这个对象是什么有着相同的想法。此字段有助于:

    • 您在不同的地方有许多不同的程序副本(比如1个服务器和100个客户机)。如果您要更改对象、更改版本号并忘记更新一个这个客户机,它将知道他不能反序列化。

    • 您已将数据存储在某个文件中,稍后尝试使用已修改对象的程序的更新版本打开它-如果您保持正确的版本,您将知道此文件不兼容。

    什么时候重要?

    最明显的是,如果向对象添加一些字段,旧版本将无法使用它们,因为它们的对象结构中没有这些字段。

    不太明显-反序列化对象时,字符串中不存在的字段将保持为空。如果已从对象中删除字段,则旧版本会将此字段保留为allways空值,如果旧版本依赖于此字段中的数据,则可能会导致错误行为(无论如何,您创建此字段是为了某种目的,而不仅仅是为了好玩):-)

    最不明显的是-有时你会改变你在某个领域的意义。例如,当你12岁的时候,你的意思是"自行车"下的"自行车",但当你18岁的时候,你的意思是"摩托车"——如果你的朋友邀请你"骑车横穿城市",而你将是唯一一个骑自行车来的人,你将不明白跨领域保持同样的意思有多重要:—)