文章目录
- 什么是序列化和反序列化?
- 什么时候需要用到序列化和反序列化呢?
- 现序列化和反序列化为什么要实现Serializable接口?
- 为什么还要显示指定serialVersionUID的值?
- Java序列化的其他特性
- 使用transient关键字修饰隔离不序列化的字段
- 自定义序列化流程
- static属性不会被序列化
- IDEA如何提示产生serialVersionUID?
- 参考
什么是序列化和反序列化?
序列化:把对象转换为字节序列的过程称为对象的序列化,可以简单理解为:Object->byte[]
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,可以简单理解为:byte[]->Object
字节序列:也被称为是二进制序列。
序列化文件格式:
AC ED被称为魔幻数字,是序列化文件都会有的开头数字
(可以通过Notepad++安装Hex View插件来看)
什么时候需要用到序列化和反序列化呢?
当我们只在本地JVM里运行下Java实例, 这个时候是不需要什么序列化和反序列化的, 但是对于以下场景:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象,或者说跟浏览器交互
- 实现RPC
- …
这个时候就需要序列化和反序列化了,序列化机制使得对象可以脱离程序的运行而独立存在
前两个需要用到序列化和反序列化的场景, 是不是让我们有一个很大的疑问? 我们在与浏览器交互时, 还有将内存中的对象持久化到数据库中时, 好像都没有去进行序列化和反序列化, 因为我们都没有实现Serializable接口, 但一直正常运行.
下面先给出结论:
只要我们对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化.
理由:
服务器与浏览器交互时真的没有用到Serializable接口吗? JSON格式实际上就是将一个对象转化为字符串, 所以服务器与浏览器交互时的数据格式其实是字符串, 我们来看来String类型的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
......
}
实际上我们平时开发的接口,通过@ResponseBody返回给前端时会先把对象进行JSON化,得到一个字符串,字符串本身已经实现了序列化接口,对于我们来说就不需要主动实现序列化接口了。
而@RequestBody接收请求也是先是对JSON字符串操作,再转化为Java中的对象,几乎对序列化无感知。
然后我们再来看对象持久化到数据库中时的情况, Mybatis数据库映射文件里的insert代码:
<insert id="insertUser" parameterType="org.tyshawn.bean.User">
INSERT INTO t_user(name, age) VALUES (#{name}, #{age})
</insert>
实际上我们并不是将整个对象持久化到数据库中, 而是将对象中的属性持久化到数据库中, 而这些属性都是实现了Serializable接口的基本属性.
现序列化和反序列化为什么要实现Serializable接口?
在Java中实现了Serializable接口后, JVM才会在底层帮我们实现序列化和反序列化, 如果我们不实现Serializable接口, 那自己去写一套序列化和反序列化代码也行, 至于具体怎么写, Google一下你就知道了.
PS:稍微Google了一下,序列化的流程真的很长,不建议自己写一套
如果不实现Serializable接口,是没有办法使用Java提供的序列化程序的,会报错:java.io.NotSerializableException
为什么还要显示指定serialVersionUID的值?
如果不显示指定serialVersionUID, JVM在序列化时会根据属性自动生成一个serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.
如果显示指定了serialVersionUID, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.
在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改或者仅修改transient关键字修饰的属性(下面transient例子证明), 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行.
简单来说,这个serialVersionUID就是证明序列化前后的类是同一个版本的类。
这里我演示一下没有指定serialVersionUID的值 的后果:
测试实体类:
@Data
public class TestSerialize implements Serializable {
/**
* 书本id
*/
private Integer id;
/**
* 书本名字
*/
private String name;
/**
* 书本权重
*/
private Integer weight;
/**
* 书本分类
*/
private String classifyName;
private LocalDateTime ctime;
private LocalDateTime mtime;
private static Integer year = 2021;
public static Integer getYear() {
return year;
}
public static void setYear(Integer year) {
TestSerialize.year = year;
}
}
序列化方法:
public void testSerialize() {
TestSerialize testSerialize = new TestSerialize();
testSerialize.setClassifyName("AA");
testSerialize.setName("京东派");
testSerialize.setWeight(3);
testSerialize.setId(1);
testSerialize.setCtime(LocalDateTime.now());
testSerialize.setMtime(LocalDateTime.now());
//这里我设置了静态成员year=2022,如果序列化是包含静态成员,那么反序列化时就应该也是2022
TestSerialize.setYear(2022);
//try...resource语法可以在try块结束后自动调用close方法
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
log.info("序列化对象:{}", testSerialize.toString());
log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
oos.writeObject(testSerialize);
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("序列化错误:", e);
}
}
反序列化方法:
注:这里为了完美测试,我是重启了应用后然后再执行反序列化方法
public void testDeserialize() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
TestSerialize testSerialize = (TestSerialize) ois.readObject();
log.info("反序列化对象:{}", testSerialize.toString());
log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
}
}
OK,现在准备好了,然后先进行序列化过程:
序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:22:24.649, mtime=2022-10-08T14:22:24.649)
序列化对象静态成员year:2022
序列化正常,然后在实体类新增一个age字段:
@Data
public class TestSerialize implements Serializable {
......
private Integer age;
......
}
重启后进行反序列化:
出现报错:java.io.InvalidClassException: com.zby.model.dto.TestSerialize; local class incompatible: stream classdesc serialVersionUID = 1784859443472419136, local class serialVersionUID = 1986709366121845600
Java序列化的其他特性
使用transient关键字修饰隔离不序列化的字段
先说结论, 被transient关键字修饰的属性不会被序列化, static属性也不会被序列化(static没法序列化的demo在下面有)
测试程序:
实体类:增加一个transient关键字修饰的字段age
@Data
public class TestSerialize implements Serializable {
/**
* 书本id
*/
private Integer id;
/**
* 书本名字
*/
private String name;
/**
* 书本权重
*/
private Integer weight;
/**
* 书本分类
*/
private String classifyName;
private LocalDateTime ctime;
private LocalDateTime mtime;
private transient Integer age;
private static Integer year = 2021;
public static Integer getYear() {
return year;
}
public static void setYear(Integer year) {
TestSerialize.year = year;
}
}
测试程序:
//序列化方法
public void testSerialize() {
TestSerialize testSerialize = new TestSerialize();
testSerialize.setClassifyName("AA");
testSerialize.setName("京东派");
testSerialize.setWeight(3);
testSerialize.setId(1);
testSerialize.setCtime(LocalDateTime.now());
testSerialize.setMtime(LocalDateTime.now());
testSerialize.setAge(18);
TestSerialize.setYear(2022);
//try...resource语法可以在try块结束后自动调用close方法
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
log.info("序列化对象:{}", testSerialize.toString());
log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
oos.writeObject(testSerialize);
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("序列化错误:", e);
}
}
//反序列化方法
public void testDeserialize() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
TestSerialize testSerialize = (TestSerialize) ois.readObject();
log.info("反序列化对象:{}", testSerialize.toString());
log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
}
}
}
结果:
序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:39:18.440, mtime=2022-10-08T14:39:18.440, age=18)
序列化对象静态成员year:2022
反序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:39:18.440, mtime=2022-10-08T14:39:18.440, age=null)
反序列化对象静态成员year:2022
最终age的反序列化结果是null,由此证明序列化文件并不包含age属性,因为age属性并没有赋值为18。
反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
而且,这个例子也让我发现了有关序列化号的一个特性,改动transient关键字修饰的属性也无需提供serialVersionUID,反序列化时不报错。
自定义序列化流程
static属性不会被序列化
因为序列化是针对对象而言的, 而static属性优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID.(这里我只是了解到serialVersionUID不会写入,感兴趣的可以深入)
测试程序:
实体类:
@Data
public class TestSerialize implements Serializable {
/**
* 书本id
*/
private Integer id;
/**
* 书本名字
*/
private String name;
/**
* 书本权重
*/
private Integer weight;
/**
* 书本分类
*/
private String classifyName;
private LocalDateTime ctime;
private LocalDateTime mtime;
private static Integer year = 2021;
public static Integer getYear() {
return year;
}
public static void setYear(Integer year) {
TestSerialize.year = year;
}
}
序列化方法:
public void testSerialize() {
TestSerialize testSerialize = new TestSerialize();
testSerialize.setClassifyName("AA");
testSerialize.setName("京东派");
testSerialize.setWeight(3);
testSerialize.setId(1);
testSerialize.setCtime(LocalDateTime.now());
testSerialize.setMtime(LocalDateTime.now());
//这里我设置了静态成员year=2022,如果序列化是包含静态成员,那么反序列化时就应该也是2022
TestSerialize.setYear(2022);
//try...resource语法可以在try块结束后自动调用close方法
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
log.info("序列化对象:{}", testSerialize.toString());
log.info("序列化对象静态成员year:{}", TestSerialize.getYear());
oos.writeObject(testSerialize);
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("序列化错误:", e);
}
}
反序列化方法:
注:这里为了完美测试,我是重启了应用后然后再执行反序列化方法
public void testDeserialize() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.pathSeparator + "111.txt")))) {
TestSerialize testSerialize = (TestSerialize) ois.readObject();
log.info("反序列化对象:{}", testSerialize.toString());
log.info("反序列化对象静态成员year:{}", TestSerialize.getYear());
} catch (FileNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (IOException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
log.error("反序列化错误:", e);
}
}
结果输出:
序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T14:02:42.889, mtime=2022-10-08T14:02:42.889)
序列化对象静态成员year:2022
反序列化对象:TestSerialize(id=1, name=京东派, weight=3, classifyName=AA, ctime=2022-10-08T13:59:03.428, mtime=2022-10-08T13:59:03.428)
反序列化对象静态成员year:2021
由此可以证明序列化并不包含static成员
IDEA如何提示产生serialVersionUID?
Settings->搜索Inspections,勾选下面的选项。
然后移到类名处就会弹出选项为你自动生成serialVersionUID
参考
https://mp.weixin.qq.com/s/YKKwpQaqVsvK4xXow5kbvA