序列化/反序列化,我忍你很久了

本文 Github开源项目:github.com/hansonwang9… 中已录入,有具体自学编程学习路途、面试题和面经、编程资料及系列技术文章等,资源持续更新中..& / U.


东西人

曾几何时,关于Java的序列化的 x U认知一贯停留在:「完结个Serializbale接口」不就好了的状况,直到 …

所以这次抽时间再次从头捧起了尘封已久的《Java编j [ 0 ~ ` 9 V i程思维》,就像之前整理《枚举部分常识》相同,把「序列化和反序l ` { U } ~ ~ .列化」这块的常识点又从头审视了X e X X g一遍。


序列化是干啥用的?

序列化的本3 T s I [来目的是j b A 5 t _希望对一个Java方针作一下“变换”,变成字节序列,这样一来便当耐久化存[ I * p /储到磁盘,避免程序工作完毕后方针就从内存里消失,其他变换成– z c *字节序列也更便于网络运送和传达,所以概念上很好了解:

  • 序列化:把Java^ n % [ M ` { L B方针转t 0 g 0化为字节序列。
  • 反序列化:把字节序列恢复为原先的Java方针。
序列化/反序列化,我忍你很久了

h r P v B x r = *且序列化机制从某种含义上来说也弥补了途径化的一些差异,究竟转化后的字节约可以在其他途径进步? W t /行反序列化来恢复方针。

作业就是那么个作业,看起来很简略,不过后边的东西还不少,请往下看。


方针如何序列化?

但是Java目前并没有一个I 5 U关键字可以直接去定义一个所谓的“可耐久化”方针。

方针的耐久化和反耐久化需求靠程序员在代码里手动显式地进行序列化和反序列化复原的动作。

举个例子,假定我们要对Student类方针序列化到g N Y W /一个名为student.txt的文本文件中,然后再经过文本文件反序列i h S R h化成Student类方针:

序列化/反序列化,我忍你很久了

1、Student类定义

public class Student implements Serializable {
private String name;
priv{ ; = |ate Integer age;
private Integer score;
@D , ? jOverrif z m * y 9de
public String toString() {
return "Student:" + L i H   a & #'n' +
"name = " + this.name + 'n' +
"age = " + this.age + 'n' +
"sc H 8 m ~ Lore = " + this0 H H ?.score + 'n'
;
}
//H _ _ ... 其他省略 ...
}

2、序列化

public static voi5 + G U Yd serialize(  ) throws IOException {
Student sth C e  v m n l Zudent = ns ! 1 L S Kew Student();
student.se3 C O $tNamee P k q w F |("CodeSheep");
st: S F i ^ I (udent.setAge( 18 );
student.setScore( 1000 )( 3 R _ 7 o;
ObjectOutputStream objecP % LtOutputStream =
new ObjectK z - i w dOutputStream( new FileOutp! . putStream( new File("studens  rt.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
System.out.println("序列化成功!现已生成studen0 o 8 g u ctu g } H # 6 ) `.txt文件& * ] ` v g e m");
System.out.println("==============================================");
}

3、反序列化

public static void deserialize(  ) throws IOException, ClassNotFoundExcepN m C Y w m Q mtionh = . [ { ] t m & {
ObjectInputStream objectInputStream =
new ObjectInputStreaz i w 6m( new FileInputStream( new File("student.txt") ) );
Studen( A m s ~ ; D xt student = (Student) objectInpuG q S ` } a L w tStrea= e N n | V `m.readObject();
objectInputStr/ b S =eam.close();
System.out.println("反序列化效果为:");
System.out.println( student );
}

4、工作效果

操控台打印:

序列化成功!现已生成student.txt文件
==` : -=====================================X P ~ & 0 C M=======
反序列化效果为:] n ) B
Student:
name = CodeSheep
age = 18
score = 1000

Serializable接口有何用?

上面在定义Student类时,完结了一个Serializable接口,但是当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!

序列化/反序列化,我忍你很久了

试想,假设上面在定义Student类时忘了加implements Serializable时会发生什么呢?

实验效果是:此时的程序工作c $ I J o o v {会报错,并抛出NotSerializableException失常:

序列化/反序列化,我忍你很久了

# @ j e们按照过错提示,由源码一贯跟到ObjectOutputStreamwriteObject0()方法底层一看,才茅塞顿开:

序列化/反序列化,我忍你很久了

假设一个方针既不是字符串数组枚举,并且也没有完结Serializable接口的话,在序列化时就会抛出NotSerializableException失常!

哦,我理解了!

本来Serializable接口也仅仅只是做一个标记用!e $ U Z % 9 /!!

它奉告代码只要是完结了Seri( ~ G M ( I _alizable接口的类都是可以被序列化的!但是真实的序列化动作不需求靠它完结。


serialVersionUID号有何用?

相信你必定常常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:

pr[ - ^ivate static final long serialVersionUID = -4392658638228508589L;

你知道这句声明的含义吗?为什么要搞一个名L 3 S 2 q q tserialE Z 3 q [ 1 YVersionUID的序列号?

持续来做一个简略实验,还拿上面的Student类为例,我们并没有人为q : o p在里边显式地声明一个serialVersionUID字段。

我们首先仍是调用上面的serialize()方法,将一个Student方针序列化到本地磁盘上的student.txt文件:

public static void serialize() throws IOEx/ X p l qception {
Student student = new StT o , audent();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 100 );
ObjectC ( D c 1 4 s Y KOutputStream objectO! I ` * : q !utputStream =
new ObjectOutputStream( new FileOuJ D ] G o m 3 - vtputStream( new File("student.txt") ) );
objectOutputStream.wriC | e Y f -teObject( student );
objectO. 5 & { 7 2utputStreamk $ j ! e.close();
}

~ 7 *下来我们在Student类里边动点四肢,比如在里边再增加一个名为studentID的字段,表示学生学号:

序列化/反序列化,我忍你很久了

这时候,我们拿刚才现已序列化到本地的student.txt文件,还用如下代码进行反序列化,企图复原出刚才那个i L ] ;Student方针:

public static void deserial{ * - 5 l 4 5iz6 6 { u t . 0e(  ) throws IOException, ClassNotFoundException {
ObjectInpuM p tStream objectInputStream =
new ObjectInputSt ~ Yream( new FileInputStream( ne8 3 (w File("t 3 o Wstudent.txt") ) );
Student student = (Student) objectInputStream.readZ k 2 Q { ? ^ l SObject();
objectInputStream.close();
System.out.println("反序列化效果t 2 ) _ C G为:");
System.out.println( student );
}

工作发现报错了,并且抛出了InvalidClassException失常:

序列化/反序列化,我忍你很久了

这当地提示的信息十分清K * j ) 2 ) E ! Y晰了:序列化前后的serialVersionUID号码不兼容!

从这当地最起码可以得出两个重要信息:

  • 1、serialVersionUID是序列化前后的仅有标识符
  • 2、默许假设没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

第1个k + [ U 9问题: serialVersionUID序列化ID,可以看成是序列化和反序列I h a w Z C E化进程中的“暗号”,在反序列化时,JVM会把字节约中的序列号ID和被序列化类中的序列号ID做比对,只要两U v % ^ ; – [ 5 h者一起,才干从头反序列化,否则就会报失常来中止反序列化的进程。

第2个问题: 假设在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java工作时环境会根据该类的各方面信息自动地为它生成一个默许的serialVersionUID,一旦像上面相同更改了类[ F q q j 6 ;的结构或者信息,则类的serialVersionUID| I F也会跟着改动!

所以,为了serialVersionUID的确定性,写代码时仍是主[ L G y m = a张,凡是implements Serializabl9 g b M o , Fe的类,都最好人为显式地为它声明一个ser | / T ~ialVersionUID清晰值!

当然,假设不想手动赋值,你也可以凭仗IDE的自动增加功用,比如我使用的IntelliJ IDEA,按alt + enter就可以为类自动生成和增加c 5 ^ P 7 i aserialVersionUID字段,十分便当:

序列化/反序列化,我忍你很久了

两种特殊情况

  • 1、凡是被static润饰的字段是不会被序列化的
  • 2、凡是被trab 6 3 m Z _ 6 Qnsient润饰符润饰的字段也是不会被序列化的

关于第一点,因为序列化保存的是方针的状况而非类W l * A v R 5 ,的状况,所以会忽略static静态域也是理所应当的。

关于第二点R W a W,就需求了解一下transient润饰符的作用了。

假设在序列化某个类的方针时,就是不希望某个字段+ _ T H . m被序列化(比如这个字段存放的是隐私值,如:暗码等),那这时就可以用transient润饰符来润饰该字段。

比如在之前定义的Student类中,加入一个暗码字段,但是% p R – ~ L p r s不希望序列化到txt文本,则可以:

序列化/反序列化,我忍你很久了

这样在序列化Student类方针时,password字段会设M _ H +置为默许值null,这一点可以从反序列化所得到的效果来看出:

序列化/反序列化,我忍你很久了

序列化的受控和加i a S h ~ q x $

束缚性加持

从上面的进程可以看出,序列化和反序! r ` ^列化的进程其实是有缝隙的,因为从序列化到反序列化是有中心j i 7 ? f ? 5进程的,假设被别人拿到了O W h x中心字节约,然后加以编造或者篡改,那反序列化出来的方针就会有必定风险了。

v l ^ H竟反序列化也相当于一种 “隐式的”方针构造 ,因此我们希望在反序列化时,进行受控的方针反序列化动作。

那怎样个受控法呢?

答案便= @ i是: 自行编写readObject()函数,用于方针的反序列化构造,然后提供( c * ! N d / 7束缚性。

已然自行编写readObject()d h P b数,那就可以做许多可X ` M控的作业:比如各种判别作业。

还以上面的StudeR - V /nt类为例,一般来说b i i学生的效果应该在0 ~ 100之间,我们为了避免学生的考试效果在反序列化时被别人篡改成一个奇葩值,我们可以自行编写reaG m # i jdObject()函数用于反序列化的操控:

private void readObject( ObjectInputStream objectInputStream ) throws$ L [ 8 7 x IOException, ClassNotFoundException {
// 调用默许的反序列化函数
objectInputStream.defaultReadObjecu 1 @ _t();
// 手工查看反序列化后学生效果的有效性,若发现有问题,即中止操作!
if( 0 > scV R $ = 7ore || 100 < score ) {
throw new IllegalArgumentException("学生分数只能在0到100之间!");
}
}

比如我故意将学生的分数改为101,此时反序列化立马@ M c { | 中止并且报错:

序列化/反序列化,我忍你很久了

关于上面的代u , b W $码,有些小伙伴可能会好奇,为什么自定义的privatered D v F + y E $adObject()方法可以被自动调用y g f,这就需求你跟一下底层源码来一探究竟了,我帮你跟到了ObjectStreamClass类的最底层,看到这儿我相信你必定茅塞顿开:

序列化/反序列化,我忍你很久了

又是反射机制在起作用!是的,在Javau C C ! {里,公开万物皆可“反射”(诙谐),即使是类中定义的private私有方法,也能被抠出来执行了,几乎引起舒适了。

单例方式增强

一个简略被忽略的问题是:3 F l b s u i _可序列化的单例类有可能并/ e {不单例

举个代码小例子就清楚了。

比如这儿我们先用java写一个常见的「静态内部类」方式的单例方式完结:

public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
priva. ~ w Z ; rte Singleton() {
}
private] k O ^ v f  ~ R static class SingletonHolder {
private staticM | - { p final Singletg X y ? e R Von singleton = new Singleton();
}
public static synchronized Singleton getSingleton() {
return Singletoj I Y 9 ^ $ =nHolder.singleton;
}
}

然后写一个验证主函数:

public class Test2 {
public static void main(4 w o 8 U DString[] args) throws IOException, ClassNotFouN O r G w k a p (ndExce4 i t : { dption {
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(
new FileOutputS+ ( G n Itream( new File("singleton.txt") )
);
// 将单例方针先序列化D - v ,到文本文件singleton.txt中
objectOutputStream.writeObject( Singleton.getSingleton() );
oC , t f 5 X s Z EbjectOutputStream.close();
OR ` TbjectInputStream obi % 6 EjectInputStream =
new ObjectInputStream(
new FileInputStream( new File("singleton.txt") )
);
// 将文本文件singleton.txt中的方针反序列化为siY f O T Wngleton1
Singleton singleton1 = (Singleton) objectInputStg t P lream.. R P y 1 D + /readObject();
objectInputStream.close();
Singleto# K x : w [ J Kn singleton2 = Singleton.getSingleton();
// 工作效果竟打印 false !
System.out.println( singleton1 == singleton2 );
}
}

工作后我们发现:反序列化后的单例方针和t ! A原单例方针并不持平了,这无疑没有抵达我们的方针。

解决方法是:在单例类中手写readResA 6 6 * 0 0 Y Bolve()函数,直接返回单例方针,来逃避之:

private Object re| Y {adResolve() {
return SingletonHolder.singleton;
}
序列化/反序列化,我忍你很久了

这样一来,当反序列化从流中读取方针时,Z w t }readResolve()会被调用,用其间返回的目g e w [ u % L标替代反序列化新( ] B建的方针。


没想到

本以为这篇会很快写完,效果又扯出了这么多东西,不过这样一整理、一串联,感o & G觉仍是清晰了不少。

就这样吧,下篇见。

本文 Github开源项目:github.com/hansonwang9… 中已录入,有具体自学编程学习路途、面试题和面经、编程资料及系列技术文章等,资源持续更新中…


慢一点,才干更快