文章目录
1.这是什么?
简单概括而言, serialVersionUID 是用于在序列化和反序列化过程中进行核验的一个版本号。
序列化运行时将一个版本号(称为serialVersionUID)与 每个可序列化类相关联,该版本号在反序列化期间用于验证序列化对象的发送方和接收方是否为该对象加载了与序列化兼容的类。
如果接收方为对象加载的类与相应发送方类的serialVersionId不同,则反序列化将导致InvalidClassException。
可序列化类可以通过声明名为 serialVersionUID 的字段显式声明自己的 serialVersionUID,且该字段必须是static、final的且类型为long:
static final long serialVersionUID = 1L;;
2.如果不使用会发生什么?
如Java(TM)对象序列化规范中所讲述的,如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据类的各个方面计算该类的默认serialVersionUID值。
但是,强烈建议所有可序列化类显式声明serialVersionUID值,因为默认的 serialVersionUID 计算对类详细信息高度敏感,这些详细信息可能因编译器实现而异,因此在反序列化过程中可能会导致意外的InvalidClassExceptions。
因此,为了保证在不同的java编译器实现中SerialVersionId值是一致的,可序列化类必须声明一个显式的SerialVersionId值。还强烈建议显式 serialVersionUID 声明尽可能使用 private 修饰符,因为此类声明仅适用于立即声明的类——serialVersionUID字段不可用作继承成员。
3.代码结合讲解
3.1不设置serialVersionUID时正常序列化和反序列化
先定义一个实体 Student.class,需要实现 Serializable 接口,但是不需要实现get(),set()方法
注意: 这里必须实现Serializable 接口
import java.io.Serializable;
public class Student implements Serializable {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
测试类,思路是先把 Student对象 序列化到 Student.txt 文件,然后再将 Student.txt 文件反序列化成对象,输出。
public class SerialTest {
public static void main(String[] args) {
serial();
deserial();
}
// 序列化
private static void serial(){
Student student = new Student(9, "Mike");//注意这里写入的值
try {
FileOutputStream fileOutputStream = new FileOutputStream("Student.txt");
ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(student);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
// 反序列化
private static void deserial() {
try {
FileInputStream fis = new FileInputStream("Student.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Student student = (Student) ois.readObject();
ois.close();
System.out.println(student.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果,序列化文件我们可以看到 Student.txt ,反序列化出来,里面的字段都是不变的,说明反序列化成功了。
在不改变类文件的字段的情况下序列化后在反序列化是可以成功的.
3.2 类增加字段后进行反序列化?
序列化之后,类文件增加了字段,反序列化会怎么样? 答案是: 会失败
我们在 Student.java 中增加了一个属性 score ,重新生成了toString()方法。
package com.aphysia.normal;
import java.io.Serializable;
public class Student implements Serializable {
private int age;
private String name;
private int score;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
然后重新调用 deserial() 方法,会报错:
注意事项: 只是调用 deserial()方法,不要进行序列化, 只使用 deserial()进行反序列化
java.io.InvalidClassException: com.aphysia.normal.Student; local class incompatible: stream classdesc serialVersionUID = 7488921480006384819, local class serialVersionUID = 6126416635811747983
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1963)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1829)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2120)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1646)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:482)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:440)
at com.aphysia.normal.SerialTest.deserial(SerialTest.java:26)
at com.aphysia.normal.SerialTest.main(SerialTest.java:7)
从上面的报错信息中,我们可以看到,类型不匹配,主要是因为serialVersionUID变化了
问题来了:: 为什么没有设置serialVersionUID但是却发生了变化?
答: 正是因为没有设置,所以变化了,因为我们增加了一个字段score,如果我们不设置serialVersionUID,系统就会自动生成,自动生成有风险,就是我们的字段类型或者长度改变(新增或者删除的时候),自动生成的serialVersionUID会发生变化,那么以前序列化出来的对象,反序列化的时候就会失败。
实测:序列化完成之后,如果原类型字段减少,不指定serialVersionUID的情况下,也是会报不一致的错误。
3.3手动设置serialVersionUID的结果
我们可以手动设置一个 serialVersionUID
然后执行序列化,序列化出文件 Student.txt 后,增加一个字段score,执行反序列化。是可以成功的…只是新增的字段是默认值null。
所以今后考虑到迭代的问题的时候,一般可能增加字段或者减少字段,都是需要考虑兼容问题的,所以最好是自己指定serialVersionUID,而不是由系统自动生成。自动生成的,由于类文件变化,它也会发生变化,就会出现不一致的问题,导致反序列化失败。
实测:如果我减少了字段,只要指定了serialVersionUID,也不会报错
4.serialVersionUID的生成
serialVersionUID是为了兼容不同版本的,在JDK中,可以利用JDK的bin目录下的serialver.exe工具产生这个serialVersionUID,对于Student.class,执行命令:serialver Student。
IDEA生成实际上也是调用这个命令,代码调用可以这样写:
ObjectStreamClass c = ObjectStreamClass.lookup(Student.class);
long serialID = c.getSerialVersionUID();
System.out.println(serialID);
如果不显式的指定,那么不同JVM之间的移植可能也会出错,因为不同的编译器,计算这个值的策略可能不同,计算类没有修改,也会出现不一致的问题。getSerialVersionUID()源码如下:
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
可以看到上面是使用了一个内部类的方式,使用特权计算computeDefaultSUID():
private static long computeDefaultSUID(Class<?> cl) {
// 代理
if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl))
{
return 0L;
}
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
// 类名
dout.writeUTF(cl.getName());
// 修饰符
int classMods = cl.getModifiers() &
(Modifier.PUBLIC | Modifier.FINAL |
Modifier.INTERFACE | Modifier.ABSTRACT);
// 方法
Method[] methods = cl.getDeclaredMethods();
if ((classMods & Modifier.INTERFACE) != 0) {
classMods = (methods.length > 0) ?
(classMods | Modifier.ABSTRACT) :
(classMods & ~Modifier.ABSTRACT);
}
dout.writeInt(classMods);
if (!cl.isArray()) {
// 继承的接口
Class<?>[] interfaces = cl.getInterfaces();
String[] ifaceNames = new String[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
ifaceNames[i] = interfaces[i].getName();
}
// 接口名
Arrays.sort(ifaceNames);
for (int i = 0; i < ifaceNames.length; i++) {
dout.writeUTF(ifaceNames[i]);
}
}
// 属性
Field[] fields = cl.getDeclaredFields();
MemberSignature[] fieldSigs = new MemberSignature[fields.length];
for (int i = 0; i < fields.length; i++) {
fieldSigs[i] = new MemberSignature(fields[i]);
}
Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.name.compareTo(ms2.name);
}
});
for (int i = 0; i < fieldSigs.length; i++) {
// 成员签名
MemberSignature sig = fieldSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE |
Modifier.TRANSIENT);
if (((mods & Modifier.PRIVATE) == 0) ||
((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0))
{
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature);
}
}
// 是否有静态初始化
if (hasStaticInitializer(cl)) {
dout.writeUTF("<clinit>");
dout.writeInt(Modifier.STATIC);
dout.writeUTF("()V");
}
// 构造器
Constructor<?>[] cons = cl.getDeclaredConstructors();
MemberSignature[] consSigs = new MemberSignature[cons.length];
for (int i = 0; i < cons.length; i++) {
consSigs[i] = new MemberSignature(cons[i]);
}
Arrays.sort(consSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.signature.compareTo(ms2.signature);
}
});
for (int i = 0; i < consSigs.length; i++) {
MemberSignature sig = consSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF("<init>");
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}
MemberSignature[] methSigs = new MemberSignature[methods.length];
for (int i = 0; i < methods.length; i++) {
methSigs[i] = new MemberSignature(methods[i]);
}
Arrays.sort(methSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
int comp = ms1.name.compareTo(ms2.name);
if (comp == 0) {
comp = ms1.signature.compareTo(ms2.signature);
}
return comp;
}
});
for (int i = 0; i < methSigs.length; i++) {
MemberSignature sig = methSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}
dout.flush();
MessageDigest md = MessageDigest.getInstance("SHA");
byte[] hashBytes = md.digest(bout.toByteArray());
long hash = 0;
for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
hash = (hash << 8) | (hashBytes[i] & 0xFF);
}
return hash;
} catch (IOException ex) {
throw new InternalError(ex);
} catch (NoSuchAlgorithmException ex) {
throw new SecurityException(ex.getMessage());
}
}
从上面这段源码大致来看,其实这个计算serialVersionUID,基本是将类名,属性名,属性修饰符,继承的接口,属性类型,名称,方法,静态代码块等等…这些都考虑进去了,都写到一个DataOutputStream中,然后再做hash运算,所以说,这个东西得指定啊,不指定的话,稍微一改类的东西,就变了…
而且这个东西指定了,没啥事,不要改!!!除非你确定两个版本就不兼容!!!
参考文章:
https://zhuanlan.zhihu.com/p/347246506
serialVersionUID作用是什么以及如何生成的?