Java序列化与反序列化的实例分析讲解
作者:灰灰是菇凉呀 发布时间:2022-09-16 05:58:39
序列化与反序列化
Java对象是有生命周期的,当生命周期结束它就会被回收,但是可以通过将其转换为字节序列永久保存下来或者通过网络传输给另一方。
把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。
Serializable接口
一个类实现java.io.Serializable接口就可以被序列化或者反序列化。实际上,Serializable接口中没有任何变量和方法,它只是一个标识。如果没有实现这个接口,在序列化或者反序列化时会抛出NotSerializableException异常。
下面是一个实现了Serializable接口的类以及它的序列化与反序列化过程。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("test");
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test1 test1 = (Test1) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
运行结果:
Test{name='test'}
serialVersionUID
private static final long serialVersionUID = -3297006450515623366L;
serialVersionUID是一个序列化版本号,实现Serializable接口的类都会有一个版本号。如果没有自己定义,那么程序会默认生成一个版本号,这个版本号是Java运行时环境根据类的内部细节自动生成的。最好我们自己定义该版本号,否则当类发生改变时,程序为我们自动生成的序列化版本号也会发生改变,那么再将原来的字节序列反序列化时就会发生错误。
下面是将Test1类加入一个变量age,此时再进行反序列化的结果。可以看出,序列化版本号已发生改变,程序认为不是同一个类,不能进行反序列化。
java.io.InvalidClassException: test.Test1; local class incompatible: stream classdesc serialVersionUID = 9097989105451761251, local class serialVersionUID = -7756223913249050270
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at test.SerialTest.main(SerialTest.java:11)
为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示地定义serialVersionUID,为他赋予明确的值。
那么在IDEA中,怎么手动生成呢?
在settings->Editor->Inspections下,搜索serial,开启Serializable class without 'serialVersionUID'的拼写检查,然后将光标放在实现Serializable的接口上,按住ALt+Enter键,选择添加serialVersionUID即可。
Transient关键字
transient修饰类的变量,可以使变量不被序列化。反序列化时,被transient修饰的变量的值被设为初始值,如int类型被设为0,对象型被设为null。
ObjectOutputStream类和ObjectInputStream类
ObjectOutputStream的writeObject方法可以序列化对象,ObjectInputStream的readObject可以反序列化对象。ObjectOutputStream实现了接口ObjectOutput,所以可以进行对象写操作。ObjectInputStream实现了接口ObjectInput,所以可以对对象进行读操作。
静态变量序列化
给Test类中增加一个静态变量,赋值为12,然后在序列化之后修改其值为10,反序列化之后打印它的值。发现打印的值为10,之前的12并没有被保存。
静态变量是不参与序列化的,序列化只是用来保存对象的状态,而静态变量属于类的状态。
父类序列化
让Test继承一个没有实现Serializable接口的类,设置父类中变量的值,对Test类的实例进行序列化与反序列化操作。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
test.setSex(12);
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test test1 = (Test) ois.readObject();
System.out.println(test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test extends TestFather implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
"sex='" + sex + '\'' +
'}';
}
}
class TestFather {
protected Integer sex;
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "TestFather{" +
"sex='" + sex + '\'' +
'}';
}
}
运行结果:
Test{name='huihui'sex='null'}
发现虽然对sex进行了复制,但是反序列化结果仍然为null。
现在让TestFather类实现Serializable接口,运行结果如下。所以当我们想要序列化父类的变量时,也需要让父类实现Serializable接口。
Test{name='huihui'sex='12'}
同理,如果Test类中有任何变量是对象,那么该对象的类也需要实现Serializable接口。查看String源代码,确实实现了Serializable接口。大家可以测试一下字段的类不实现Serializable接口的情况,运行会直接报java.io.NotSerializableException异常。
敏感字段加密
如果对于某些字段我们并不想直接暴露出去,需要对其进行加密处理,那么就需要我们自定义序列化和反序列化方法。使用Serializable接口进行序列化时,如果不自定义方法,则默认调用ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。下面我们来尝试一下自己实现序列化与反序列化过程。
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putField = out.putFields();
System.out.println("原name:" + name);
// 模拟加密
name = "change";
putField.put("name", name);
System.out.println("加密后的name:" + name);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField getField = in.readFields();
Object object = getField.get("name", "");
System.out.println("要解密的name:" + object.toString());
name = "huihui";
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
原name:huihui
加密后的name:change
要解密的name:change
解密后的name:huihui
这种写法重写了writeObject方法和readObject方法,下面一种接口也可以实现相同的功能。
Externalizable接口
除了Serializable接口,Java还提供了一个Externalizable接口,它继承了Serializable接口,提供了writeExternal和readExternal两个方法,实现该接口的类必须重写这两个方法。同时还发现,类还必须提供一个无参构造方法,否则会报java.io.InvalidClassException异常。
先不深究为什么要加一个无参构造方法,我们先试一下这个接口的序列化效果。将类Test改为如下所示:
class Test implements Externalizable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public Test() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
再次运行测试方法,发现输出的name是null。在readObject处打断点,发现会调用无参构造方法。
name其实并没有被序列化与反序列化,writeExternal方法和readExternal方法中是需要我们自己来实现序列化与反序列化的细节的。在反序列化时,会首先调用类的无参考构造方法创建一个新对象,然后再填充每个字段。
我们对writeExternal方法和readExternal方法进行重写:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
}
此时运行测试方法,发现Test类被正常序列化与反序列化。
序列化存储规则
当多次序列化一个对象时,是会序列化多次还是会序列化一次呢?
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
// 两次写入文件
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
// 读取两个对象
Test test1 = (Test) ois.readObject();
Test test2 = (Test) ois.readObject();
System.out.println(test1 == test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
运行结果:
73
78
true
可以发现,当第二次写入对象时,文件的长度仅仅增加了5个字节,并且在判等时,两个引用指向同一地址。
Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。
来源:https://blog.csdn.net/sinat_28394909/article/details/84956158


猜你喜欢
- java ,javaw 和 javaws 的区别:首先,所有的这些都是java的启动装置,java.e
- 这篇文章主要介绍了Java实现inputstream流的复制代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习
- 最近在开发项目的时候遇到一个问题,在myecilpes上使用googlede gson读取项目中的json文件成功,然后把项目发布到tomc
- 一、概述项目中经常用到倒计时的功能,比如说限时抢购,手机获取验证码等等。而google官方也帮我们封装好了一个类:CountDownTime
- 问题(1)重入锁是什么?(2)ReentrantLock如何实现重入锁?(3)ReentrantLock为什么默认是非公平模式?(4)Ree
- 效果:一个手指实现(所有手势事件)和(部分事件的);A. 所有手势activity_main.xml<TextView android
- Java获取控制台输入的方法在学习网络编程中,有需要从控制台输入数据,进行两个线程之间的通信,其中,涉及到了读取控制台输入的两种不同的操作,
- 概述异步这个概念在不同语境下有不同的解释,比如在一个单核CPU里开启两个线程执行两个函数,通常认为这种调用是异步的,但对于CPU来说它是单核
- C#延时函数使用在线程中如果需要延时,尽量不要使用Sleep()函数,这样会导致时间片切到别的线程中。使用如下函数:
- 自从使用 HttpClient 和 Jsoup 配合编写了几个简单的入门爬虫之后,发现对于绝对路径的需求是很频繁的,因为大部分的网页都写相对
- Android标题栏最右边添加按钮的实例step1:重写activity的onCreateOptionsMenu方法@Override pu
- 目前为止,许多编程语言和工具都包含对正则表达式的支持,C#也不例外,C#基础类库中包含有一个命名空间(System.Text.Regular
- 本示例演示如何通过设置Intent对象的标记,来改变当前任务堆栈中既存的Activity的顺序。1. Intent对象的Activity启动
- Android应用经常会和服务器端交互,这就需要手机客户端发送网络请求,下面介绍四种常用网络请求方式,我这边是通过Android单元测试来完
- 前言Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化,只是这是默认的行为,无法进一步自定义。圆角按钮实现【重写
- 由于GitHub上面的zxing功能太多,有的用不到,我就抽取了重要的出来使用,这个可以生成二维码,扫描二维码和相册中的二维码Demo效果:
- /// <summary>/// 获取数据缓存/// </summary>/// <param name=&q
- 1.导入相关jar包,具体哪些包我记不太清了2.在applicationContext中加入相关配置信息,如下所示:<beans xm
- 本文介绍了Android定时器Timer的停止和重启实现代码,分享给大家,具体如下:7月份做了一个项目,利用自定义控件呈现一幅动画,当时使用
- Java注解介绍基于注解(Annotation-based)的Java开发无疑是最新的开发趋势.[译者注: 这是05年的文章,在2014年,