【Java高级】Java IO进阶,最通俗易懂的IO/对象序列化教程

本文为原创文章,转载请注明出处
查看[Java]系列内容请点击:https://www.jianshu.com/nb/45938443

Java的IO模块分为传统的IO和NIO(即:new IO),NIO会在下一篇文章中说,这里详解IO

IO就是Input和Output,就是输入输出。Java的IO按照操作的对象分为两部分:

  • 一部分是按照字节来操作数据,是由InputStreamOutputStream扩展来的一系列类,扩展的类一般以Stream结尾;
  • 另一部分是按照字符来操作数据(由于字符使用的是Unicode字符,所以:1字符=2字节),扩展自ReaderWriter,扩展的类一般也以ReaderWriter结尾。

InputStream和OutputStream家族

我们看InputStream和OutputStream的定义:

public abstract class InputStream implements Closeable
public abstract class OutputStream implements Closeable, Flushable

两者都是abstract类,都不能实例化,两者都各有一个方法需要实现:readwrite方法。
当需要定义自己的数据输入类的时候,只需要继承自InputStream并实现read方法即可:

public int read()

read方法每次读取一个字节,并转换成int值。
同理,当需要自定义自己的数据输出类的时候,只需要继承自OutputStream并实现write方法即可:

public void write(int b)

write方法每次写出一个字节。

也可以看到,二者都继承了Closeable接口,都需要关闭,OutputStream继承了Flushable接口表示需要刷新,当输出流关闭的时候会自动刷新。

二者的方法说明如下:

InputStream的部分方法说明:

  • public abstract int read():读入一个字节,范围:-1~255
  • public int read(byte b[]):读入b.length个字节到b中,返回读入字节的数量(其他的read不再介绍)
  • public byte[] readAllBytes():读入所有可用的字节
  • public byte[] readNBytes(int len):读入长度位len的字节,其他readNBytes不再介绍
  • public long skip(long n):跳过多少个字节不读
  • public void skipNBytes(long n):没有跳过足够的字节会异常(文件末尾等)
  • public int available():返回当前可用字节数
  • public void close():关闭
  • public synchronized void mark(int readlimit):为输入流的某一位置打标记,有些输入流不支持打标记
  • public boolean markSupported():检测是否支持打标记
  • public synchronized void reset():重新回到上一次mark打标记的地方开始读入
  • public long transferTo(OutputStream out):将一个输入流转换为输出流

OutputStream的部分方法说明:

  • public abstract void write(int b):写出一个字节b
  • public void write(byte b[]):写出字节数组b
  • public void flush():冲刷输出流,一般用来将数据回写
  • public void close():关闭输出流,一般关闭之前要flush或者自动flush

从JDK11开始,这两个类分别提供了public static InputStream nullInputStream()public static OutputStream nullOutputStream()来产生空的输入输出流,一般产生和处理废弃数据使用。

由这两个基础的输入输出流类,衍生出了一大批输入输出流的类,他们分别有不同的功能:

InputStream(基础类)
  |--FileInputStream(处理文件输入)
  |--ObjectInputStream(对象反序列化使用)
  |--ByteArrayInputStream(字节数组输入流,获取字节数组使用)
  |--FilterInputStream(过滤字节输入流,对字节输入做了更多操作,主要用子类)
      |--BufferedInputStream(字节输入缓冲流,处理字节输入)
      |--DataInputStream(处理基本数据类型、String类型等读入,二进制形式存储)
      |--...

OutputStream(基础类)
  |--FileOutputStream(输出到文件)
  |--ObjectOutputStream(对象序列化使用)
  |--ByteArrayOutputStream(字节数组输出流,输出字节数组)
  |--FilterOutputStream(过滤字节输出流,对字节输出做了一定处理)
      |--BufferedOutputStream(字节缓冲输出流)
      |--DataOutputStream(数据类型输出流,输出基本的数据类型、String等,二进制形式存储)
      |--...

所有的类都实现了Closeable接口,在每次使用完之后都需要关闭。

组合使用输入输出流

在实际使用过程中,一般使用组合流的较多,比如对于一个文本文件,里面存储的是一些数字,那么我们可能使用FileInputStream来读入文件,而将之与DataInputStream组合来达到读取数字的目的:

DataInputStream input = new DataInputStream(new FileInputStream("data.txt"));
int a = input.readInt();

同样,输出流也可以进行类似的组合。

Reader和Writer家族

ReaderWriter家族主要是用来处理文本的输入输出,他们将以字符的形式输入输出数据。

首先来看这两个类的定义:

public abstract class Reader implements Readable, Closeable
public abstract class Writer implements Appendable, Closeable, Flushable

两者都是abstract类,继承Reader需要实现readclose方法,继承Writer需要实现writeflushclose方法,其中readwrite方法定义如下:

public abstract int read(char cbuf[], int off, int len) throws IOException;
public abstract void write(char cbuf[], int off, int len) throws IOException;

可以看到,这里实际上是按照char的数据类型读写的。具体的还有很多其他的衍生方法,这里不再介绍。

与上面类似,Reader也可以进行markreset等操作。从JDK11开始,这两个类也提供了空的默认读写流。

他们的类关系图如下所示:

Reader(基础类)
  |--InputStreamReader(使用InputStream作为输入数据源的字符输入流)
  |--BufferedReader(带缓冲的字符输入流,可以按行读)
      |--LineNumberReader(带行号的BufferedReader)
  |--StringReader(String作为数据源)
  |-...

Writer(基础类)
  |--BufferedWriter(带缓冲区的字符输出流)
  |--OutputStreamWriter(输出到OutputStream的字符输出流)
      |--FileWriter(输出到文件)
  |--PrintWriter(可自主格式化的字符输出流)
  |--StringWriter(用来处理字符串)
  |--...

与上面的类似,这些类之间也是可以组合使用的,甚至可以与InputStreamOutputStream一起组合使用,比如我们想从一个文件读入数据也可以这么写:

InputStreamReader reader = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);
int val = reader.read();

Java8之后,可以使用BufferedReader直接产生一个流,对于大文件可以使用流的方法来进行读入:

try (Stream<String> stream = new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"))).lines()) {
    stream.forEach(System.out::println);
}

对于文本输出,可以直接使用PrintWriter来进行输出:

PrintWriter writer = new PrintWriter("data.txt", StandardCharsets.UTF_8);
writer.println("hello");
writer.close();

对象的序列化与反序列化

对于我们很多人经常会用对象的序列化和反序列化来保存一些信息,对象的序列化和反序列化使用的是ObjectInputStreamObjectOutputStream来完成的。我们先来看其各自的定义:

public void writeObject(Object obj) throws IOException; // 来源于ObjectOutputStream,用于保存对象
public Object readObject() throws ClassNotFoundException, IOException; // 来源于ObjectInputStream,用于读入对象

在正式开始介绍之前,请自行运行如下代码:

import java.io.*;

public class Test {

    public static void main(String[] args) throws Exception {
        Entity e1 = new Entity(1, "this is e1");
        Entity e2 = new Entity(2, "this is e2");
        Entity e3 = new Entity(3, "this is e3");
        e1.next = e3;
        e2.next = e3;

        serialize(e1, "D:\\e1.dat");
        serialize(e2, "D:\\e2.dat");

        e1 = unserialize("D:\\e1.dat");
        e2 = unserialize("D:\\e2.dat");

        System.out.println("e1:val=" + e1.val + " desc:" + e1.desc);
        System.out.println("e2:val=" + e2.val + " desc:" + e2.desc);
        System.out.println("e1.next:val=" + e1.next.val + " desc:" + e1.next.desc);
        System.out.println("e1.next==e2.next: " + (e1.next == e2.next));
    }

    // 序列化
    public static void serialize(Entity e, String file) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file));
        outputStream.writeObject(e);
    }

    // 反序列化
    public static Entity unserialize(String file) throws Exception {
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
        return (Entity) inputStream.readObject();
    }

    private static class Entity implements Serializable {
        int val;
        transient String desc;

        Entity next;

        Entity() {
        }

        Entity(int val, String desc) {
            this.val = val;
            this.desc = desc;
        }
    }
}

预期输出:

e1:val=1 desc:null
e2:val=2 desc:null
e1.next:val=3 desc:null
e1.next==e2.next: false

当我们调用序列化的代码时,序列化的流程图如下图所示:


对象序列化流程

这里要注意几个事情:

  • 一个对象中包含了其他对象,其他对象也会被序列化
  • 已经被序列化的对象只会保存其序列化的序列号,而不会重复序列化(这样能保证反序列化后指向的是同一个对象,上面的例子两个对象是分别序列化的,所以不适用这条)
  • 序列化的每个对象在该文件中都有一个唯一的序列号(自动生成,不是serialVersionUID
  • transient关键字标记和static的变量不会被序列化
  • 可以自定义自己的序列化和反序列化方法,可以在方法中对trainsient关键字标记的字段等进行序列化,例如HashMap中的writeObjectreadObject方法

对于枚举类型的序列化,需要有一些特殊的序列化方法,用到的时候请自行查阅

序列化的版本管理

为了保持序列化的兼容情况,一般在需要被序列化的类中添加一个serialVersionUID静态最终变量,来表示这个类的指纹信息,对象的输入流拒绝序列化不同指纹的对象,所以想要序列化的数据版本兼容,最好自己定义一个serialVersionUID并赋予一个唯一的值,后续这个值就不会再改变了,除非你想标记为不与以前的版本兼容...

public static final long serialVersionUID = 9273862375L;

上面介绍的IO相关的内容都是阻塞的IO,也就是说,一个InputStream读入不到字符的时候就会等待到一直读到字符为止,这种方式也叫BIO,就是Block IO的意思,后续会介绍非阻塞的IO

下一节将介绍NIO

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。