Serializable
我们知道可以通过实现Serializable这个标记接口来实行Java对象的序列化,进而可以实现持久化缓存或者进行网络传输等操作。
serialVersionUID
- 通过 Serializable 来实现序列化是根据serialVersionUID是否相同来判断序列化对象的版本是否一致。在反序列化时,当JVM判断出字节流中的serialVersionUID与当前class文件中的serialVersionUID不同时就会抛出InvalidClassException异常并返回null;
- 当没有指定serialVersionUID的值的时候,JVM也会根据类名、成员方法及属性名自动生成serialVersionUID。
案发现场
看下面这种情况,有一个用户信息的类需要通过序列化进行缓存。
/**
* @version V1.0
*/
public class UserInfo implements Serializable {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
现在没有指定serialVersionUID,那测试一下能不能实现序列化与反序列化呢?
执行下面的程序
UserInfo userInfo = new UserInfo();
userInfo.setName("LiSi");
userInfo.setPassword("jko044sdf");
serializeObject(userInfo);
UserInfo userInfoSelz= (UserInfo) deserializeObject();
System.out.println(userInfoSelz);
/**
* 序列化对象
*
* @param o 目标对象
*/
public static void serializeObject(Object o) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(SERIAZABLE_TEST));
oos.writeObject(o);
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 反序列化对象
*/
public static Object deserializeObject() {
ObjectInputStream ois = null;
Object o = null;
try {
ois = new ObjectInputStream(new FileInputStream(SERIAZABLE_TEST));
o = ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return o;
}
可以看到成功反序列化:
UserInfo{name='LiSi', password='jko044sdf'}
但随着版本迭代这个版本中需要加入性别字段,如下:
/**
* @version V2.0
*/
public class UserInfo implements Serializable {
private String name;
private String password;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
当我们更新客户端版本时需要去反序列化上个版本的序列化文件来获取name、password进行自动登录操作,这个时候还能反序列化成功吗?
结果反序列化失败了:
null
java.io.InvalidClassException: UserInfo; local class incompatible: stream classdesc
serialVersionUID = -6309591275732240138, local class serialVersionUID = -6832452531586003042
因为类结构的改变导致JVM生成的两个版本的serialVersionUID不一样,此时的class文件是V2版本,从序列化文件字节流中读取的serialVersionUID是V1版本,由于二者不一致,读取失败。
事已至此 如何补救
我们可以在V2版本的UserInfo 中指定serialVersionUID的值为V1的版本的值。那就需要获取V1版本的值:
这个值怎么获取:
1. 通过日志获取 (上面的异常信息中就有stream classdesc serialVersionUID = -6309591275732240138)
2. 回退到V1版本通过ObjectStreamClass获取 :
ObjectStreamClass osc = ObjectStreamClass.lookup(UserInfo.class);
System.out.println("" + osc.getSerialVersionUID());
获取后在V2版本添加serialVersionUID 的值:
private static final long serialVersionUID = -6309591275732240138L;
可成功反序列化:
UserInfo{name='LiSi', password='jko044sdf', sex='null'}
小结
- 未指定serialVersionUID,JVM会根据类名成员方法及属性名自动生serialVersionUID,类结构改变时会导致之前版本的序列化文件反序列化时InvalidClassException并返回null;
- 通过实现Serializable进行序列化操作时,建议指定serialVersionUID 而不是让JVM自动生成;
tips:在 Eclipse中会有生成serialVersionUID的警告提醒。Android Studio和IntelliJ IDEA 则默认是关闭的。可以通过settings|Editor |inspections勾选 Serializable class without ‘serialVersionUID’来打开,然后alt+enter就可以看见 Add ‘serialVersionUID’ field了。