摘要:这篇文章次要给大家介绍了对于java中对象的序列化与反序列化的相干内容,文中通过具体示例代码介绍,心愿能对大家有所帮忙。
本文分享自华为云社区《java中什么是序列化和反序列化?》,原文作者:dayu_dls 。
这篇文章次要给大家介绍了对于java中对象的序列化与反序列化的相干内容,文中通过具体示例代码介绍,心愿能对大家有所帮忙。
1、序列化是干啥用的?
序列化的本来用意是心愿对一个Java对象作一下“变换”,变成字节序列,这样一来不便长久化存储到磁盘,防止程序运行完结后对象就从内存里隐没,另外变换成字节序列也更便于网络运输和流传,所以概念上很好了解:
- 序列化:把Java对象转换为字节序列。
- 反序列化:把字节序列复原为原先的Java对象。
而且序列化机制从某种意义上来说也补救了平台化的一些差别,毕竟转换后的字节流能够在其余平台上进行反序列化来复原对象。
2、对象序列化的形式?
在Java中,如果一个对象要想实现序列化,必须要实现上面两个接口之一:
- Serializable 接口
- Externalizable 接口
那这两个接口是如何工作的呢?两者又有什么关系呢?咱们别离进行介绍。
2.1 Serializable 接口
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包含private属性、包含其援用的对象)都能够被序列化和反序列化来保留、传递。不想序列化的字段能够应用transient润饰。
因为Serializable对象齐全以它存储的二进制位为根底来结构,因而并不会调用任何构造函数,因而Serializable类无需默认构造函数,然而当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因而该父类必须有默认构造函数,否则会抛异样。
应用transient关键字阻止序列化尽管简略不便,但被它润饰的属性被齐全隔离在序列化机制之外,导致了在反序列化时无奈获取该属性的值,而通过在须要序列化的对象的Java类里退出writeObject()办法与readObject()办法能够管制如何序列化各属性,甚至齐全不序列化某些属性或者加密序列化某些属性。
2.2 Externalizable 接口
它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 办法,用来决定如何序列化和反序列化。
因为序列化和反序列化办法须要本人实现,因而能够指定序列化哪些属性,而transient在这里有效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列形式的。如果把类的不带参数的构造方法删除,或者把该构造方法的拜访权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异样,因而Externalizable对象必须有默认构造函数,而且必须是public的。
2.3 比照
应用时,你只想暗藏一个属性,比方用户对象user的明码pwd,如果应用Externalizable,并除了pwd之外的每个属性都写在writeExternal()办法里,这样显得麻烦,能够应用Serializable接口,并在要暗藏的属性pwd后面加上transient就能够实现了。如果要定义很多的非凡解决,就能够应用Externalizable。
当然这里咱们有一些纳闷,Serializable 中的writeObject()办法与readObject()办法科能够实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 办法也能够,他们有什么异同呢?
- readExternal(),writeExternal()两个办法,这两个办法除了办法签名和readObject(),writeObject()两个办法的办法签名不同之外,其办法体齐全一样。
- 须要指出的是,当应用Externalizable机制反序列化该对象时,程序会应用public的无参结构器创立实例,而后才执行readExternal()办法进行反序列化,因而实现Externalizable的序列化类必须提供public的无参结构。
- 尽管实现Externalizable接口能带来肯定的性能晋升,但因为实现ExternaLizable接口导致了编程复杂度的减少,所以大部分时候都是采纳实现Serializable接口方式来实现序列化。
3、Serializable 如何序列化对象?
3.1 Serializable演示
然而Java目前并没有一个关键字能够间接去定义一个所谓的“可长久化”对象。
对象的长久化和反长久化须要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。
举个例子,如果咱们要对Student类对象序列化到一个名为student.txt的文本文件中,而后再通过文本文件反序列化成Student类对象:
1、Student类定义
public class Student implements Serializable { private String name; private Integer age; private Integer score; @Override public String toString() { return "Student:" + '\n' + "name = " + this.name + '\n' + "age = " + this.age + '\n' + "score = " + this.score + '\n' ; } // ... 其余省略 ... }
2、序列化
public static void serialize( ) throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge( 18 ); student.setScore( 1000 ); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) ); objectOutputStream.writeObject( student ); objectOutputStream.close(); System.out.println("序列化胜利!曾经生成student.txt文件"); System.out.println("=============================================="); }
3、反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("student.txt") ) ); Student student = (Student) objectInputStream.readObject(); objectInputStream.close(); System.out.println("反序列化后果为:"); System.out.println( student ); }
4、运行后果
控制台打印:
序列化胜利!曾经生成student.txt文件 ============================================== 反序列化后果为: Student: name = CodeSheep age = 18 score = 1000
3.2 Serializable接口有何用?
下面在定义Student类时,实现了一个Serializable接口,然而当咱们点进Serializable接口外部查看,发现它居然是一个空接口,并没有蕴含任何办法!
试想,如果下面在定义Student类时忘了加implements Serializable时会产生什么呢?
试验后果是:此时的程序运行会报错,并抛出NotSerializableException异样:
咱们依照谬误提醒,由源码始终跟到ObjectOutputStream的writeObject0()办法底层一看,才豁然开朗:
如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异样!
原来Serializable接口也仅仅只是做一个标记用!!!它通知代码只有是实现了Serializable接口的类都是能够被序列化的!然而真正的序列化动作不须要靠它实现。
3.3 serialVersionUID号有何用?
置信你肯定常常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:
private static final long serialVersionUID = -4392658638228508589L;
你晓得这句申明的含意吗?为什么要搞一个名为serialVersionUID的序列号?
持续来做一个简略试验,还拿下面的Student类为例,咱们并没有人为在外面显式地申明一个serialVersionUID字段。
咱们首先还是调用下面的serialize()办法,将一个Student对象序列化到本地磁盘上的student.txt文件:
接下来咱们在Student类外面动点手脚,比方在外面再减少一个名为id的字段,示意学生学号:
public class Student implements Serializable { private String name; private Integer age; private Integer score; private Integer id;
这时候,咱们拿方才曾经序列化到本地的student.txt文件,还用如下代码进行反序列化,试图还原出方才那个Student对象:
运行发现报错了,并且抛出了InvalidClassException异样
这中央提醒的信息十分明确了:序列化前后的serialVersionUID号码不兼容!
从这中央最起码能够得出两个重要信息:
1、serialVersionUID是序列化前后的惟一标识符
2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它主动申明一个!
第1个问题: serialVersionUID序列化ID,能够看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者统一,能力从新反序列化,否则就会报异样来终止反序列化的过程。
第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会依据该类的各方面信息主动地为它生成一个默认的serialVersionUID,一旦像下面一样更改了类的构造或者信息,则类的serialVersionUID也会跟着变动!
所以,为了serialVersionUID的确定性,写代码时还是倡议,但凡implements Serializable的类,都最好人为显式地为它申明一个serialVersionUID明确值!
当然,如果不想手动赋值,你也能够借助IDE的主动增加性能,比方我应用的IntelliJ IDEA,按alt + enter就能够为类主动生成和增加serialVersionUID字段,非常不便:
两种非凡状况
1、但凡被static润饰的字段是不会被序列化的
2、但凡被transient修饰符润饰的字段也是不会被序列化的
对于第一点,因为序列化保留的是对象的状态而非类的状态,所以会疏忽static动态域也是理所应当的。
对于第二点,就须要理解一下transient修饰符的作用了。
如果在序列化某个类的对象时,就是不心愿某个字段被序列化(比方这个字段寄存的是隐衷值,如:明码等),那这时就能够用transient修饰符来润饰该字段。
比方在之前定义的Student类中,退出一个明码字段,然而不心愿序列化到txt文本,则能够:
public class Student implements Serializable { private static final long serialVersionUID = -4392658638228508589L; private transient String name; private Integer age; private Integer score; private transient String passwd;
这样在序列化Student类对象时,password字段会设置为默认值null,这一点能够从反序列化所失去的后果来看出:
public static void serialize() throws IOException { Student student = new Student(); student.setName("CodeSheep"); student.setAge(18); student.setScore(1000); student.setPasswd("123");
4、实现Externalizable
public Use<mark style="color:transparent">来源gaodaimacom搞#代%码网</mark>rInfo() { userAge=20;//这个是在第二次测试应用,判断反序列化是否通过结构器 } public void writeExternal(ObjectOutput out) throws IOException { // 指定序列化时候写入的属性。这里依然不写入年龄 out.writeObject(userName); out.writeObject(usePass); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 指定反序列化的时候读取属性的程序以及读取的属性 // 如果你写反了属性读取的程序,你能够发现反序列化的读取的对象的指定的属性值也会与你写的读取形式一一对应。因为在文件中装载对象是有序的 userName=(String) in.readObject(); usePass=(String) in.readObject(); }
咱们在序列化对象的时候,因为这个类实现了Externalizable 接口,在writeExternal()办法里定义了哪些属性能够序列化,哪些不能够序列化,所以,对象在通过这里就把规定能被序列化的序列化保留文件,不能序列化的不解决,而后在反序列的时候主动调用readExternal()办法,依据序列程序挨个读取进行反序列,并主动封装成对象返回,而后在测试类接管,就实现了反序列。
Externalizable 实例类的惟一个性是能够被写入序列化流中,该类负责保留和复原实例内容。 若某个要齐全管制某一对象及其超类型的流格局和内容,则它要实现 Externalizable 接口的 writeExternal 和 readExternal 办法。这些办法必须显式与超类型进行协调以保留其状态。这些办法将代替定制的 writeObject 和 readObject 办法实现。
- writeExternal(ObjectOutput out)
该对象可实现 writeExternal 办法来保留其内容,它能够通过调用 DataOutput 的办法来保留其根本值,或调用 ObjectOutput 的 writeObject 办法来保留对象、字符串和数组。 - readExternal(ObjectInput in)
对象实现 readExternal 办法来复原其内容,它通过调用 DataInput 的办法来复原其根底类型,调用 readObject 来复原对象、字符串和数组。
externalizable和Serializable的区别:
1、实现serializable接口是默认序列化所有属性,如果有不须要序列化的属性应用transient润饰。externalizable接口是serializable的子类,实现这个接口须要重写writeExternal和readExternal办法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。
2、实现serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个长久化状态,再将这个状态赋值给该类的另一个变量。实现externalizable接口的对象序列化文件进行反序列化先走构造方法失去控对象,而后调用readExternal办法读取序列化文件中的内容给对应的属性赋值。
5、序列化的受控和增强
5.1 约束性加持
从下面的过程能够看出,序列化和反序列化的过程其实是有破绽的,因为从序列化到反序列化是有两头过程的,如果被他人拿到了两头字节流,而后加以伪造或者篡改,那反序列化进去的对象就会有肯定危险了。
毕竟反序列化也相当于一种 “隐式的”对象结构 ,因而咱们心愿在反序列化时,进行受控的对象反序列化动作。
那怎么个受控法呢?
答案就是: 自行编写readObject()函数,用于对象的反序列化结构,从而提供约束性。
既然自行编写readObject()函数,那就能够做很多可控的事件:比方各种判断工作。
还以下面的Student类为例,一般来说学生的问题应该在0 ~ 100之间,咱们为了避免学生的考试成绩在反序列化时被他人篡改成一个奇葩值,咱们能够自行编写readObject()函数用于反序列化的管制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException { // 调用默认的反序列化函数 objectInputStream.defaultReadObject(); // 手工查看反序列化后学生问题的有效性,若发现有问题,即终止操作! if( 0 > score || 100 < score ) { throw new IllegalArgumentException("学生分数只能在0到100之间!"); } }
比方我成心将学生的分数改为101,此时反序列化立马终止并且报错:
对于下面的代码,为什么自定义的private的readObject()办法能够被主动调用,跟一下底层源码来一探到底,跟到了ObjectStreamClass类的最底层,是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即便是类中定义的private公有办法,也能被抠出来执行了,几乎引起舒服了。
5.2 单例模式加强
一个容易被疏忽的问题是:可序列化的单例类有可能并不单例!
举个代码小例子就分明了。
比方这里咱们先用java写一个常见的「动态外部类」形式的单例模式实现:
public class Singleton implements Serializable { private static final long serialVersionUID = -1576643344804979563L; private Singleton() { } private static class SingletonHolder { private static final Singleton singleton = new Singleton(); } public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } }
而后写一个验证主函数:
public class Test2 { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream( new File("singleton.txt") ) ); // 将单例对象先序列化到文本文件singleton.txt中 objectOutputStream.writeObject( Singleton.getSingleton() ); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream( new File("singleton.txt") ) ); // 将文本文件singleton.txt中的对象反序列化为singleton1 Singleton singleton1 = (Singleton) objectInputStream.readObject(); objectInputStream.close(); Singleton singleton2 = Singleton.getSingleton(); // 运行后果竟打印 false ! System.out.println( singleton1 == singleton2 ); } }
运行后咱们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到咱们的指标。
解决办法是:在单例类中手写readResolve()函数,间接返回单例对象:
private Object readResolve() { return SingletonHolder.singleton; } package serialize.test; import java.io.Serializable; public class Singleton implements Serializable { private static final long serialVersionUID = -1576643344804979563L; private Singleton() { } private static class SingletonHolder { private static final Singleton singleton = new Singleton(); } public static synchronized Singleton getSingleton() { return SingletonHolder.singleton; } private Object readResolve() { return SingletonHolder.singleton; } }
这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象代替反序列化新建的对象。
点击关注,第一工夫理解华为云陈腐技术~