关于java:一文带你全面了解java对象的序列化和反序列化

3次阅读

共计 9320 个字符,预计需要花费 24 分钟才能阅读完成。

摘要:这篇文章次要给大家介绍了对于 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 UserInfo() {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()会被调用,用其中返回的对象代替反序列化新建的对象。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0