一文彻底掌握Java IO类库

Java开发过程中经常会用到 Java IO 类库,本文将深入源码,带你彻底掌握 Java IO 类库。

Java IO类图框架

Java IO 类库可以大体划分为字节流和字符流两大类,再根据输入和输出两种情况,可以再分为四小类。所以大致框架图如下所示:
Java IO 类图.png

如上图,Java 类图并不繁多,而且分类和命名都非常清晰。其中需要重点掌握的类已经加粗展示在图里。

下边我们逐类逐个展开分析整个类库。

字节流

顾名思义,字节流相关类是处理字节类型数据的,而且都是以『Stream』为后缀的类。根据输入输出类型,可以划分为 InputStream 或 OutputStream 的两大类的实现类。

InputStream

先看看内部方法:

public abstract class InputStream implements Closeable {
    public abstract int read() throws IOException;
    public int read(byte b[]) throws IOException {/***/}
    public int read(byte b[], int off, int len) throws IOException {/***/}
    public long skip(long n) throws IOException {/***/}
    public int available() throws IOException {/***/}
    public void close() throws IOException {/***/}
    public synchronized void mark(int readlimit) {/***/}
    public synchronized void reset() throws IOException {/***/}
    public boolean markSupported() {/***/}

InputStream 实现了 Closeable 接口,并且内部只有一个抽象方法 read, 所有实现类都强制要求实现该方法。其他方法提供了非常扼要的默认实现,实现类可以酌情覆盖实现。

FileInputStream

《深入理解Java文件输入输出流和文件描述符》我们已经深入探讨过该类。

简单来说,FileInputStream 内部通过文件描述符 FileDescriptor 和系统文件关联起来,并通过 native 方法调用系统 API 读写文件。

FilterInputStream

FilterInputStream 采用装饰器模式,内部包装了一个 InputStream 对象,并且继承了 InputStream 并覆写全部方法,但方法内容都是单纯地调用内部包装的 InputStream 对象。如下:

public
class FilterInputStream extends InputStream {
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }

    public long skip(long n) throws IOException {
        return in.skip(n);
    }

    public int available() throws IOException {
        return in.available();
    }

    public void close() throws IOException {
        in.close();
    }


    public synchronized void mark(int readlimit) {
        in.mark(readlimit);
    }

    public synchronized void reset() throws IOException {
        in.reset();
    }

    public boolean markSupported() {
        return in.markSupported();
    }
}

所以正如其名,FilterInputStream 就像是内部 InputStream 对象的一个过滤器一般,所有方法调用都需要经过一层包装方法的『过滤』才能到达内部对象。FilterInputStream 并没有逻辑实现,具体实现需要子类覆写相关方法实现。

比较有意思的实现有以下几个:

  • BufferedInputStream

    BufferedInputStream 内部使用一个 buf 字节数组进行缓冲,覆写了 FilterInputStream 的全部方法实现一个带缓冲区的字节流类。在进行磁盘或网络IO时,原始的InputStream对数据读取的过程都是一个字节一个字节操作的,而BufferedInputStream在其内部提供了一个buffer,在读数据时,会一次读取一大块数据到buffer中,这样比单字节的操作效率要高的多,特别是进程磁盘IO和对大量数据进行读写的时候,能提升IO性能。

  • PushbackInputStream

    PushbackInputStream 内部同样使用一个 buf 字节数组对已读数据进行缓存,然后可以通过 unread 方法将已读的数据重新放回 buf 数组,从而实现了一个支持 push back 的字节流类。

  • DataInputStream

    DataInputStream 提供了许多可以读取 Java 基本类型的方法。

ByteArrayInputStream

ByteArrayInputStream 支持从 byte 数组读取数据,通过构造函数可以指定该 byte 数组:

    protected byte buf[];

    protected int pos;

    protected int mark = 0;

    protected int count;

    public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }

    public ByteArrayInputStream(byte buf[], int offset, int length) {
        this.buf = buf;
        this.pos = offset;
        this.count = Math.min(offset + length, buf.length);
        this.mark = offset;
    }

ObjectInputStream

ObjectInputStream 与 DataInputStream 类似也支持 Java 基本类型的读取,此外还支持反序列化读取对象。它常常与 ObjectOutputStream 搭配使用。因此,ObjectOutputStream 实现将基本类型或者对象序列化并输出到 IO 字节流或者设备上,而 ObjectInputStream 从 IO 字节流或者设备上反序列化读取基本类型或者对象。

比如,从文件中读取一个 person 对象。

ObjectInputStream input = new ObjectInputStream(new FileInputStream("data.txt"));
Person person = (MyClass) input.readObject(); 
input.close();

这里要求 Person 一定要实现 java.io.Serializable 接口。

PipedInputStream

PipedInputStream 通常和 PipedOutputStream 搭配使用,实现了一个承载字节流的管道类。PipedOutputStream 的输出会自动调用 PipedInputStream 的 receive 方法作为输入。PipedInputStream 提供了以下几个特殊方法:

// 连接 PipedOutputStream 对象,形成管道
public void connect(PipedOutputStream src) throws IOException;
// 接收一个字节
protected synchronized void receive(int b) throws IOException;
// 接收一个字节数组
synchronized void receive(byte b[], int off, int len)  throws IOException;

注意到其 read 方法和 receive 都是同步方法,read 方法在没有数据的时候会发生阻塞,而 receive 方法在缓冲数组没有剩余空间的时候也会发生阻塞:

public synchronized int read()  throws IOException {
        if (!connected) {
            throw new IOException("Pipe not connected");
        } else if (closedByReader) {
            throw new IOException("Pipe closed");
        } else if (writeSide != null && !writeSide.isAlive()
                   && !closedByWriter && (in < 0)) {
            throw new IOException("Write end dead");
        }

        readSide = Thread.currentThread();
        int trials = 2;
        while (in < 0) {
            // in小于0表示缓冲数组为空,处于无数据状态
            if (closedByWriter) {
                /* closed by writer, return EOF */
                return -1;
            }
            if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
                throw new IOException("Pipe broken");
            }
            /* might be a writer waiting */
            notifyAll();
            try {
                // 阻塞等待
                wait(1000);
            } catch (InterruptedException ex) {
                throw new java.io.InterruptedIOException();
            }
        }
        int ret = buffer[out++] & 0xFF;
        if (out >= buffer.length) {
            out = 0;
        }
        if (in == out) {
            /* now empty */
            in = -1;
        }

        return ret;
    }


protected synchronized void receive(int b) throws IOException {
        checkStateForReceive();
        writeSide = Thread.currentThread();
        if (in == out)
            // 当in等于out,意味着缓冲数组已满,阻塞等待空间释放
            awaitSpace();
        if (in < 0) {
            in = 0;
            out = 0;
        }
        buffer[in++] = (byte)(b & 0xFF);
        if (in >= buffer.length) {
            in = 0;
        }
    }

SequenceInputStream

SequenceInputStream 支持将多个 InputStream 组合起来,并按照顺序进行读取。

OutputStream

OutputStream 与 InputStream 相对应,实现上存在很多相似之处。先看看内部方法:

public abstract class OutputStream implements Closeable, Flushable {
    public abstract void write(int b) throws IOException;
    public void write(byte b[]) throws IOException {/***/}
    public void write(byte b[], int off, int len) throws IOException {/***/}
    public void flush() throws IOException {/***/}
    public void close() throws IOException {/***/}
}

OutputStream 实现了 Closeable 接口和 Flushable 方法,同样有一个抽象的 write 方法需要实现。其他方法提供框架性代码,也需要实现类覆写相关方法,提供更多的自定义功能。

FileOutputStream

实现上与 FileInputStream 类似,提供对文件写入字节流的功能。

FilterOutputStream

与 FilterInputStream 类似,对 OutputStream 对象进行包装,并继承了 OutputStream 并覆写全部方法,方法内容都是简单地调用内部的 OutputStream 对象。

同样的也有几个子类实现:

  • BufferedOutputStream:带缓冲区的字节流输出类,与 BufferedInputStream 对应;
  • DataOutputStream:提供写 Java 基本类型相关方法的字节流类,与 DataInputStream 对应;
  • PrintStream:与 DataOutputStream 有些类似,不过它提供了更加丰富的写出方法,并且支持换行输出。

ByteArrayOutputStream

与 ByteArrayInputStream 相反,ByteArrayOutputStream 实现输出到内部的缓存字节数组 buf 中。特有的方法有:

/** 将该 Stream 输出为 byte 数组**/
public synchronized byte toByteArray()[] {
    return Arrays.copyOf(buf, count);
}
/** 将该 Stream 输出到另一个 Stream 上**/
public synchronized void writeTo(OutputStream out) throws IOException {
    out.write(buf, 0, count);
}

ObjectOutputStream

与 ObjectInputStream 对应,ObjectOutputStream 实现将 Java 基本类型数据或者 Java 对象序列化后写入输出字节流中。

PipedOutputStream

与 PipedInputStream 搭配使用,PipedOutputStream 会输出字节流到管道另一端的 PipedInputStream。

字符流

字节流处理的是 byte 数组,而字符流处理的是 char 数组。而且字符流相关的类都以 Reader 或者 Writer 为后缀。

Reader

先看看内部方法:

public abstract class Reader implements Readable, Closeable {
    public int read(java.nio.CharBuffer target) throws IOException;
    public int read() throws IOException;
    public int read(char cbuf[]) throws IOException;
    abstract public int read(char cbuf[], int off, int len) throws IOException;
    public long skip(long n) throws IOException;
    public boolean ready() throws IOException;
    public boolean markSupported();
    public void mark(int readAheadLimit) throws IOException;
    public void reset() throws IOException;
    abstract public void close() throws IOException;
}

内部方法与 InputStream 非常相似,同样实现类需要实现 read 方法。

BufferedReader

带缓冲区的 Reader 实现。

CharArrayReader

从字符数组读取数据的 Reader 实现。

InputStreamReader

InputStreamReader 是一个包装类,内部对象是 StreamDecoder。StreamDecoder 支持从 InputStream 中读取字符。

public class InputStreamReader extends Reader {

    private final StreamDecoder sd;

    /**
     * Creates an InputStreamReader that uses the default charset.
     *
     * @param  in   An InputStream
     */
    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            // The default encoding should always be available
            throw new Error(e);
        }
    }
    // ...
}
  • FileReader:InputStreamReader 的实现,入参是一个 FileInputStream 对象

      public FileReader(String fileName) throws FileNotFoundException {
          super(new FileInputStream(fileName));
      }
    

FilterReader

FilterReader 与上文的 FilterInputStream 类似,也是一个包装类。它内部包含一个 Reader 对象,并且继承自 Reader 并覆写所有方法,而方法内容都是简单调用内部 Reader 对象。

  • PushbackReader:FilterReader 的实现类,内部使用一个 buf 字符数组对已读数据进行缓存,然后可以通过 unread 方法将已读的数据重新放回 buf 数组,从而实现了一个支持 push back 的字符流类。

PipedReader

与 PipedInputStream 类似

StringReader

与 CharArrayReader 类似

Writer

BufferedWriter

带缓冲区的 Writer 实现。

CharArrayWriter

与 CharArrayReader 相反,CharArrayWriter 将数据写入内部字符数组中。其特有方法有:

public char toCharArray()[] {
    synchronized (lock) {
        return Arrays.copyOf(buf, count);
    }
}

public void writeTo(Writer out) throws IOException {
    synchronized (lock) {
        out.write(buf, 0, count);
    }
}

OutputStreamWriter

与 InputStreamReader 相对,OutputStreamWriter 也是一个包装类,内部对象是 StreamEncoder。StreamEncoder 支持将字符输出到 OutputStream 中。

  • FileWriterOutputStreamWriter 的实现,入参是一个 FileOutputStream 对象

      public FileWriter(String fileName) throws IOException {
          super(new FileOutputStream(fileName));
      }
    

FilterWriter

FilterWriter 的是一个包装类,内部包含一个 Writer 对象,同时也继承了 Writer,并覆写了部分方法。这个类在 JDK 里没有找到相关实现子类。

PipedWriter

与 PipedReader PipedWriter 会输出字符流到管道另一端的 PipedWriter。

PrintWriter

类似 PrintStream,提供了丰富的写出方法,并且支持换行输出。这里发现其并没有实现 FilterWriter,这点与 PrintStream 的继承结构并不吻合。个人觉得这点不是很好,但是也无伤大雅。

StringWriter

将字符流输出到内部的 StringBuffer 上,同时可以通过 toString 方法获取内部的字符串缓存:

public String toString() {
    return buf.toString();
}

其他

RandomAccessFile

FileInputStream(文件字符流)或 FileReader(文件字节流)来读文件都只能够实现从文件开始顺序读取到文件结束。

而 RandomAccessFile 可以实现随机读写,用来只读取文件中的一部分:

public class RandAccessDemo {
    public static String randomAccessFileRead() throws IOException {
        // 创建一个RandomAccessFile对象
        RandomAccessFile file = new RandomAccessFile( "data.txt", "rw");
        // 通过seek方法来移动读写位置的指针
        file.seek(10);
        // 获取当前指针
        long pointerBegin = file.getFilePointer();
        // 从当前指针开始读
        byte[] contents = new byte[1024];
        file.read( contents);
        long pointerEnd = file.getFilePointer();
        System. out.println( "pointerBegin:" + pointerBegin + "\n" + "pointerEnd:" + pointerEnd + "\n" + new String(contents));
        String dataStr = new String(contents);
        file.close();
        return dataStr;
    }

    public static void randomAccessFileWrite(String dataStr) throws IOException {
        // 创建一个RandomAccessFile对象
        RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
        // 通过seek方法来移动读写位置的指针
        file.seek(10);
        // 获取当前指针
        long pointerBegin = file.getFilePointer();
        // 从当前指针位置开始写
        file.write(dataStr.getBytes());
        long pointerEnd = file.getFilePointer();
        System.out.println("pointerBegin:" + pointerBegin + "\n" + "pointerEnd:" + pointerEnd + "\n");
        file.close();
    }
}

参考资料


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com

×

喜欢就点赞,疼爱就打赏