欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java-Java I/O流解读之Object Serialization and Object Streams

程序员文章站 2022-04-03 16:17:20
...

概述

Java-Java I/O流解读之Object Serialization and Object Streams

数据流(DataInputStream和DataOutputStream)允许我们读取和写入原始数据(如int,double)和String,而不是单个字节。 对象流(ObjectInputStream和ObjectOutputStream)进一步让我们读取和写入整个对象(如Date,ArrayList或任何自定义对象)。

对象序列化是在序列化比特流(bit-stream)中表示“对象的特定状态”的过程,从而将比特流写入外部设备(例如,磁盘文件或网络)。 我们也可以重新构造bit-stream以恢复该对象的状态。

对象序列化对于将对象的状态保存到磁盘文件中以进行持久化是必需的,或者通过网络将对象发送到Web服务,分布式对象应用程序和远程方法调用(RMI)等应用程序。

在Java中,需要序列化的对象必须实现java.io.Serializable或java.io.Externalizable接口。 Serializable接口是一个没有声明的空接口(或标记接口)。 其目的只是声明特定的对象是可序列化的。


方法概述

ObjectOutputStream类实现了ObjectOutput接口,该接口定义了将对象写入输出流的方法:

writeObject(Object)

将对象写入底层存储或流。 如果发生I / O错误,此方法将抛出IOException异常。

将对象写入输出流的过程称为序列化。

ObjectOutput接口从DataOutput接口扩展,这意味着ObjectOutputStream继承了写入基本类型和字符串的所有行为,如DataOutputStream。

同样,ObjectInputStream类实现了ObjectInput接口,该接口定义了一种从输入流读取对象的方法:

readObject()

读取并返回一个对象。
如果找不到序列化对象的类,则此方法抛出ClassNotFoundException,如果发生I / O错误,则抛出IOException。

从输入流重建对象的过程称为反序列化。

ObjectInput接口从DataInput接口扩展,这意味着ObjectInputStream还具有读取原始类型和字符串(如DataInputStream)的行为。

下面的类图描述了对象流类和接口的API层次结构:

Java-Java I/O流解读之Object Serialization and Object Streams


哪些类型的对象有资格进行序列化?

请注意,只有实现java.io.Serializable接口的类的对象才能被写入输出/输入流并从其读取。

Serializable是一个标记界面,它没有定义任何方法。

只有标记为’serializable’的对象可以与ObjectOutputStream和ObjectInputStream一起使用。

Java中的大多数类(包括Date和原始包装器Integer,Double,Long等)都实现了Serializable接口。 我们必须为我们的自定义类实现此接口。

如果尝试编写一个不可序列化的类的对象,将抛出一个java.io.NotSerializableException.


ObjectInputStream & ObjectOutputStream

StudentRecordWriter

package com.xgj.master.java.io.fileDemo.byteStreams.objectStreams;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

/**
 * 
 * 
 * @ClassName: StudentRecordWriter
 * 
 * @Description: This class will uses the ObjectOutputStream class to write a
 *               list of Students object to a file on disk
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年9月7日 下午5:25:28
 */
public class StudentRecordWriter {

    @Test
    public void writeStudent2DiskFile() {

        DateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");

        // JDK 7中的写法
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new BufferedOutputStream(new FileOutputStream(new File("D:\\StudentRecord.txt"))))) {

            List<Student> studentList = new ArrayList<>();

            studentList.add(new Student("Xiao", dateFormat.parse("1993-02-15"), false, 23, 80.5f));

            studentList.add(new Student("Gong", dateFormat.parse("1994-10-03"), true, 22, 95.0f));

            studentList.add(new Student("Jiang", dateFormat.parse("1995-08-22"), false, 21, 79.8f));

            for (Student student : studentList) {
                oos.writeObject(student);
            }

        } catch (IOException | ParseException ex) {
            ex.printStackTrace();
        }
    }
}

StudentRecordReader

package com.xgj.master.java.io.fileDemo.byteStreams.objectStreams;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.junit.Test;

/**
 * 
 * 
 * @ClassName: StudentRecordReader
 * 
 * @Description: StudentRecordReader uses the ObjectInputStream class to read
 *               objects from a file on disk
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年9月7日 下午5:31:50
 */
public class StudentRecordReader {

    @Test
    public void readStudentFromDiskFile() {

        DateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");

        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(new FileInputStream(new File("D:\\StudentRecord.txt"))))) {

            while (true) {

                Student student = (Student) ois.readObject();

                System.out.print(student.getName() + "\t");
                System.out.print(dateFormat.format(student.getBirthday()) + "\t");
                System.out.print(student.getGender() + "\t");
                System.out.print(student.getAge() + "\t");
                System.out.println(student.getGrade());
            }

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

默认情况下,原始类型和数组是可序列化的。

ObjectInputStream和ObjectOutputStream分别实现DataInput和DataOutput接口。我们可以使用readInt(),readDouble(),writeInt(),writeDouble()等方法来读取和写入原始类型。

transient & static
- 静态字段不是序列化的,因为它属于类而不是要序列化的特定实例。
- 为防止某些字段被序列化,请使用关键字transient标记它们。 这可以减少数据流量


java.io.Serializable & Externalizable Interfaces

当我们创建可能被序列化的类时,该类必须实现java.io.Serializable接口。 Serializable接口没有声明任何方法。 诸如Serializable之类的空接口称为标记接口。 它们将实现类标识为具有某些属性,而不需要这些类来实际实现任何方法。

大多数核心Java类都实现了Serializable,例如所有的包装类,集合类和GUI类。 实际上,唯一没有实现Serializable的核心Java类是不应该被序列化的。 原始数组或可序列化对象的数组本身是可序列化的。


java.io.Externalizable Interface

在序列化中,Java虚拟机完全负责写入和读取对象的过程。 这在大多数情况下是有用的,因为程序员不必关心序列化过程的底层细节。 但是,默认序列化不会保护敏感信息,例如密码和凭据,或者程序员想要在序列化过程中要保护某些信息呢?

因此,Externalizable是为了让程序员在序列化期间对对象的读写进行完全控制。

Serializable有一个Externalizable的子接口,如果要自定义类的序列化方式,可以使用它。 由于Externalizable扩展了Serializable,它也是一个Serializable,可以调用readObject()和writeObject()。

Externalizable声明了两种抽象方法:

void writeExternal(ObjectOutput out) throws IOException

The object implements this method to save its contents by calling the methods of DataOutput for its primitive values or calling the writeObject method of ObjectOutput for objects, strings, and arrays.

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

The object implements this method to restore its contents by calling the methods of DataInput for primitive types and readObject for objects, strings and arrays.

ObjectOutput和ObjectInput是由ObjectOutputStream和ObjectInputStream实现的接口,它们分别定义了writeObject()和readObject()方法。

当Externalizable的一个实例传递给ObjectOutputStream时,会绕过默认的序列化过程,会用实例的writeExternal()方法。

类似地,当ObjectInputStream读取一个Exteranlizabled实例时,它使用readExternal()来重建该实例。

示例

假设我们有个User类,实现了externalization 接口

public class User implements Externalizable {
     public static final long serialVersionUID = 1234L;

    // attributes
    private int code;
    private String name;
    private String password;
    private Date birthday;
    private int socialSecurityNumber;

    public User() {
    }

    // methods (getters and setters)
    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return this.password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Date getBirthday() {
        return this.birthday;
    }

    public void setSocialSecurityNumber(int ssn) {
        this.socialSecurityNumber = ssn;
    }

    public int getSocialSecurityNumber() {
        return this.socialSecurityNumber;
    }

    // externalization methods:

    public void writeExternal(ObjectOutput out) {
        // implement your own code to write objects of this class
    }

    public void readExternal(ObjectInput in) {
        // implement your own code to read serialized objects of this class
    }
}

强烈建议所有可序列化的类定义在上面的User类中声明的serialVersionUID常量

public static final long serialVersionUID = 1234L;

这有助于反序列化过程在可序列化类超时更改时保持正确重新构建对象,并避免InvalidClassException。

下面来重写writeExternal() 方法

由于writeExternal()方法使用ObjectOutput,我们可以使用它的方法将对象的状态写入基础流,遵循以下规则:

  • 对于原始类型,使用DataOutput接口的writeXXX()方法,如writeBoolean(),writeByte(),writeInt(),writeLong()等)。

  • 对于对象类型(字符串,数组,自定义类),请使用writeObject()方法

@Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // implement your own code to write objects of this class
        out.writeInt(code);
        out.writeObject(name);
        // write empty password:
        out.writeObject("");
        out.writeObject(birthday);

    }

可以看到,我们序列化以下属性: code, name, password and birthday。 为了安全起见,密码设置为“”,并且没有序列化socialSecurityNumber。 这给出了我们如何通过实现Externalizable接口来控制序列化过程的想法。


下面来重写readExternal() 方法

由于readExternal()方法接受一个ObjectInput,我们可以使用它的方法从基础流中读取对象的状态,遵循以下规则:

  • 对于原始类型,使用DataInput接口的readXXX()方法,如readBoolean(),readByte(),readInt(),readLong()等。

  • 对于对象类型(字符串,数组,您的自定义类),请使用readObject()方法。

@Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // implement your own code to read serialized objects of this class
        this.code = in.readInt();
        this.name = (String) in.readObject();
        this.password = (String) in.readObject();
        this.birthday = (Date) in.readObject();

    }

我们可以看到,我们将以下属性反序列化: code, name, password and birthday。 为了安全起见,socialSecurityNumber没有被反序列化。 这给了我们通过实现Externalizable接口来控制反序列化过程的想法。

输出:

User's details before serialization:
Code: 123
Name: Tom
Birthday: Fri Sep 08 14:02:30 BOT 2017
Password: secret123
socialSecurityNumber: 1234567890
Serialization done

=============

User's details afeter de-serialization:
Code: 123
Name: Tom
Birthday: Fri Sep 08 14:02:30 BOT 2017
Password: 
socialSecurityNumber: 0

代码

代码已托管到Github—> https://github.com/yangshangwei/JavaMaster