浅析Java中的IO流 -序列化和反序列化流 + 打印流

182 阅读6分钟

文章目录


一、序列化和反序列化流

1. 序列化

在前面IO流的学习中,我们使用流对象读取和写入的数据类型都是数字、字符串等类型。那么如果想将对象保存到文件中,以及使用流来读取文件中保存的对象时,就需要使用Java中的序列化和反序列化机制:

  • 序列化:用一个字节序列表示一个对象,该字节序列包含该对象的数据、类型和对象中存储的属性等信息,通过序列化可以持久化的保存对象
  • 反序列化:根据文件中保存的字节序列,重构对象,将保存的对象的数据、类型和对象中存储的属性等信息读取出来

Java中使用ObjectOutputStream和ObjectInputStream来实现对象的序列化和反序列化。


2. ObjectOutputStream

ObjectOutputStream将Java对象的原始数据类型写出到文件中,从而实现对象的持久化存储。它是java.io.OutputStream的子类,因此,可以使用父类中共性的成员方法:

  • public void close():关闭此输出流并释放与此相关的任何系统资源
  • public void flush():刷新此输出流并强制任何缓冲的输出字节被写入
  • public void write(byte[] b): 将b.length的字节从指定的字符数组写入此输出流
  • public void write(byte[] b, int off, int len): 从指定的字节数组写入len长度的字节,从偏移量off开始输出到此输出流中
  • public abstract void write(int b):将指定的字节输出流

它的构造方法有:

  • ObjectOutputStream(OutputStream out):创建写入指定OutputStream的ObjectOutputStream

它有一个特有的成员方法:

  • void writeObject(Object obj):将指定的对象写入ObjectOutputStream

使用步骤:

  • 创建ObjectOutputStream对象,构造方法中传递字节输出流
  • 使用ObjectOutputStream对象的writeObject()将对象写入到文件中
  • 释放资源
public class Person implements Serializable {

    private int age;
    private String name;

    public Person() {
    }

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
public class ObjectOutputStreamTest {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\data\\Code\\Java_code\\src\\IOStream\\test.txt"));
        oos.writeObject(new Person(10, "Forlogen"));
        oos.close();
    }
}

Serializable接口,也叫标记型接口,要进行序列化和反序列化的类必须实现Serializable接口,它会给类添加一个标记。

public interface Serializable {}

当进行序列化和反序列化时,就会检测类上是否有此标记

  • 有则可正常序列化和反序列化
  • 没有则抛出NotSerializableException异常

3. ObjectInputStream

ObjectInputStream是对象的反序列化流,它是java.io.InputStream的子类,因此可以使用父类中共性成员方法:

  • int read():从输入流中读取数据的下一个字节
  • int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中
  • void close():关闭此输入流并释放与该留相关的所有系统资源

构造方法:

  • ObjectInputStream(InputStream in):创建从指定InputStream读取的ObjectInputStream

特有的成员方法:

  • Object readObject():从ObjectInputStream中读取对象

使用步骤:

  • 创建ObjectInputStream对象,构造方法中传递字节输入流
  • 使用ObjectInputStream对象中的readObject()读取保存对象的文件
  • 释放资源
  • 使用读取出的对象

反序列化的前提:

  • 类必须实现Serializable接口接口
  • 必须存在类对应的.class文件
public class ObjectInoutStreamTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(    "D:\\data\\Code\\Java_code\\src\\IOStream\\test.txt"));
        Object o = ois.readObject();
        ois.close();
        System.out.println(o); // Person{age=10, name='Forlogen'}
    }
}

ClassNotFoundException异常会出现在反序列化的过程中,示意图如下所示:

在这里插入图片描述

为了避免异常的抛出,额可以显式的声明serialVersionUID,格式为:

static final long serialVersionUID = 42L

4. transient关键字

transient关键字也称为瞬态关键字,它用于序列化和反序列化是进行关键字的屏蔽,被此关键字修饰的成员变量就不进行序列化操作;同时在反序列化时也会忽视被它修饰的存变量。例如,如果上面的Person类中的age属性被transient关键字修饰,那么就行序列化->反序列化的操作后,输出的信息就是Person{age=0, name="Forlogen"},此时age会保持默认值。


二、打印流

java.io.PrintStream为其他输出添加了功能,使它们能够方便的打印各种数据值表示形式。PrintStream具有如下特点:

  • 只负责数据的输出,不负责数据的读取
  • 与其他输出流不同,它永远不会抛出IOException,但有可能抛出别的异常

PrintStream的父类为OutputStream,因此可以使用父类中共性的成员方法。

  • public void close():关闭此输出流并释放与此相关的任何系统资源
  • public void flush():刷新此输出流并强制任何缓冲的输出字节被写入
  • public void write(byte[] b): 将b.length的字节从指定的字符数组写入此输出流
  • public void write(byte[] b, int off, int len): 从指定的字节数组写入len长度的字节,从偏移量off开始输出到此输出流中
  • public abstract void write(int b):将指定的字节输出流

此外,PrintStream还有两个特有的方法:

  • print():输出任意类型的值

  • println():输出任意类型的值并换行

    例如我们常使用的System.out.println()就是使用了PrintStream中的print()方法。

        /**
         * Prints a String and then terminate the line.  This method behaves as
         * though it invokes <code>{@link #print(String)}</code> and then
         * <code>{@link #println()}</code>.
         *
         * @param x  The <code>String</code> to be printed.
         */
        public void println(String x) {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    

构造方法:

  • PrintStream(File file):输出目的地是一个文件
  • PrintStream(OutputStream out):输出目的地是一个字节流
  • PrintStream(String fileName):输出目的地是一个文件路径

在使用PrintStream写数据时,如果使用继承自父类的write()写数据,那么查看数据的时候会查询编码表;如果使用自己特有的方法print()println()方法写数据,那么写的数据原样输出。

public class PrintStreamTest {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("D:\\data\\Code\\Java_code\\src\\IOStream\\test.txt");
        ps.write(97); // a
        ps.println(97);  // 97
        ps.close();
    }
}

除了写数据外,PrintStream还可以改变输出语句的目的地,即改变信息应该在哪里输出。PrintStream作为System.setOut()的参数传入,实现输出目的地的改变。

public class PrintStreamTest {
    public static void main(String[] args) throws FileNotFoundException {
        System.out.println("hello world"); // 在控制台输出
        PrintStream ps = new PrintStream("D:\\data\\Code\\Java_code\\src\\IOStream\\test.txt");
        System.setOut(ps);
        System.out.println("HELLO WORLD"); // 在指定的文件中输出
    }
}

除了PrintStream外,还可以使用PrintWriter实现相同的功能。PrintStream、PrintWriter的方法名是完全一致的,PrintWriter类实现了在PrintStream类中的所有print方法,因此一般使用PrintWriter即可。