Java的序列化反序列化
一、介绍
序列化和反序列化 几乎是工程师们每天都需要面对的事情,尤其是当前流行的微服务开发。
光看定义上,对于初学者来说,可能很难一下子理解序列化的意义,尤其是面对这种特别学术词语的时候,内心会不由自主的发问: 它到底是啥,用来干嘛的 ?
如果用通俗的方式来理解,你可以用变魔术的方式来理解它,就好比你想把一件铁器从一个地方运往到另一个地方,在出发的时候,通过魔术方式将这个东西融化成一桶铁水,当到达目的地之后,又通过变魔术的方式,将这桶铁水还原成一件铁器。当铁器变成铁水的过程,可以理解为 序列化 ;从铁水变成铁器,可以理解为 反序列化 。
站在程序世界的角度看,我们都知道计算机之间传递信息的最小单元是字节流,序列化其实就是将一个对象变成所有的计算机都能识别的字节流;反序列化就是将接受到的字节流还原成一个程序能识别的对象。
简单的说,序列化最终的目的是为了对象可以更方面的进行跨平台存储和进行网络传输。
基本上只要是涉及到 跨平台存储 或者进行 网络传输 的数据,都需要进行序列化。
互联网早期的序列化方式主要有COM和CORBA。
COM主要用于Windows平台,并没有真正实现跨平台,另外COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大(想一下这个场景, 工程师需要是简单的序列化协议,但却要先掌握语言编译器)。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。
CORBA是早期比较好的实现了跨平台,跨语言的序列化协议。COBRA的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩。这些政治经济,技术实现以及早期设计不成熟的问题,最终导致COBRA的渐渐消亡。J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。
随着软件技术的快速发展,之后逐渐出现了比较流行的序列化方式,例如:XML、JSON、Protobuf、Thrift 和 Avro等等。
这些序列化方式各有千秋,不能简单的说哪一种序列化方式是最好的,只能从你的当时环境下去选择最适合你的序列化方式,如果你要为你的公司项目进行序列化技术的选型,主要可以从以下几个方面进行考虑: 是否支持跨平台 :尤其是多种语言混合开发的项目,是否支持跨平台直接决定了系统开发难度 序列化的速度 :速度快的方式会为你的系统性能提升不少 序列化出来的大小 :数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能
BB了这么多,作为一名 java 程序员,我们应该如何使用序列化呢,以及序列化的过程中应该需要注意的问题。
下面,我们一起来了解一下! 二、代码实践2.1、序列化操作
java 实现序列化方式非常简单,只需要实现 Serializable 接口即可,例如下面这个类。 public class Student implements Serializable { /** * 用户名 */ private String name; /** * 年龄 */ private Integer age; public Student(String name, Integer age) { this.name = name; this.age = age; } @Override public String toString() { return "Student1{" + "name="" + name + """ + ", age=" + age + "}"; } }
我们来测试一下,将 Student 对象进行二进制的数据存储后,并从文件中读取数据出来转成Student 对象,这个过程其实就是一个序列化和反序列化的过程。 public class ObjectMainTest { public static void main(String[] args) throws Exception { //序列化 serializeAnimal(); //反序列化 deserializeAnimal(); } private static void serializeAnimal() throws Exception { Student black = new Student("张三", 20); System.out.println(black.toString()); System.out.println("=================开始序列化================"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log")); oos.writeObject(black); oos.flush(); oos.close(); } private static void deserializeAnimal() throws Exception { System.out.println("=================开始反序列化================"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log")); Student black = (Student) ois.readObject(); ois.close(); System.out.println(black.toString()); } }
输出结果: Student{name="张三", age=20} =================开始序列化================ =================开始反序列化================ Student{name="张三", age=20}
看起来是不是超级简单,但是 请你别大意,这里面的坑还真不少 ,请看下面的问题汇总! 三、序列化问题汇总3.1、static 属性不能被序列化
实际在序列化的时候,被 static 修饰的属性字段是不能被序列化进去的,因为静态变量属于类的状态,序列化并不保存静态变量! 2.3、Transient 属性不会被序列化
被 Transient 修饰的属性无法被序列化,眼见为实,我们给Student 类的name 字段加一个transient 修饰符。 public class Student implements Serializable { /** * 用户名 */ private transient String name; //...省略 }
运行测试方法,输出结果如下: Student{name="张三", age=20} =================开始序列化================ =================开始反序列化================ Student{name="null", age=20}
很明显,被 transient 修饰的name 属性,反序列化后的结果为null 。 2.4、序列化版本号 serialVersionUID 问题
只要是实现了 Serializable 接口的类都会有一个版本号,如果我们没有定义,JDK 工具会按照我们对象的属性生成一个对应的版本号,当然我们还可以自定义,例如给Student 类自定义一个序列化版本号,操作如下。 public class Student implements Serializable { //自定义序列化版本号 private static final long serialVersionUID = 1l; //...省略 }
如何验证这一点呢?
首先,我们先序列化一个 Student 对象,里面没有自定义版本号,然后在反序列化的时候,我们给这个对象自定义一个版本号,运行测试程序,看能不能反序列化成功? Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = 1 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
答案很明显,反序列化失败!
分析原因: Student 对象序列化时的版本号是821478144412499207 ,反序列化时的版本号是1 ,两者不一致,导致无法反序列化成功!
当我们没有显式的自定义序列化版本号时,JDK 会根据当前对象的属性自动生成一个对象的版本号,只要对象的属性不会发生变化,这个版本号也基本上不会发生变化,但是当对象的属性发生了变化,对应的反序列化对象没有跟着一起变化,大概率会出现反序列化失败!
为了眼见为实,我们继续以实际案例给大家演示一下。
还是以上面那个为主,我们先序列化一个 Student 对象,里面没有自定义版本号,然后在反序列化操作的时候,我们给Student 对象新增一个属性email ,同时也不自定义版本号。 public class Student implements Serializable { /** * 用户名 */ private String name; /** * 年龄 */ private Integer age; /** * 邮箱 */ private String email; //省略set、get... }
看看运行效果: Exception in thread "main" java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student; local class incompatible: stream classdesc serialVersionUID = 821478144412499207, local class serialVersionUID = -5996907635197467174 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
答案很显然,反序列化报错了!两者的版本号不一致!
在平时开发的过程中,实体类的属性难免会发生改动,我们有些同学啊, 在写代码的时候只是把序列化的接口实现了,但是没有自定义版本号 ,在这点上,我强烈建议大家一定要给每个实现了 Serializable 接口的类,自定义一个版本号,即使对象的属性发生了变化,也不会影响到数据的序列化和反序列化操作!
操作很简单,直接在实体类里面加上这个静态变量即可! //自定义序列化版本号 private static final long serialVersionUID = 1l; 2.5、父类、子类序列化问题
在实际的开发过程中,尤其是实体类,为了对象属性的复用,我们往往会采用继承的方式来处理。
使用了继承之后,父类属性是否可以正常被序列化呢?下面我们一起来看看! 父类没有实现序列化,子类实现序列化
首先我们创建两个类 Parent 和Child ,Child 继承自Parent 。 public class Parent { private String name; public String getName() { return name; } public Parent setName(String name) { this.name = name; return this; } } public class Child extends Parent implements Serializable{ private static final long serialVersionUID = 1l; private String id; public String getId() { return id; } public Child setId(String id) { this.id = id; return this; } }
编写测试类,先序列化,然后再反序列化! public class ObjectMainTest { public static void main(String[] args) throws Exception { serializeAnimal(); deserializeAnimal(); } private static void serializeAnimal() throws Exception { Child black = new Child(); black.setId("123"); black.setName("张三"); System.out.println("id:" + black.getId() + ",name:" + black.getName()); System.out.println("=================开始序列化================"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.log")); oos.writeObject(black); oos.flush(); oos.close(); } private static void deserializeAnimal() throws Exception { System.out.println("=================开始反序列化================"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.log")); Child black = (Child) ois.readObject(); ois.close(); System.out.println("id:" + black.getId() + ",name:" + black.getName()); } }
运行结果如下: id:123,name:张三 =================开始序列化================ =================开始反序列化================ id:123,name:null
结果很明显,父类的属性没有被序列化进去!
我们在来试试,另一种常见 父类实现序列化,子类不实现序列化 public class Parent implements Serializable { private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public Parent setName(String name) { this.name = name; return this; } } public class Child extends Parent { private String id; public String getId() { return id; } public Child setId(String id) { this.id = id; return this; } }
接着运行一次程序,结果如下! id:123,name:张三 =================开始序列化================ =================开始反序列化================ id:123,name:张三
结果很明显,父类的属性被序列化进去!
假如,子类和父类,都实现了序列化,并且序列化版本号都不一样,会不会出现问题呢? 父类实现序列化,子类实现序列化 public class Parent implements Serializable { private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public Parent setName(String name) { this.name = name; return this; } } public class Child extends Parent implements Serializable{ private static final long serialVersionUID = 2l; private String id; public String getId() { return id; } public Child setId(String id) { this.id = id; return this; } }
运行一次程序,结果如下! id:123,name:张三 =================开始序列化================ =================开始反序列化================ id:123,name:张三
父类的属性序列化依然成功,当父、子类都实现了序列化,并且定义了不同的版本号,这种情况下,版本号是跟着子类的版本号走的!
总结起来,当父类实现序列化时,子类所有的属性也会全部被序列化;但是当父类没有实现序列化,子类在序列化时,父类属性并不会被序列化! 2.6、自定义序列化过程
Serializable 接口内部序列化是 JVM 自动实现的,但是在某些少数的场景下,你可能想自定义序列化和反序列化的内容,但是又不想改实体类属性,这个时候你可以采用自定义序列化的实现方式。
自定义序列化方式,其实也很简单,只需要实现 JDK 自身提供的 Externalizable 接口就行,里面有两个核心方法,一个是数据写入,另一个是数据的读取。 public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
Externalizable 接口的实现过程也很简单,我们创建一个Person ,实现自Externalizable 的两个方法。 public class Person implements Externalizable { private static final long serialVersionUID = 1l; private String name; private int age; /** * 实现了Externalizable这个接口时需要提供无参构造,在反序列化时会检测 */ public Person() { System.out.println("Person: empty"); } public Person(String name, int age) { this.name = name; this.age = age; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("person writeExternal..."); out.writeObject(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException { System.out.println("person readExternal..."); name = (String) in.readObject(); age = in.readInt(); } @Override public String toString() { return "Person{" + "name="" + name + """ + ", age=" + age + "}"; } }
测试 Person 对象的序列化和反序列化。 public class ExternalizableMain { public static void main(String[] args) throws IOException, ClassNotFoundException { serializable(); deserializable(); } private static void serializable() throws IOException { Person person = new Person("张三", 15); System.out.println(person.toString()); System.out.println("=================开始序列化================"); FileOutputStream boas = new FileOutputStream("person.log"); ObjectOutputStream oos = new ObjectOutputStream(boas); oos.writeObject(person); oos.close(); boas.close(); } private static void deserializable() throws IOException, ClassNotFoundException { System.out.println("============反序列化============="); ObjectInputStream bis = new ObjectInputStream(new FileInputStream("person.log")); Person person = (Person)bis.readObject(); System.out.println(person.toString()); } }
运行结果如下: Person{name="张三", age=15} =================开始序列化================ person writeExternal... ============反序列化============= Person: empty person readExternal... Person{name="张三", age=15} 四、小结
对象的序列化,在实际的开发过程中,使用的非常频繁,尤其是微服务开发,如果你用的是 SpringBoot + Dubbo 组合的框架,那么在通过rpc 调用的时候,如果传输的对象没有实现序列化,会直接报错!
在使用序列化的时候,坑点还不少,尤其是版本号的问题,这个很容易被忽略,大家在实际开发的时候, 强烈推荐自定义版本号 ,这样可以避免传输的对象属性发生变化的时候,接口反序列化出错的概率!
为什么要开发酒店预约小程序?银弹谷为什么要开发酒店预约小程序?随着社会的发展,人们消费观念正在慢慢改变,比如住酒店,人们更加注重酒店订房是否简单价格是否透明环境是否真实服务是否周到等,那么酒店可以开发一款预约订房小
iPhone13开售瞬间秒没,低价策略实现了对国产厂商的精准打击9月17日晚间8时,iPhone13在各平台开售,几乎在瞬间被用户抢完,苹果中国官网一度瘫痪,时间持续半小时以上,等苹果商店再次恢复,预定的送货时间已经延长至10月10日以后。iP
冷库回收市场的意义与价值冷库回收的意义从我市上世纪70年代的第一台冷库,到今天的商业的繁荣及冷库的遍地开花,可以说每天都有大大小的冷库的安装建设,也同样有许许多多的冷库因为种种的原因而拆掉或废弃掉。也有很
十个问题一文了解集成灶!真实用户从安装到使用全体验艾维巴蒂,我是M慢性子。作为厨房器材党,烤箱灶具买过多款也用过多次,对于这类产品的性能好坏有深入的了解,但对于集成灶,我之前的接触还非常少。集成灶早在十年前就有小区里的邻居安装,当
从重投机到重玩法,谈谈游戏区块链的发展史现如今,在虚拟小宠物拉响了区块链游戏第一枪,行业大量新用户流入的引诱下,各大游戏生产商竞相进军区块链游戏,作为覆盖群体较广的娱乐产业链,游戏行业本来就存有区块链应用的客户群体虚拟产
国内的区块链挖矿游戏现在都有哪些玩法?目前现阶段市面上的国内区块链挖矿游戏越来越火爆了,因为我追随时尚潮流免费试玩了一些国内区块链挖矿游戏,现如今就向大家详细介绍在其中一些游戏的感受体会,便于大家可以掌握和挑选本身想玩
国内区块链游戏目前的几种主要玩法现如今目前市面上的国内区块链游戏愈来愈受欢迎了,我也跟随潮流免费试玩了一些国内区块链游戏,如今就向大伙儿详细介绍其中一些游戏的体验感受,便于大伙儿能够了解和选择自身想玩的区块链游戏
银弹谷软件应用商有必要使用零代码软件开发工具吗?软件应用商有必要使用零代码软件开发工具吗?这个要从整个行业的痛点来看一系统维护难软件应用商往往缺少专业人员的维护,对程序员依赖太大,但是养一个程序员成本又是昂贵的,银弹谷零代码软件
银弹谷软件开发商有必要使用零代码开发工具吗?软件开发商有必要使用零代码开发工具吗?这个要从整个行业目前所存在的问题来看一工期紧人力成本高软件开发交付工期是软件开发商头痛的点,程序员招多了,项目少的时候,人员工资又是一大笔开销
免费的低零代码开发平台都有什么功能?免费的低零代码开发平台都有什么功能?以银弹谷零代码开发平台为例一零代码开发之个性设计页面配置灵活零代码开发之个性设计页面配置灵活。零代码开发平台提供了强大的自定义设计能力,任何表单
Linux绝对路径和相对路径详解在Linux中,简单的理解一个文件的路径,指的就是该文件存放的位置,例如,在Linux文件系统的层次结构中提到的homecat就表示的是cat文件所存放的位置。只要我们告诉Linu