最近在做超市管理项目,通过序列化将数据存储在Redis数据库中。在实现用户信息的修改功能时,为了方便将Date数据类型修改为String类型结果报错了,打开控制台发现出现下面的错误。
java.io.InvalidClassException: com.li.pojo.SmbmsUser;
local class incompatible: stream classdesc serialVersionUID = 2416888619525883151,
local class serialVersionUID = 516098953370879925
错误原因:序列化与反序列化前后ID不一致导致报错。
既然出现了错误就要解决。之前对于序列化、反序列化的理解只是停留在会用的基础上
今天就深入了解。
1.首先知道什么是序列化与反序列化
学习新的知识点的时候我们要时刻抱有疑问:是什么、为什么、怎么做、三个方面去完成这件事。这样才会清晰明了,记忆也更加深刻。
-
什么是序列化与反序列化呢?
序列化:就是将对象转化为字符序列
反序列化:就是将字符序列恢复为原来的对象
-
为什么要序列化、反序列化呢?
前面提到在做ssm项目的时候使用了序列化将对象转化为了字符序列存储在了Redis数据库中,
如果不使用序列化我们应该怎么做,使用map(key-value)存储对象,单表数据不是特别多的时候可以去使用,数据量大会很麻烦。所以建议数据量特别大的时候序列化对象存储在Redis中
一般序列化用于被写入数据库、文件、用于网络传输。 -
怎么做,才能序列化呢?
实现Serializable接口
public interface Serializable {
}
Serializable 分明就是一个空接口,其实这个接口的作用就是告诉JVM我要序列化、反序列化。
具体操作如下
准备实体类
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
//初始化
Student student = new Student();
student.setName("张三");
student.setAge(12);
System.out.println(student.toString());
//序列化
try {
//把对象写入文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
oos.writeObject(student);
System.out.println("序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
//反序列化
try {
//从文件中读出对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
Student stu = (Student) ois.readObject();
System.out.println("反序列化成功!");
System.out.println(stu.toString());
} catch (Exception e) {
e.printStackTrace();
}
没有实现Serializable接口报错。
java.io.NotSerializableException: com.li.pojo.Student
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.li.test.SerializableTest.serializable(SerializableTest.java:25)
at com.li.test.SerializableTest.main(SerializableTest.java:14)
实现Serializable接口
public class Student implements Serializable{
//省略属性
//省略setter、getter方法
//省略toString方法
}
序列化之前打印对象 :
Student{name=‘张三’, age=12}
序列化成功! 并且b.txt 有一些内容
接着调用反序列化,并且输出对象
反序列化成功!
Student{name=‘张三’, age=12}
反序列化以后发现与序列化之前的对象保持一致!
2.序列化与反序列化的具体操作过程
具体序列化和反序列化是怎么执行的呢?
- 具体序列化执行流程如下(参数列表省略):
ObjectOutputStream->writeObject() -> writeObject0() -> writeOrdinaryObject() -> writeSerialData() -> defaultWriteFields()
其中writeObject0方法进行了判断
是否是String类型、枚举类型、序列化对象,如果都不是将抛出异常
NotSerializableException这个异常是不是很眼熟前面我们没有实现Serializable接口抛出的异常就是从这来的。
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
- 具体反序列化执行流程如下(参数列表省略):
ObjectInputStream -> readObject() -> readObject0() -> readOrdinaryObject() -> readSerialData()-> defaultReadFields()
3.transient和static修饰的属性为什么不能序列化
测试两个关键字:
- static 静态的
- transient 临时的
//新增加两个字段
public static String addRess="宁夏";
private transient String carName="捷豹";
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", addRess='" + addRess + '\'' +
", carName='" + carName + '\''+
'}';
}
//初始化
Student student = new Student();
student.setName("张三");
student.setAge(12);
System.out.println(student.toString());
//序列化
try {
//把对象写入文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
oos.writeObject(student);
System.out.println("序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
student.setAddRess("陕西");
//反序列化
try {
//从文件中读出对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
Student stu = (Student) ois.readObject();
System.out.println("反序列化成功!");
System.out.println(stu.toString());
} catch (Exception e) {
e.printStackTrace();
}
输出打印结果:
//序列化前的对象
Student{name='张三', age=12, addRess='宁夏', carName='捷豹'}
序列化成功!
反序列化成功!
//反序列化后的对象
Student{name='张三', age=12, addRess='陕西', carName='null'}
从结果中我们可以对比来看:
序列化前:addRess
字段的值为'宁夏'
,序列化后修改字段值为'陕西'
,反序列化后字段值是'陕西'
,而不是反序列化前的状态
为什么呢?原来static修饰的字段是属于类状态的,序列化是针对对象状态,所以static关键字修饰的关键字不保存序列化前的状态
序列化前carName
字段的值为 ''捷豹
,序列化后字段值为null,这又是为什么呢?
原来transient(临时的)关键字修饰的字段它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null。
如果要探究其实源码中也有体现
/**
* Returns array of ObjectStreamFields corresponding to all non-static
* non-transient fields declared by given class. Each ObjectStreamField
* contains a Field object for the field it represents. If no default
* serializable fields exist, NO_FIELDS is returned.
*/
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
注释中说明了:Modifier.STATIC | Modifier.TRANSIENT
静态的、临时的字段都不会序列化
4.序列化ID的作用是什么
不知道小伙伴们有没有发现,凡是实现了Serializable接口的类中都会有下面这行代码
private static final long serialVersionUID = 5791892247841471821L
serialVersionUID:是序列化ID的意思
那么这个序列化ID有什么作用?
其实他是为了确保反序列化能够成功的因子
如果在实现了序列化接口后不设置序列化ID java会自作主张的给你生成一个序列化ID (根据字段属性类型数目生成)
但是如果你在序列化后 增加某一个字段就会报错
下面就来验证一下:
//准备两个字段
private String name;
private int age;
//提供setter、getter方法
测试:
序列化
//初始化
Student student = new Student();
student.setName("张三");
student.setAge(12);
System.out.println(student.toString());
//序列化
try {
//把对象写入文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
oos.writeObject(student);
System.out.println("序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
//反序列化
try {
//从文件中读出对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
Student stu = (Student) ois.readObject();
System.out.println("反序列化成功!");
System.out.println(stu.toString());
} catch (Exception e) {
e.printStackTrace();
}
打印结果:
Student{name=‘张三’, age=12}
序列化成功!
反序列化成功!
Student{name=‘张三’, age=12}
修改字段age
的数据类型为String类型
这次测试我们先序列化
//初始化
Student student = new Student();
student.setName("张三");
student.setAge(12);
System.out.println(student.toString());
//序列化
try {
//把对象写入文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
oos.writeObject(student);
System.out.println("序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
然后将实体类的字段修改为String类型 ,注释序列化代码,执行反序列化
结果报错:
java.io.InvalidClassException: com.li.pojo.Student; local class incompatible: stream classdesc serialVersionUID = -5791892247841471821, local class serialVersionUID = -3882745368779827342
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
这个错误就是和我一开始说的错误是一样的序列化ID和反序列化ID不一致导致的错误
所以一般我们只要实现了序列化接口就采用默认的序列化 ID(1L)就可以
private static final long serialVersionUID = 1L