工具人

曾几何时,对于Java的序列化的认知始终停留在:「实现个Serializbale接口」不就好了的状态,直到 ...

所以这次抽时间再次从新捧起了尘封已久的《Java编程思维》,就像之前梳理《枚举局部常识》一样,把「序列化和反序列化」这块的知识点又从新扫视了一遍。

序列化是干啥用的?

序列化的本来用意是心愿对一个Java对象作一下“变换”,变成字节序列,这样一来不便长久化存储到磁盘,防止程序运行完结后对象就从内存里隐没,另外变换成字节序列也更便于网络运输和流传,所以概念上很好了解:

  • 序列化:把Java对象转换为字节序列。
  • 反序列化:把字节序列复原为原先的Java对象。

而且序列化机制从某种意义上来说也补救了平台化的一些差别,毕竟转换后的字节流能够在其余平台上进行反序列化来复原对象。

事件就是那么个事件,看起来很简略,不过前面的货色还不少,请往下看。

对象如何序列化?

然而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 = CodeSheepage = 18score = 1000

Serializable接口有何用?

下面在定义Student类时,实现了一个Serializable接口,然而当咱们点进Serializable接口外部查看,发现它居然是一个空接口,并没有蕴含任何办法!

试想,如果下面在定义Student类时忘了加implements Serializable时会产生什么呢?

试验后果是:此时的程序运行会报错,并抛出NotSerializableException异样:

咱们依照谬误提醒,由源码始终跟到ObjectOutputStreamwriteObject0()办法底层一看,才豁然开朗:

如果一个对象既不是字符串数组枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异样!

哦,我明确了!

原来Serializable接口也仅仅只是做一个标记用!!!

它通知代码只有是实现了Serializable接口的类都是能够被序列化的!然而真正的序列化动作不须要靠它实现。

serialVersionUID号有何用?

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

private static final long serialVersionUID = -4392658638228508589L;

你晓得这句申明的含意吗?为什么要搞一个名为serialVersionUID的序列号?

持续来做一个简略试验,还拿下面的Student类为例,咱们并没有人为在外面显式地申明一个serialVersionUID字段。

咱们首先还是调用下面的serialize()办法,将一个Student对象序列化到本地磁盘上的student.txt文件:

public static void serialize() throws IOException {    Student student = new Student();    student.setName("CodeSheep");    student.setAge( 18 );    student.setScore( 100 );    ObjectOutputStream objectOutputStream =         new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );    objectOutputStream.writeObject( student );    objectOutputStream.close();}

接下来咱们在Student类外面动点手脚,比方在外面再减少一个名为studentID的字段,示意学生学号:

这时候,咱们拿方才曾经序列化到本地的student.txt文件,还用如下代码进行反序列化,试图还原出方才那个Student对象:

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 );}

运行发现报错了,并且抛出了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文本,则能够:

这样在序列化Student类对象时,password字段会设置为默认值null,这一点能够从反序列化所失去的后果来看出:

序列化的受控和增强

约束性加持

从下面的过程能够看出,序列化和反序列化的过程其实是有破绽的,因为从序列化到反序列化是有两头过程的,如果被他人拿到了两头字节流,而后加以伪造或者篡改,那反序列化进去的对象就会有肯定危险了。

毕竟反序列化也相当于一种 “隐式的”对象结构 ,因而咱们心愿在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行编写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,此时反序列化立马终止并且报错:

对于下面的代码,有些小伙伴可能会好奇,为什么自定义的privatereadObject()办法能够被主动调用,这就须要你跟一下底层源码来一探到底了,我帮你跟到了ObjectStreamClass类的最底层,看到这里我置信你肯定豁然开朗:

又是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即便是类中定义的private公有办法,也能被抠出来执行了,几乎引起舒服了。

单例模式加强

一个容易被疏忽的问题是:可序列化的单例类有可能并不单例

举个代码小例子就分明了。

比方这里咱们先用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;}

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象代替反序列化新建的对象。

没想到

本认为这篇会很快写完,后果又扯出了这么多货色,不过这样一梳理、一串联,感觉还是清晰了不少。

就这样吧,

起源:开源中国

作者:CodeSheep

原文:序列化/反序列化,我忍你很久了 - hansonwang的个人空间 - OSCHINA