深入理解Java原生的序列化机制
概念
一个对象如果想在硬盘上存储,一定就需要借助于一定的数据格式。这种把对象转换为硬盘存储的格式的过程就叫做对象的序列化,同样地,将这些文件再反向转换为程序中对象的操作就叫做反序列化
一些复杂的解决方案可能是将对象转换为json字符串的方式,这种方式的优点是易读,但是效率还是太低,所以java的序列化的解决方案是将对象转换为一个二进制流的形式,来实现数据的持久化,本篇文章将会来详细讲解序列化的实现和原理
实现
准备
我们这里有一个普通的对象,要注意的是这个类和其中用到的所有对象都需要实现序列化接口serializable:
class demo implements serializable { int val = 10; string time = new simpledateformat("hh:mm:ss").format(new date()); a a = new a(20); @override public string tostring() { return "[hashcode=" + hashcode() + " val=" + val + ", time=" + time + ", a.val=" + a.val +"]"; } }
这个a是一个普通的对象,如下:
class a implements serializable { int val = 20; public a(int val) { this.val = val; } }
现在我们有一个demo对象,来输出一下这个对象的标志字符串:
demo demo = new demo(); system.out.println(demo.tostring());
输出结果:
[hashcode=1625635731 val=10, time=20:28:56, a.val=20]
序列化
现在,我们需要将这个对象序列化为二进制流,则需要以下的操作:
fileoutputstream fileoutputstream = new fileoutputstream("target"); objectoutputstream objectoutputstream = new objectoutputstream(fileoutputstream); objectoutputstream.writeobject(demo); objectoutputstream.flush(); objectoutputstream.close();
这样,demo对象就被我们持久化到硬盘的target文件中了
反序列化
反之,如果我们想将这个对象从target文件中取出,就需要如下的操作:
fileinputstream fileinputstream = new fileinputstream("target"); objectinputstream objectinputstream = new objectinputstream(fileinputstream); demo newdemo = (demo)objectinputstream.readobject();
检验
现在,我们用以下的语句来检验这两个对象是否是一个对象:
system.out.println(newdemo.tostring()); system.out.println("demo == newdemo : " + (demo == newdemo));
输出
[hashcode=885284298 val=10, time=20:28:56, a.val=20] demo == newdemo : false
我们会发现,反序列化得到的对象虽然值和原有对象一致,但是其不是同一个对象,这一点很重要
原理
我们打开序列化生成的target文件,这里需要用二进制流的方式打开:
这里可以将文件分为5个部分:
- 文件头:声明文件是一个对象序列化文件,同时声明了序列化版本
- 类描述:声明类信息,包括类名、序列化id,以及域的个数等属性
- 属性描述
- 父类信息描述
- 对象属性的实际值
也就是说,在这个二进制文件中,通过这几部分就能表明一个类的全部信息,在反序列化的过程中,java将会按照指定的文件格式来从文件中恢复数据
注意事项
序列化的类一定要实现serializable接口
序列化类中包含的自定义对象都需要实现serializable接口
这两点是为什么呢,我们来看objectoutputstream中的writeobject0方法,这里截取了一小段:
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()); } }
这段代码中的obj不仅仅是被序列化的对象,还会是这个对象中的所有字段,也就是说其中的域对象,必须是字符串、数组、枚举和序列化接口中的一种,否则就会抛出异常
序列化id
其实,还有一点注意事项,我留在了这里来讲:
在序列化和反序列化之间,对象的字段名称、类型和数量均不能改变
这是为什么呢,我们来看反序列化中的一块代码:
if (model.serializable == osc.serializable && !cl.isarray() && suid != osc.getserialversionuid()) { throw new invalidclassexception(osc.name, "local class incompatible: " + "stream classdesc serialversionuid = " + suid + ", local class serialversionuid = " + osc.getserialversionuid()); }
这是objectstreamclass中的initnonproxy方法中的一段,这个方法也就是读取我们序列化文件的核心方法,用于初始化类描述符
不过我们重点不在这里,重点是一个suid和osc.getserialversionuid()的比较,这时候就要涉及到一个序列化id的概念了,序列化id的声明类似下面这种形式:
class demo implements serializable { // 这个序列化id一般的ide都会提供有自动生成的插件,感兴趣的可以自行下载 private static final long serialversionuid = -5809782578272943999l; // ... }
java的反序列化成功与否的关键,就是比较文件的序列化id和类的序列化id是否一致,如果一致,则认为文件中的对象和类对象是同一个对象,否则,就说明两个类压根就不是一个类,如果强行转换则很有可能发生异常
但是我们之前没有手动设置序列化id也一样能反序列化成功不是吗?其实,之前能反序列化成功仅仅是因为我们没有改动原来的类,如果我们没有设置序列化id,则以下任何的操作,均会导致反序列化失败:
- 修改了字段/方法的名称/类型
- 添加或删除字段/方法
看到了吗,即使我们仅仅修改了字段的名称,也会导致反序列化的失败,如果不注意这一点,将会导致所有反序列化操作的崩溃,但是只要我们设置一个序列化id,即使我们把类中元素删的一干二净,也一样会反序列化成功,只不过是丢失属性而已
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 微信小程序 在线支付功能的实现