当缺少 serialVersionUID
时,Eclipse会发出警告。
可序列化类Foo没有声明静态的final serialVersionUID 字段,类型为long
serialVersionUID
是什么以及为什么它很重要?请给出一个示例,其中缺少serialVersionUID
将导致问题。
当缺少 serialVersionUID
时,Eclipse会发出警告。
可序列化类Foo没有声明静态的final serialVersionUID 字段,类型为long
serialVersionUID
是什么以及为什么它很重要?请给出一个示例,其中缺少serialVersionUID
将导致问题。
关于 java.io.Serializable
的文档可能是你能找到的最好的解释:
序列化运行时会为每个可序列化的类关联一个版本号,称为
serialVersionUID
。在反序列化过程中,该版本号用于验证序列化对象的发送方和接收方是否已加载与序列化兼容的该对象的类。如果接收方已经加载了与对应发送方的类不同serialVersionUID
的对象类,则反序列化将导致一个InvalidClassException
异常。可序列化的类可以通过声明一个名为serialVersionUID
的静态、常量、类型为long
的字段来显式地声明其自己的serialVersionUID
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果一个可序列化的类没有显式声明 serialVersionUID
,那么序列化运行时会根据该类的各个方面计算出一个默认的 serialVersionUID
值,具体描述详见 Java(TM) Object Serialization Specification。然而,强烈建议所有可序列化的类都要显式声明 serialVersionUID
值,因为默认的 serialVersionUID
计算过程非常敏感,可能会因编译器实现不同而导致意料之外的反序列化异常 InvalidClassExceptions
。因此,为了保证在不同的Java编译器实现之间具有一致的 serialVersionUID
值,可序列化的类必须声明一个显式的 serialVersionUID
值。同时,强烈建议使用 private 修饰符来声明 serialVersionUID
,因为这样的声明仅适用于直接声明的类 —— serialVersionUID
字段作为继承成员并不实用。HTTPSession
,无论它是否被存储,你可能不关心反序列化一个表单对象),那么你可以忽略这个问题。serialVersionUID
代表你的类版本,如果当前版本与其前一版本不兼容,则应增加它。SerialVersionUID
,然后就不用担心它了。serialVersionUID
是最后的手段,这是绝望的建议。 - user207421我不能错过介绍Josh Bloch的书《Effective Java》(第二版)的机会。第10章是Java序列化方面的不可或缺的资源。
根据Josh的说法,自动生成的UID基于类名、实现的接口以及所有公共和受保护成员生成。只要以任何方式更改这些内容,都将更改serialVersionUID
。因此,只有当您确定该类的不止一个版本永远不会被序列化(在进程之间或稍后从存储中检索)时,才无需修改它们。
如果现在忽略它们,并发现稍后需要以某种方式更改类但仍需与旧版本兼容,则可以使用JDK工具serialver在旧类上生成serialVersionUID
,并在新类上明确设置它。(根据您的更改,您可能还需要通过添加writeObject
和readObject
方法来实现自定义序列化 - 请参见Serializable
javadoc或前述第10章。)
您可以告诉Eclipse忽略这些serialVersionUID警告:
窗口 > 首选项 > Java > 编译器 > 错误/警告 > 潜在的编程问题
如果您不知道的话,还有很多其他警告可以在此部分启用(甚至将其报告为错误),其中许多功能非常有用:
还有许多其他的警告。
serialVersionUID
有助于序列化数据的版本控制。在序列化时,其值与数据一起存储。在反序列化时,检查相同的版本以查看序列化数据如何匹配当前代码。
如果要对数据进行版本控制,通常从serialVersionUID
0开始,并在对更改类的结构(添加或删除非瞬态字段)影响序列化数据时递增它。
内置的反序列化机制(in.defaultReadObject()
)将拒绝从旧版本的数据进行反序列化。但是,如果您愿意,可以定义自己的 readObject()函数,该函数可以读取旧数据。然后,此自定义代码可以检查serialVersionUID
以了解数据的版本,并决定如何进行反序列化。如果您存储了经过多个代码版本的序列化数据,则此版本控制技术很有用。
但是,长时间保留序列化数据并不太常见。更常见的情况是使用序列化机制将数据临时写入缓存或通过网络发送到具有相关代码库相同版本的另一个程序。在这种情况下,您不关心维护向后兼容性。您只关心确保通信的代码库确实具有相关类的相同版本。为了方便这样的检查,您必须像以前一样维护serialVersionUID
并在更改类时不要忘记更新它。
如果忘记更新字段,则可能会出现两个具有不同结构但具有相同serialVersionUID
的类的不同版本。如果发生这种情况,则默认机制(in.defaultReadObject()
)将无法检测到任何差异,并尝试反序列化不兼容的数据。现在,您可能会遇到难以理解的运行时错误或静默失败(null字段)。这些类型的错误可能很难找到。
serialVersionUID
。相反,在编译时会生成类结构的哈希值并用作ID。这种机制可以确保您永远不会有相同ID的不同类结构,因此您不会遇到上述难以跟踪的运行时序列化失败问题。SerialVersionUID
是每个类的唯一标识符,JVM
使用它来比较类的版本,确保在序列化期间使用的相同类在反序列化期间被加载。向后不兼容性
],在这种情况下,您只需更改serialVersionUID即可。
Java文档对Serializable的定义中说:
因此,你必须声明serialVersionUID,因为它可以给我们更多的控制。这篇文章提供了一些关于这个话题的好点子。默认的serialVersionUID计算非常敏感于类细节,这些细节可能因编译器实现而异,因此可能导致意外的
InvalidClassException
在反序列化过程中。
serialVersionUID
,可能会导致意想不到的后果。Tom Anderson 对 MetroidFan2002 的回答发表了评论,解释说:“我认为,如果您不是在使用序列化进行永久存储,那么应该使用 @SuppressWarnings 而不是添加值。这样可以减少类的混乱程度,并保留 serialVersionUID 机制保护您免受不兼容更改的能力。” - KirbyserialVersionUID
不是每个类的“唯一标识符”,完全限定类名才是。它是一个版本指示器,用于识别序列化对象的版本。请注意,此处不提供任何解释。 - user207421原始问题要求解释“为什么很重要”和“何时使用序列版本号”的例子,那么我找到了一个。
比方说你创建了一个Car
类,将其实例化并写入对象流。这个压缩后的汽车对象在文件系统中存在了一段时间。同时,如果Car
类被修改并添加了一个新字段。后来,当你尝试读取(即反序列化)压缩后的Car
对象时,你会得到java.io.InvalidClassException
,因为所有可序列化的类都会自动分配一个唯一标识符。当类的标识符与压缩对象的标识符不相等时,就会抛出此异常。如果你认真思考,会发现这个异常是由于添加了新字段而引起的。你可以通过声明显式的serialVersionUID
来控制版本,从而避免抛出此异常。显式声明serialVersionUID
也有一定的性能优势(因为不需要计算)。因此,最佳实践是在创建可序列化类时立即添加自己的serialVersionUID
,如下所示:
public class Car {
static final long serialVersionUID = 1L; //assign a long value
}
1
等相冲突。 - VadzimserialVersionUID
与“找到正确的类版本”没有任何关系。 - user207421首先,我需要解释一下什么是序列化。
序列化允许将对象转换为流,以便通过网络发送该对象或保存到文件或保存到数据库以备后用。
序列化有一些规则:
只有类或其超类实现了Serializable接口的对象才可以序列化。
即使其超类没有实现Serializable接口,一个对象本身也可以进行序列化。但是,在可序列化类层次结构中第一个未实现Serializable接口的超类必须具有无参数构造函数。如果违反此规则,则readObject()方法会在运行时产生java.io.InvalidClassException异常。
所有基本类型都可以序列化。
瞬态字段(使用transient修饰)不会被序列化(即不会被保存或还原)。实现Serializable接口的类必须标记那些不支持序列化的类的瞬态字段(例如文件流)。
静态字段(使用static修饰)不会被序列化。
当对Object
进行序列化时,Java运行时将关联序列版本号,即serialVersionID
。
我们何时需要使用serialVersionID:
在反序列化期间,用于验证发送方和接收方在序列化方面是否兼容。如果接收方使用不同的serialVersionID
加载了类,则反序列化将以InvalidClassCastException
结束。
可序列化类可以通过声明一个名为serialVersionUID
的字段(必须是static、final类型的long型字段)来显式声明自己的serialVersionUID
。
让我们通过一个示例来尝试一下。
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() {
return getEmpName() + " is " + getEmpAge() + "years old";
}
}
创建序列化对象。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");
}
}
反序列化对象
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类的serialVersionUID并保存:
private static final long serialVersionUID = 4L;
执行Reader类,不要执行Writer类,否则会引发异常。
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)
如果您永远不需要将对象序列化为字节数组并发送/存储它们,那么您就不必担心这个问题。但是如果您需要这样做,那么您必须考虑您的 serialVersionUID,因为对象的反序列化器将使用它来匹配其类加载器版本的对象。请在Java语言规范中了解更多相关信息。
所以,代替继承:
public class MyExample extends ArrayList<String> {
public MyExample() {
super();
}
...
}
做。public class MyExample {
private List<String> myList;
public MyExample() {
this.myList = new ArrayList<String>();
}
...
}
在相关方法中调用myList.foo()
而不是this.foo()
(或super.foo()
)。这并非适用于所有情况,但仍然很常见。
我经常看到人们扩展JFrame等类,当他们实际上只需要委托给它时。(这也有助于在IDE中自动完成,因为JFrame有数百个方法,而当您想要在您的类上调用自定义方法时,您并不需要它们。)
其中一个情况下无法避免警告(或serialVersionUID)是当您从AbstractAction继承时,在一个匿名类中通常只添加了actionPerformed方法。我认为在这种情况下不应该有警告(因为通常不能在不同版本的类之间可靠地序列化和反序列化这样的匿名类),但我不确定编译器如何识别这一点。
__AUTOLOAD
函数,但我不知道。 - M_M