Java基础入门总结之序列化和反序列化
作者:攻城狮Chova 发布时间:2023-02-19 03:29:10
基本概念
Java中创建对象时,一旦程序终止,创建的对象可能就不存在.要想使得对象能够在程序不运行的状态下依然能够保存对象的信息,这时就需要用到序列化机制
序列化机制:
对象的数据
对象的类型信息
存储在对象中的数据类型
一个对象可以被表示为一个字节序列,包括:
将可序列化对象写入文件后,可以从文件中读取出来,根据对象的各种信息在内存中创建该对象. 这里的读取并创建对象的过程就是反序列化
序列化和反序列化的整个过程都是JVM独立的.也就是说,在一个JVM中的序列化对象可以在另一个完全不同的JVM中反序列化对象
一般情况下,序列化需要实现java.io.Serializable接口,使用ObjectInputStream和ObjectOutputStream进行对象的读写操作
还可以实现java.io.Externalizable接口,进行标准的序列化或者自定义的二进制格式.用来满足不同场景下的需求
Java序列化场景:
将Java对象的字节序列持久化到硬盘中
在网络上传输对象的字节序列
进行远程方法调用RMI(Remote Method Invocation)
JVM运行结束时,还需要使用创建的对象
需要将创建的对象保存下来以便后续的传输
使得旧JVM创建的对象能够在一个新的JVM中运行
Java序列化注意点:
对象的序列化保存的是对象成员变量对象,对象的序列化不会关注类中的静态变量
类的序列化要保证类的所有属性都是可以序列化的,如果想要某个属性不被序列化.可以声明为瞬时态transient
序列化
Java对象序列化:
使得可序列化的对象实现Serializable接口
创建一个ObjectOutputStream输出流
调用ObjectOutputStream对象的writeObject() 方法进行输出可序列化对象即可
序列化示例:
public class Person implements Serializable {
private String name;
private int age;
public Person() {
System.out.println("无参构造...");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造...");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + "\'" +
", age='" + age + "\'"
"}";
}
}
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("Lily", 20);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.txt"));
oos.writeObject(person);
oos.close();
}
}
反序列化
Java对象反序列化:
创建一个ObjectInputStream输入流
调用ObjectInputStream对象的readObject() 方法得到序列化对象
public class DeserializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.txt"));
Pesron person = (Person)ois.readObject(ois);
System.out.println(person);
}
}
反序列化的对象是由JVM生成的对象,而不是通过类的构造函数生成的:
反序列化对象时 ,JVM中要存在对象对应的类,否则会抛出ClassNotFoundException异常
如果一个可序列化的类的成员不是基本类型,而是一个引用类型时,那么这个引用类型必须实现Serializable接口,否则会抛出NotSerializableException异常
序列化和反序列化总结
实现Serializable接口就可以进行序列化的原因:
保存到磁盘的对象都有一个序列化编号,当程序试图进行序列化时,会检查该对象是否已经序列化
只有对象从未被序列化过时,才会将此对象序列化为字节序列,如果对象已经序列化过,那么直接输出序列化编号
Java序列化机制不会重复序列化同一个对象,会记录已经序列化对象的编号,此时如果序列化了一个可变对象后,如果更改了对象的内容会再次进行序列化.如果没有更改内容,则不会将此对象转换为字节序列,只会保存序列化编号
首先会处理之前被编写的以及不可替换的对象
如果有的对象被替换了,则检查被替换的对象
最后如果对象都被替换了,则进行原始的检查
原始的检查即检查被替换的对象类型是否为String类型,数组类型 ,Enum类型或者实现了Serializable接口,符合条件就可以对检查的对象执行相应的序列化操作,否则将会抛出NotSerializableException异常
writeObject():
对于序列化的机制来说,如果对同一个对象执行多次序列化操作时,不会得到多个对象
实现Serializable接口时可以重写writeObject() 方法和readObject() 方法:
重写writeObject() 方法和readObject() 方法后,对象进行序列化和反序列化时,就会自动调用重写的writeObject() 方法和readObject() 方法
实现Externalizable接口时可以重写writeExternal() 方法和readExternal() 方法:
重写writeExternal() 方法和readExternal() 方法后,对象进行序列化和反序列化时,就会自动调用重写的writeExternal() 方法和readExternal() 方法
自定义序列化策略
Externalizable
如果需要使得对象的一部分可以被序列化,另一部分数据不被序列化,此时可以自定义实现Externalizable接口,并且实现writeExternal() 和readExternal() 方法,可以在序列化和反序列化过程中自动调用来执行一些特殊的操作
注意点:
Serializable接口实现的对象是与二进制的构建有关的,不会调用构造器
Externalizable接口实现的对象的所有构造函数都会被调用,所以要编写出类的无参和有参构造函数
使用Externalizable自定义序列化示例:
public class CustomExternal implements Externalizable {
private String name;
private int code;
public CustomExternal() {
System.out.println("无参构造...");
}
public CustomExternal(String name, int code) {
this.name = name;
this.code = code;
System.out.println("有参构造...");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("执行writeExternal()方法...");
out.writeObject(name);
out.writeInt(code);
}
@Override
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
System.out.println("执行readExternal()方法...");
name = (String) in.readObject();
code = in.readInt();
}
@Override
public String toString() {
return "类:" + name + code;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
CustomExternal custom = new CustomeExternal("oxford", 666);
System.out.println(custom);
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileInputStream("oxford.txt"));
System.out.println("序列化对象...");
out.writeObject(custom);
out.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("oxford.txt"));
System.out.println("反序列化对象...");
custom = (CustomExternal) in.readObject();
System.out.println(custom);
in.close();
}
}
有参构造...
类:oxford666
序列化对象...
执行writeExternal()方法...
反序列化对象...
无参构造...
执行readExternal()方法...
类:oxford666
使用Externalizable自定义序列化时,为了保证序列化和反序列化的正确性,需要在writeExternal() 方法中将信息写入,并且在readExternal() 方法中恢复数据
transient
可以使用transient关键字配置一些重要的信息比如密码等不进行序列化
transient关键字修饰的属性不会参与到序列化过程中
transient关键字修饰的属性在反序列化过程中,如果是引用数据类型,则返回null. 如果是基本数据类型,则返回默认值.不一定是基本数据类型序列化之前的值
因为实现Externalizable接口的对象默认情况下不会保存任何字段,所以transient关键字只能和Serializable对象一起使用
transient关键字的使用场景:
服务器端给客户端发送序列化对象数据时,对象中存在敏感数据
比如密码字符串,在序列化时进行加密,客户端拥有解密的密钥,只有在客户端进行反序列化时,才会对密码进行读取
这时就可以对密码字符串对象使用transient修饰,这样可以一定程度上保证序列化对象的数据安全
静态变量
序列化时不会序列化静态变量
静态变量属于类的状态,序列化中保存的是对象,也就是类的实例的状态
序列化操作的是序列化中对象,也就是类的实例的状态,静态变量属于类的状态.所以序列化时不会对静态变量进行序列化
序列化ID
Java虚拟机进行反序列化:
两个类的类路径和功能代码一致
两个类的序列化ID,也就是serialVersionUID一致
功能代码一致:
客户端A将类对象序列化客户端B, 客户端B进行反序列化
这时要求A和B都有这样的一个类文件,功能代码一致,并且都实现了Serializable接口
序列化的类和反序列化的类所实现的功能和功能相关的代码是一样的
示例:
serialVersionUID的两种生成方式:
默认值 1L
通过类名,接口名和方法名以及属性随机生成的一个不重复的long类型的值
在序列化ID, 即serialVersionUID相同的情况下,即使序列化对象的序列化属性修改,序列化对象也可以进行反序列化.因此如果只是修改了方法或者修改了静态变量或transient变量,只要不修改序列化ID, 那么反序列化就不会受到影响
显式声明序列化ID, 即serialVersionUID的场景:
如果需要类的不同版本对序列化兼容,要确保类的不同版本具有相同的serialVersionUID
如果不需要类的不同版本对序列化兼容,要确保类的不同版本具有不同的serialVersionUID
序列化一个类的实例后,如果修改一个字段或者增加一个字段,如果没有设置类的serialVersionUID, 就会导致无法反序列化旧的实例,会在反序列化时抛出异常
序列化类添加SerialVersionUID后,如果修改一个字段或者增加一个字段,反序列化旧的实例时,修改的或者增加的字段的值会设置为初始化的值
破坏单例
除了反射可以破坏单例模式外,序列化和反序列化后会得到一个新的对象,也可以破坏单例模式
序列化和反序列化破坏单例模式:
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek();
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}如果类实现的是Serializable接口,就调用第一个不可进行序列化超类的无参构造方法数创建新的实例
如果类实现的Externalizable接口,就调用公共的无参构造方法创建新的实例
isInstantiable() 方法表示如果一个实现了Serializable接口或者Externalizable接口的类可以在运行时实例化,那么该方法就返回true
如果可以在运行时序列化,就会调用desc.newInstance() 方法使用反射的方式调用无参构造方法新建一个对象,创建一个类的新的实例
readOrdinaryObject() 方法用于读取并返回普通对象. 这里的普通对象不包括String, Class, ObjectStreamClass, 数组或者枚举常量这些对象
readObject0() 方法中会返回一个checkResolve(readOrdinaryObject(unshared))
反序列化时,使用ObjectInputStream对象中的readObject() 方法
readObject() 的方法中调用readObject0() 方法
因为序列化过程中会通过反射调用无参构造函数创建一个新的对象,所以序列化会破坏单例模式
为了防止序列化破坏单例模式,可以在Singleton.java中添加readResolve() 方法并且指定要返回的对象的生成策略
因为readOrdinaryObject() 方法源码中的hasResolveMethod() 表示如果实现了Serializable或者Externalizable接口的类中包含readResolve() 方法就返回true
invokeReadResolve() 方法会通过反射的方式调用要被反序列化的类的readResolve() 方法
来源:https://juejin.cn/post/7068277615689367589


猜你喜欢
- 今天给大家介绍下用Java swing开发一款音乐播放器, * 酷狗音乐播放器,完整源码地址在最下方,本文只列出部分源码,因为源码很多,全部贴
- 一、注解的概念1、注解官方解释注解叫元数据,一种代码级别的说明,它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举在同一个层次,它可
- 业务场景近年来B2C、O2O等商业概念的提出和移动端的发展,使得分布式系统流行了起来。分布式系统相对于单系统,解决了流量大、系统高可用和高容
- 前言不知道你是否参加过拼多多上邀请微信好友砍价功能,这个功能实现首先需要考虑的就是获取微信用户的信息。获取用户信息就是获取公众号下微信用户的
- 本文实例讲述了Android中ProgressBar用法。分享给大家供大家参考,具体如下:在android中会经常用到ProgressBar
- 前言:在纯 Java 代码里 我们一般都会用class.getResource(String name) 或者 class.getClass
- 本文实例为大家分享了Android仿qq分组管理的第三方库,供大家参考,具体内容如下下面先看效果 我们点击展开与折叠分组的功能在库
- 如下所示:if(str.indexOf(",") >= 0) System.out.println(&
- 事件的声明和使用与代理有很密切的关系,事件其实是一个或多个方法的代理,当对象的某个状态发生了变化,代理会被自动调用,从而代理的方法就被自动执
- 题目描述BM99 顺时针旋转矩阵描述 有一个NxN整数矩阵,请编写一个算法,将矩阵顺时针旋转90度。 给定一个NxN的矩阵,和矩阵的阶数N,
- 为什么使用JUnit5JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余
- 最近项目中新增的功能,需要对手机号、姓名、身份证号等一些信息进行验证,最好的方法是通过正则表达式来验证,网上查了一些资料,写了这几个工具方法
- 一、只读自动属性(Read-only auto-properties) C# 6之前我们构建只读自动属性: public stri
- 0.介绍预览针对需要在IOS手机上接入原生微信支付场景,调用微信进行支付。如图:1.资料准备1.1 账号注册打开https://open.w
- 1 Struts2框架内部执行过程Structs请求过程源码分析参考链接https://www.jb51.net/article/22058
- 在开发中,我们通常需要将从数据库中查询的集合数据转换成类似文件系统一样的树形集合,比如:省市单位,部门机构,书籍分类等TreeNode对象@
- summarydetail传统的Spring项目会有很多的配置文件,比如我们要使用Redis,一般除了对应的依赖的jar包我们还需要在app
- 读XMLXmlDocument xd = new XmlDocument(); string fi
- SpringBoot starter用了springboot 那么久了居然都还没自定义过starter,想想都觉得羞愧,所以今天来玩一下。S
- 十六进制字符串与数值类型之间转换(C# 编程指南) 以下示例演示如何执行下列任务: 获取字符串中每个字符的十六进制值。 获取与十六进制字符串