OpenJDK 源码阅读之 Java 字节流输入类的实现

Java 的输入输出总是给人一种很混乱的感觉,要想把这个问题搞清楚,必须对各种与输入输出相关的类之间的关系有所了解。只有你了解了他们之间的关系,知道设计这个类的目的是什么,才能更从容的使用他们。

我们先对 Java I/O 的总体结构进行一个总结,再通过分析源代码,给出把每个类的关键功能是如何实现的。

Java
I/O 的主要结构

Java 的输入输出,主要分为以下几个部分:

  • 字节流
  • 字符流
  • Socket
  • 新 I/O

每个部分,都包含了输入和输出两部分。

实现概要

这里只给出每个类的实现概要,具体每个类的实现分析,可以参见我的 GitHub-SourceLearning-OpenJDK 页面。根据导航中的链接,进入 java.io ,即可看到对每个类的分析。

字节流输入

图1 Java 字节输入类

  • InputStream

InputStream 是所有字节输入类的基类,它有一个未实现的 read 方法,子类需要实现这个 read 方法,
它和数据的来源相关。它的各种不同子类,或者是添加了功能,或者指明了不同的数据来源。

public abstract int read() throws IOException;
  • ByteArrayInputStream

ByteArrayInputStream 有一个内部 buffer ,
包含从流中读取的字节,还有一个内部 counter, 跟踪下一个要读入的字节。

protected byte buf[];
protected int pos;

这个类在初始化时,需要指定一个 byte[],作为数据的来源,它的 read,就读入这个 byte[] 中所包含的数据。

public ByteArrayInputStream(byte buf[]) {
    this.buf = buf;
    this.pos = 0;
    this.count = buf.length;
}
public synchronized int read() {
    return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
  • FileInputStream

FileInputStream 的数据来源是文件,即从文件中读取字节。初始化时,需要指定一个文件:

public FileInputStream(File file)
throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    open(name);
}

以后读取的数据,都来自于这个文件。这里的 read 方法是一个 native 方法,它的实现与操作系统相关。

public native int read() throws IOException;
  • FilterInputStream

FilterInputStream将其它输入流作为数据来源,其子类可以在它的基础上,对数据流添加新的功能。我们经常看到流之间的嵌套,以添加新的功能。就是在这个类的基础上实现的。所以,它的初始化中,会指定一个字节输入流:

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

读取操作,就依靠这个流实现:

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

BufferedInputStream 是 FilterInputStream 的子类,所以,需要给它提供一个底层的流,用于读取,而它本身,则为此底层流增加功能,即缓冲功能。以减少读取操作的开销,提升效率。

protected volatile byte buf[];

内部缓冲区由一个 volatile byte 数组实现,大多线程环境下,一个线程向 volatile 数据类型中写入的数据,会立即被其它线程看到。

read 操作会先看一下缓冲区里的数据是否已经全部被读取了,如果是,就调用底层流,填充缓冲区,再从缓冲区中按要求读取指定的字节。

public synchronized int read() throws IOException {
    if (pos >= count) {
        fill();
        if (pos >= count)
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}
private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
}
  • DataInputStream

DataInputStream 也是 FilterInputStream 的子类,它提供的功能是:可以从底层的流中读取基本数据类型,例如 intchar等等。DataInputStream 是非线程安全的,
你必须自己保证处理线程安全相关的细节。

例如,readBoolean 会读入一个字节,然后根据是否为0,返回 true/false

public final boolean readBoolean() throws IOException {
    int ch = in.read();
    if (ch < 0)
        throw new EOFException();
    return (ch != 0);
}

readShort 会读入两个字节,然后拼接成一个 short 类型的数据。

public final short readShort() throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    if ((ch1 | ch2) < 0)
        throw new EOFException();
    return (short)((ch1 << 8) + (ch2 << 0));
}

int 和 long 依此类推,分别读入4个字节,8个字节,然后进行拼接。

但是,浮点数就不能通过简单的拼接来解决了,而要读入足够的字节数,然后再按照 IEEE 754 的标准进行解释:

public final float readFloat() throws IOException {
    return Float.intBitsToFloat(readInt());
}
  • PushbackInputstream

PushbackInputstream 类也是FilterInputStream的子类,它提供的功能是,可以将已经读入的字节,再放回输入流中,下次读取时,可以读取到这个放回的字节。这在某些情境下是非常有用的。它的实现,就是依靠类似缓冲区的原理。被放回的字节,实际上是放在缓冲区里,读取时,先查看缓冲区里有没有字节,如果有就从这里读取,如果没有,就从底层流里读取。

缓冲区是一个字节数组:

protected byte[] buf;

读取时,优先从这里读取,读不到,再从底层流读取。

public int read() throws IOException {
    ensureOpen();
    if (pos < buf.length) {
        return buf[pos++] & 0xff;
    }
    return super.read();
}
  • PipedInputStream

PipedInputStream 与 PipedOutputStream 配合使用,它们通过 connect 函数相关联。

public void connect(PipedOutputStream src) throws IOException {
    src.connect(this);
}

它们共用一个缓冲区,一个从中读取,一个从中写入。

PipedInputStream内部有一个缓冲区,

protected byte buffer[];

读取时,就从这里读:

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) {
        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;
}

过程比我们想的要复杂,因为这涉及两个线程,需要相互配合,所以,需要检查很多东西,才能最终从缓冲区中读到数据。

PipedOutputStream 类写入时,会调用 PipedInputStream 的receive功能,把数据写入 PipedInputStream 的缓冲区。

我们看一下 PipedOutputStream.write 函数:

public void write(int b)  throws IOException {
    if (sink == null) {
        throw new IOException("Pipe not connected");
    }
    sink.receive(b);
}

可以看出,调用了相关联的管道输入流的 receive 函数。

protected synchronized void receive(int b) throws IOException {
    checkStateForReceive();
    writeSide = Thread.currentThread();
    if (in == out)
        awaitSpace();
    if (in < 0) {
        in = 0;
        out = 0;
    }
    buffer[in++] = (byte)(b & 0xFF);
    if (in >= buffer.length) {
        in = 0;
    }
}

receive 的主要功能,就是把写入的数据放入缓冲区内。

注意注意的是,这两个类相互关联的对象,应该属于两个不同的线程,否则,容易造成死锁。

这个系列的第一部分到此结束,扩展阅读部分的文章非常好,推荐阅读。

扩展阅读

OpenJDK 源码阅读之 Java 字节流输入类的实现

时间: 2024-10-11 23:10:29

OpenJDK 源码阅读之 Java 字节流输入类的实现的相关文章

OpenJDK 源码阅读之 Java 字节流输出类的实现

Java 的输入输出总是给人一种很混乱的感觉,要想把这个问题搞清楚,必须对各种与输入输出相关的类之间的关系有所了解.只有你了解了他们之间的关系,知道设计这个类的目的是什么,才能更从容的使用他们. 这是这个系列的第二篇,描述字节输出类的实现,第一篇见:OpenJDK 源码阅读之 Java 字节流输入类的实现 字节流输出 图1 Java 字节输出类 OutputStream OutputStream是所有字节输出类的超类,这是个抽象类,需要实现其中定义的 write 函数,才能有实用的功能. pub

OpenJDK 源码阅读之 TimSort

概要 这个类在 Oracle 的官方文档里是查不到的,但是确实在 OpenJDK 的源代码里出现了,Arrays 中的 sort 函数用到了这个用于排序的类.它将归并排序(merge sort) 与插入排序(insertion sort) 结合,并进行了一些优化.对于已经部分排序的数组,时间复杂度远低于 O(n log(n)),最好可达 O(n),对于随机排序的数组,时间复杂度为 O(nlog(n)),平均时间复杂度 O(nlog(n)).强烈建议在看此文前观看 Youtube 上的 可视化Ti

OpenJDK 源码阅读之 Arrays

概要 类继承关系 java.lang.Object java.util.Arrays 定义 public class Arrays extends Object 要点 此类主要是提供了一些操作数组的方法,比如排序啊,搜索啊.也提供一个工厂,用于将数组当成一个 List. 实现 quick sort public static void sort(int[] a) { DualPivotQuicksort.sort(a); } sort 使用了 util 中的另一个类中的方法,DualPivotQ

OpenJDK 源码阅读之 LinkedList

概要 类继承关系 java.lang.Object java.util.AbstractCollection<E> java.util.AbstractList<E> java.util.AbstractSequentialList<E> java.util.LinkedList<E> 定义 public class LinkedList<E> extends AbstractSequentialList<E> implements

OpenJDK 源码阅读之 ArrayDeque

概要 类继承关系 java.lang.Object java.util.AbstractCollection<E> java.util.ArrayDeque<E> 定义 public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable 要点 对 Deque 接口的实现 可调整大小 非线程安全 作为栈比 Stac

OpenJDK 源码阅读之 ArrayList

概要 类继承关系 java.lang.Object java.util.AbstractCollection<E> java.util.AbstractList<E> java.util.ArrayList<E> 定义 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serial

转-OpenJDK源码阅读导航跟编译

OpenJDK源码阅读导航 OpenJDK源码阅读导航 博客分类: Virtual Machine HotSpot VM Java OpenJDK openjdk 这是链接帖.主体内容都在各链接中. 怕放草稿箱里过会儿又坑掉了,总之先发出来再说…回头再慢慢补充内容. 先把ItEye网站上的信息聚合起来. 近期提问帖: 阅读openjdk源代码 如何来看OpenJDK源码 如何分析OpenJDK中JVM的实现 一个个回复太麻烦了,合在一块儿写这么一篇. ================ 前言 我的

openjdk源码阅读导航

转自:http://rednaxelafx.iteye.com/blog/1549577 这是链接帖.主体内容都在各链接中. 怕放草稿箱里过会儿又坑掉了,总之先发出来再说…回头再慢慢补充内容. 先把ItEye网站上的信息聚合起来. 近期提问帖: 阅读openjdk源代码 如何来看OpenJDK源码 如何分析OpenJDK中JVM的实现 一个个回复太麻烦了,合在一块儿写这么一篇. ================ 前言 我的VM帖的索引 高级语言虚拟机(HLLVM)群组 新浪微群“JVM源码阅读活

【源码阅读】Java集合 - ArrayList深度源码解读

Java 源码阅读的第一步是Collection框架源码,这也是面试基础中的基础: 针对Collection的源码阅读写一个系列的文章,从ArrayList开始第一篇. [email protected] JDK版本 JDK 1.8.0_110 概述总结 ArrayList底层是通过数组实现的:其中capacity表示底层数组的长度,而ArrayList长度由size表示: ArrayList允许存放null元素,也可以查找null所在的index, 比如indexOf(), lastIndex