美文网首页Java web一些收藏
序列化和反序列化

序列化和反序列化

作者: Djbfifjd | 来源:发表于2020-08-28 09:49 被阅读0次

    一、序列化:将 Java 对象转换成字节流的过程

    1️⃣序列化过程:是指把一个 Java 对象变成二进制内容,实质上就是一个 byte[] 数组。因为序列化后可以把 byte[] 保存到文件中,或者把 byte[] 通过网络传输到远程(IO),这样,就相当于把 Java 对象存储到文件或者通过网络传输出去了。
    2️⃣一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

    public interface Serializable {}
    

    Serializable接口没有定义任何方法,它是一个空接口。这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

    二、反序列化:将字节流转换成 Java 对象的过程。

    反序列化过程:把一个二进制内容(也就是 byte[] 数组)变回 Java 对象。有了反序列化,保存到文件中的 byte[] 数组又可以“变回” Java 对象,或者从网络上读取 byte[] 并把它“变回” Java 对象。以下是一些使用序列化的示例:
    ①以面向对象的方式将数据存储到磁盘上的文件。例如,Redis存储Student对象的列表。
    ②将程序的状态保存在磁盘上。例如,保存游戏状态。
    ③通过网络以表单对象形式发送数据。例如,在聊天应用程序中以对象形式发送消息。

    三、为什么需要序列化与反序列化

    当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。当两个Java进程进行通信时,需要Java序列化与反序列化实现进程间的对象传送。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

    优点:
    ①实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)。
    ②通过序列化以字节流的形式使对象在网络中进行传递和接收。
    ③通过序列化在进程间传递对象。

    注意事项:
    1️⃣某个类可以被序列化,则其子类也可以被序列化
    2️⃣声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据。
    3️⃣反序列化读取序列化对象的顺序要保持一致。

    四、Java中的序列化如何工作

    当 Java 对象需要在网络上传输或者持久化存储到文件中时,就需要对 Java 对象进行序列化处理。当且仅当对象的类实现java.io.Serializable接口时,该对象才有资格进行序列化,然而真正的序列化动作不需要靠它完成。可序列化是一个标记接口(不包含任何方法),该接口告诉Java虚拟机(JVM)该类的对象已准备好写入持久性存储或通过网络进行读取。

    序列化算法一般会按步骤做如下事情:
    ①将对象实例相关的类元数据输出。
    ②递归地输出类的超类描述直到不再有超类。
    ③类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
    ④从上至下递归输出实例的数据

    默认情况下,JVM负责编写和读取可序列化对象的过程。序列化/反序列化功能通过对象流类的以下两种方法公开:
    1️⃣ObjectOutputStream.writeObject(Object):将可序列化的对象写入输出流。如果要序列化的某些对象未实现Serializable接口,则此方法将引发NotSerializableException。

    按照提示,由源码一直跟到ObjectOutputStream的writeObject0()底层:

    2️⃣ObjectInputStream.readObject():从输入流读取,构造并返回一个对象。如果找不到序列化对象的类,则此方法将引发ClassNotFoundException。readObject()返回一个Object类型的对象,因此需要将其强制转换为可序列化的类,在这种情况下为String类。

    如果序列化使用的类有问题,则这两种方法都将引发InvalidClassException,如果发生 I/O 错误,则将引发IOException。无论NotSerializableException和InvalidClassException是子类IOException异常。

    举个例子,对Student类对象序列化到一个名为student.txt的文本文件中,然后再通过文本文件反序列化成Student类对象:
    ①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'
            ;
        }
        // ... 其他省略 ...
    }
    

    ②序列化

    public static void serialize() throws IOException {
        Student student = new Student();
        student.setName("new");
        student.setAge(18);
        student.setScore(100);
        ObjectOutputStream objectOutputStream = 
            new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
        objectOutputStream.writeObject( student );
        objectOutputStream.close();
        System.out.println("序列化成功!已经生成student.txt文件");
        System.out.println("============================");
    }
    

    ③反序列化

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

    ④运行结果:

    序列化成功!已经生成student.txt文件
    ============================
    反序列化结果为:
    Student:
    name = new
    age = 18
    score = 100
    

    五、什么是serialVersionUID常数

    import java.io.*;
    import java.util.*;
     
    public class Student extends Person implements Serializable {
        public static final long serialVersionUID = 1234L;
     
        private long studentId;
        private String name;
        private transient int age;
     
        public Student(long studentId, String name, int age) {
            super();
            this.studentId = studentId;
            this.name = name;
            this.age = age;
            System.out.println("Constructor");
        }
     
        public String toString() {
            return String.format("%d - %s - %d", studentId, name, age);
        }
    }
    

    如上代码:

    • long serialVersionUID类型的常量。
    • 成员变量age被标记为transient。

    serialVersionUID是一个常数,用于唯一标识可序列化类的版本。从输入流构造对象时,JVM在反序列化过程中检查此常数。如果正在读取的对象的serialVersionUID与类中指定的序列号不同,则JVM抛出InvalidClassException。这是为了确保正在构造的对象与具有相同serialVersionUID的类兼容。

    serialVersionUID是可选的。如果不显式声明,Java编译器将自动生成一个。那么,为什么要显式声明serialVersionUID呢?

    原因是:自动生成的serialVersionUID是基于类的元素(成员变量、方法和构造函数等)计算的。如果这些元素之一发生更改,serialVersionUID也将更改。想象一下这种情况:

    ①一个程序,将Student类的某些对象存储到文件中。Student类没有显式声明的serialVersionUID。
    ②而后更新了Student类(比如新增了一个私有方法),现在自动生成的serialVersionUID也被更改了。
    ③该程序无法反序列化先前编写的Student对象,因为那里的serialVersionUID不同。JVM抛出InvalidClassException。

    这就是为什么建议为可序列化类显式添加serialVersionUID的原因。

    六、什么是瞬时变量?

    上面Student类的成员变量age被标记为transient,JVM 在序列化过程中会跳过瞬态变量。这意味着在序列化对象时不会存储age变量的值。因此,如果成员变量不需要序列化,则可以将其标记为瞬态。以下代码将Student对象序列化为名为“ students.ser”的文件:

    String filePath = "students.ser";
    Student student = new Student(123, "John", 22);
    try (
        FileOutputStream fos = new FileOutputStream(filePath);
        ObjectOutputStream outputStream = new ObjectOutputStream(fos);
    ) {
        outputStream.writeObject(student);
    } catch (IOException ex) {
        System.err.println(ex);
    }
    

    请注意,在序列化对象之前,变量age的值为22。下面的代码从文件中反序列化Student对象:

    String filePath = "students.ser";
    try (
        FileInputStream fis = new FileInputStream(filePath);
        ObjectInputStream inputStream = new ObjectInputStream(fis);
    ) {
        Student student = (Student) inputStream.readObject();
        System.out.println(student);
    } catch (ClassNotFoundException ex) {
        System.err.println("Class not found: " + ex);
    } catch (IOException ex) {
        System.err.println("IO error: " + ex);
    }
    

    此代码输出如下:
    1个
    123 - John - 0

    七、总结

    1️⃣序列化一个对象时,它所引用的所有其他对象也会被序列化,依此类推,直到序列化完整的对象树为止。
    2️⃣如果超类实现Serializable,则其子类会自动执行。
    3️⃣反序列化可序列化类的实例时,构造函数将不会运行。
    4️⃣如果超类未实现Serializable,则在反序列化子类对象时,超类构造函数将运行。
    5️⃣静态变量未序列化,因为它们不是对象本身的一部分。
    6️⃣如果序列化集合或数组,则每个元素都必须可序列化。单个不可序列化的元素将导致序列化失败(NotSerializableException)。
    7️⃣JDK中的可序列化类包括原始包装器(Integer,Long,Double等)、String、Date、collection类等。对于其他类,请查阅相关的Javadoc来了解它们是否可序列化。
    8️⃣相关接口及类:
    ①java.io.Serializable
    ②java.io.Externalizable
    ③ObjectOutput
    ④ObjectInput
    ⑤ObjectOutputStream
    ⑥ObjectInputStream

    相关文章

      网友评论

        本文标题:序列化和反序列化

        本文链接:https://www.haomeiwen.com/subject/espzjktx.html