Java源码阅读与理解(1):二进制IO流篇

0. 前言

感觉深入学习一门语言确实需要深入到源码层面去理解一下语言的设计思想,所以我准备开始进行Java的源码阅读和学习。虽然是准备学习核心的基础库,但也可以看到其十分庞大,所幸Java语言的源码库在阅读层面上可读性还是非常强的,所以虽然很多,一点一滴地研习也总有收获。
另外,一开始阅读源码对做笔记的方式也不太熟悉,希望随着阅读的深入能够发展出一套自己的笔记方法。

IO类有比较清晰的结构图,就根据这一结构进行分析:
在这里插入图片描述

1. 两个二进制流基类:InputSteam与OutputSteam

这两个类是所有以byte为单位进行IO操作的类的基类。

InputStream

定义

public abstract class InputStream implements Closeable
可以看到,它是抽象类并实现了Closeable接口,是用流式传输方法定义一系列Input的接口。·

Closeable接口:
Closable接口重写了AutoClosable接口,二者都只有1个方法声明:void close(),用于指示资源的关闭,但是AutoCloseable的close方法抛出Exception,而前者则抛出IOException,说明Closable接口将资源的打开与关闭限定在IO上。

整体结构

在这里插入图片描述

常量和变量

类中只有3个常量,定义如下:

    private static final int MAX_SKIP_BUFFER_SIZE = 2048;
    private static final int DEFAULT_BUFFER_SIZE = 8192;
    private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

其中定义了buffer的一些信息。应当注意到此处最大的buffer长度是int最大值-8,这是因为有些VM会在数组之前存储一些头部信息,如此一来再申请大可能会出现OutOfMemoryError。自己所设计的buffer乃至数组都应当参照这个上限

构造方法

只有空的构造函数(因为抽象类也用不到构造函数)

类似工厂方法

public static InputStream nullInputStream()
返回一个无法读取字节的InputStream

需要/期待子类重写的方法

  • public int available() throws IOException:返回会根据实现而estimate的能够从流中读取的字节的长度,本类中默认返回0。很多它的实现会返回buffer内的总字节数,但有些不会,所以绝对不能用这个函数的返回值来估算这个输出流内的字节数,当流已经被close掉后,调用它返回IOException。
  • public void close() throws IOException:对Closable的实现,默认为空,需要被重写
  • public synchronized void mark(int readlimit):在InputStream中标记当前的位置,之后调用reset时,流的位置会返回到最后一次mark的位置。readlimit参数表示在一mark处后读取这么多字节后,这一mark就会失效。默认为空。
  • public synchronized void reset() throws IOException:将流位置重置到mark处,默认返回表达不可使用mark/reset的IOException。
  • public boolean markSupported() :返回此字节流能不能使用mark和reset方法,默认返回false。
  • !!public abstract int read() throws IOException:从字节流中读取1个byte,并转化成int返回,使用int的原因是如果遇到了EOF,需要返回-1(这在让byte覆盖所有byte值时无法办到),该方法应在[可以读取byte前,读取到了EOF,抛出错误]时阻塞。没有默认实现,子类必须重写这一方法

已提供的和比较基本的成员方法

  • public int read(byte b[], int off, int len) throws IOException:返回实际读取到的byte数
  • public int read(byte b[]) throws IOException:企图读满byte数组
  • public byte[] readAllBytes() throws IOException
  • public int readNBytes(byte[] b, int off, int len) throws IOException:是上述第二个方法的累计版,实现方法如下:
    public int readNBytes(byte[] b, int off, int len) throws IOException {
        Objects.checkFromIndexSize(off, len, b.length);

        int n = 0;
        while (n < len) {
            int count = read(b, off + n, len - n);
            if (count < 0)
                break;
            n += count;
        }
        return n;
    }

可以看到,它会在一个while循环中持续读取,直到读够了n,或者调用的read方法返回了-1(也即堵到了EOF)才会跳出循环,这样的方法可能适用于流仍在不断读入,但一次无法满足读取的byte的情况,可以通过这一方法确保在动态的流中确保读取到正确数量的字节。另外IO错误可能会导致不一致性,所以调用它出现错误时应当关闭流。

上述四个方法都是从read()方法衍生出来的一系列方便调用的实用方法。另外注意到,这些方法都使用了 Objects.checkFromIndexSize来快速且准确地检查数组下标是否越界,之后的开发应当积极使用这种边界检查方法

  • public long skip(long n) throws IOException:期望在流中跳过并丢弃n个byte的数据,并在之后开始读入,函数返回实际跳过的字节数(这说明有很多情况下,skip掉的字节很可能比期望的少)。这个函数的内部实现不是直接跳过,而是把要跳过的byte先read到无用的buffer中,它的实现相当精妙:
    public long skip(long n) throws IOException {
        long remaining = n;
        int nr;
        if (n <= 0) {
            return 0;
        }
        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        byte[] skipBuffer = new byte[size];
        while (remaining > 0) {
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            if (nr < 0) {
                break;
            }
            remaining -= nr;
        }
        return n - remaining;
    }
  • public void skipNBytes(long n) throws IOException:效果类似于readNBytes,也是通过while循环确保跳过n个字节,并且如果每跳够就到EOF,会抛出更详细的EOFException。
  • public long transferTo(OutputStream out) throws IOException:将现有InputStream中的byte全部读出并写出到给定的OutputStream,返回实际上传输的字节数

OutputStream

定义

public abstract class OutputStream implements Closeable, Flushable

之前已经描述过了Closable的定义,而OutputStream多实现了Flushable。

Flushable接口
该接口定义依然非常简单,只有一个flush方法:void flush() throws IOException;这个接口就是描述流可以将buffer中的内容冲入底层流(underlying stream),而flush方法就用来实现它

同时可以看到,OutputStream也是抽象类,需要一些细节的实现。

整体结构

在这里插入图片描述
可以看到OutputStream类没有类属性,只提供了一系列的方法

构造方法

与InputStream相同的空构造器(构造器也是有必要的,因为继承他们的子类会调用这些构造器)

工厂方法

与InputStream类似的工厂方法public static OutputStream nullOutputStream(),它将丢弃所有字节。

类方法&成员方法

  • public void close() throws IOException:实现Closable的方法,默认为空,需要子类重写
  • public void flush() throws IOException:实现Flushable的方法,默认为空,需要子类重写
  • !!public abstract void write(int b) throws IOException:输出流的核心方法,将一个byte(但是在参数中转换成了int)的数据输出到输出流中,所以输入的b只取低四位。
  • public void write(byte b[], int off, int len) throws IOException:类内定义的实用方法,通过相同的手段检查下标是否越界后,反复调用write实现
  • public void write(byte b[]) throws IOException:调用上一个write方法实现。

InputStream和OutputStream的总结

这两个抽象类总体上定义了字节输入输出流的工作模式,但事实上只是定义了一个流模型的大体框架,现在依然不太明白一些设计细节,随着之后类的实现,应该可以揭示更多设计细节。

2. InputStream的流家族

2.1 FileInputStream

定义与总体结构

public class FileInputStream extends InputStream:只是简单地继承关系,没有实现其它接口

总体结构:在这里插入图片描述

类字段和成员字段

拥有以下五个成员:

  • private volatile boolean closed:指示该流是否已经关闭
  • private final String path;:表示所指向的文件的路径,如果文件是通过一个FileDescriptor描述的,那么这个path的值为null。
  • private final Object closeLock = new Object();:似乎是对流加锁的机制,需要应用在之后的方法中进行探究。
  • private volatile FileChannel channel;FileChannel对象能够对文件进行各种操作,在该流中对文件的各种操作推测应该通过FileChannel实现
  • private final FileDescriptor fd;FileDescriptor对象充当底层的文件句柄,表示打开的文件、打开的套接字或资源接收器。这个类本身就要求被FileInputStream和FileOutputStream包裹。

构造方法

本类有3个构造方法,分别接受File对象,String和FileDescriptor作为参数

  • public FileInputStream(File file) throws FileNotFoundException:通过读入一个文件对象构建一个文件输入流。此时的path就是这个文件的路径,而FileDescriptor可以通过默认构造之后fd.attach(this);的调用进行初始化.

对attach方法探究发现,每一个FileDescriptor对象都以对应着一系列的流包装,它把他们称为parent。每个FileDescriptor维护着一个主要的parent和用ArrayList存放otherParents,并且确保它们不会重复。在这一构造函数中,fd是新建的,attach方法将对应的FileOutputStream设置为它对应的fd的主parent(其实attach的机制就是先来先当亲爹,后来都做后爹)。
attach的内容:

synchronized void attach(Closeable c) {
    if (parent == null) {
        // first caller gets to do this
        parent = c;
    } else if (otherParents == null) {
        otherParents = new ArrayList<>();
        otherParents.add(parent);
        otherParents.add(c);
    } else {
        otherParents.add(c);
    }
}
  • public FileInputStream(String name) throws FileNotFoundException:与上述构造函数类似,使用这个String作为文件的路径构造文件对象,在使用接收文件对象的构造器。
  • public FileInputStream(FileDescriptor fdObj):用fd去初始化一个文件输入流,前者意味着一个已存在的对文件的连接。此时检查fdObj是否为空,再通过系统的SecurityManager检查当前文件输入流所在的线程是否能够从文件读取数据:security.checkRead(fdObj);,检查完毕之后,再把这个输入流attach进fd中,注意到这里往往这个输入流会被加入otherParents,这可能意味着它需要等待其他的输入输出流结束(close掉,因为otherParents是类型为Closable的ArrayList)对文件的操作。

常用成员方法

除了一些对FileChennel和FileDescriptor的操作外,FileInputStream事实上包装了对文件的输入操作,也即对InputStream的一些函数做了重写。但是一些关键的方法,如read,open和skip对应的底层方法read0,open0和skip0使用了native调用,这是java环境封装的底层操作。之后的范式和使用都与InputStream类似。

2.2 FilterInputStream

这个类事实上只是简单地重写了InputStream定义的方法。它最重要的含义就是封装了另外一种InputStream(这在设计模式中被称为过滤器模式?):

protected volatile InputStream in;

在类中,所有重写的方法事实上都调用(在这个类中是简单调用)了被封装的InputStream对应的方法。

FilterInputStream的子类

图中,继承了FilterInputStream的类实际上是对Filter方法的一个具体描述,它们整体上以InputStream为参数,进而初始化父类的FilterInputStream,这样可以通过自身定义的特性(如BufferedInputStream可以在读入时采取缓冲的方式,DataInputStream可以以剧本数据类型的方式从输入流中读取数据)。

注意到,以InputStream作为参数意味着这些子类可以互相包装,最常用的包装方法就是:

DataInputStream di = new DataInputStream(
					 new BufferedInputStream(
					 new FileInputStream("fileName")));

通过这样的封装,最后得到的DataInputStream就可以从文件中(底层包装了文件输入流)读取各种基本类型的数据(最外层实现了DataInput),并且还可以在读取的过程中使用缓冲(中层使用了BufferedInputStream,这意味着从文件输入流中读入的byte将被缓冲于BufferedInputStream,而其又开放了同样的InputStream接口给最上层)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值