JavaIO之InputStream类源码剖析

InputStream类源码剖析

一、InputStream类的简介

InputStream是所有字节输入流的公共祖先类,下面有很多的实现类,最常用的包括ByteArrayInputStreamFileInputStreamBufferedInputStream,后面我们会单独对这些类一一讲解。

img

二、InputStream类的字段

1
2
3
4
5
6
// MAX_SKIP_BUFFER_SIZE is used to determine the maximum buffer size to use when skipping.
private static final int MAX_SKIP_BUFFER_SIZE = 2048;
// 默认的Buffer大小
private static final int DEFAULT_BUFFER_SIZE = 8192;
// 分配数组的最大大小,一些虚拟机在数组中保留一些头字。尝试分配更大的数组可能会导致OutOfMemoryError,即请求的数组大小超出VM限制
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

三、InputStream类的方法

字节输入流InputStream中最重要的方法必然就是读取字节数据的read方法了,包含三个重载形式,最终调用的还是原始的read()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 从输入流读取下一个数据字节,以0到255范围内的int返回。如果由于已到达流末尾而没有可用字节,则返回值-1。此方法会阻塞,直到输入数据可用、检测到流结束或引发异常为止。
public abstract int read() throws IOException;

// 从输入流读取一定数量的字节并将它们存储到缓冲区数组b中。实际读取的字节数以整数形式返回。此方法会阻塞,直到输入数据可用、检测到文件结尾或引发异常。
// 如果b的长度为零,则不读取任何字节,返回0;否则,将尝试读取至少一个字节。如果由于流位于文件末尾而没有可用字节,则返回值-1;否则,至少读取一个字节并将其存储到b中。
// 读取的第一个字节存储到元素b[0]中,下一个字节存储到b[1]中,依此类推。读取的字节数最多等于b的长度。令k为实际读取的字节数;这些字节将存储在元素b[0]到b[k-1]中,而元素b[k]到b[b.length-1]不受影响。
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}

// 从输入流读取最多len个字节的数据到字节数组中。尝试读取多达len个字节,但可能会读取更少的字节数。实际读取的字节数以整数形式返回。此方法会阻塞,直到输入数据可用、检测到文件结尾或引发异常。
// 如果len为零,则不读取任何字节并返回0;否则,将尝试读取至少一个字节。如果由于流位于文件末尾而没有可用字节,则返回值-1;否则,至少读取一个字节并将其存储到b中。
// 读取的第一个字节存储到元素b[off]中,下一个字节存储到b[off+1]中,依此类推,读取的字节数最多等于len。令k为实际读取的字节数;这些字节将存储在元素b[off]到b[off+k-1]中,而元素b[off+k]到b[off+len-1]不受影响。在每种情况下,元素b[0]到b[off]和元素b[off+len]到b[b.length-1]不受影响。
// InputStream类的read(b, off, len)方法只是重复调用read()方法。如果第一个此类调用导致IOException,则该异常将从对read(b, off, len)方法的调用中返回。如果对read()的任何后续调用导致IOException,则该异常将被捕获并被视为文件结尾;到目前为止读取的字节存储到b中,并返回异常发生之前读取的字节数。此方法的默认实现会阻塞,直到读取了请求的输入数据量len、检测到文件末尾或引发异常。鼓励子类提供此方法的更有效的实现。
public int read(byte b[], int off, int len) throws IOException {
Objects.checkFromIndexSize(off, len, b.length);
// 如果len为零,则不读取任何字节并返回0
if (len == 0) {
return 0;
}
// 读取第一个字节
int c = read();
// 检测到文件末尾
if (c == -1) {
return -1;
}
// 放入字节数组的第一个偏移位置
b[off] = (byte)c;
// 实际读取的字节数
int i = 1;
try {
// 循环调用read直至读取len个字节、检测到文件末尾或发生异常
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
// 返回实际读取的字节数
return i;
}

第二个我们看一下skip方法,这在一些子类中可能会使用到,用于跳过若干字节的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 跳过并丢弃此输入流中的n字节的数据。由于各种原因,skip方法最终可能会跳过一些较小数量的字节,可能是0。这可能是由多种情况中的任何一种造成的,在跳过n字节之前到达文件末尾只是一种可能性。
// 返回实际跳过的字节数。如果n为负数,则类InputStream的skip方法始终返回0,并且不会跳过任何字节。子类可能会以不同的方式处理负值。
// 此类的skip方法实现创建一个字节数组,然后重复读取它,直到读取n字节或到达流的末尾。鼓励子类提供此方法的更有效的实现。例如,具体实现可能取决于seek的能力。
public long skip(long n) throws IOException {
// 待读取(跳过)的字节数
long remaining = n;
int nr;
// 始终返回0,不会跳过任何字节
if (n <= 0) {
return 0;
}
// 计算字节数组的大小,不能超过MAX_SKIP_BUFFER_SIZE
int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
byte[] skipBuffer = new byte[size];
// 循环读取n字节的数据不做任何处理,表示跳过这些数据
while (remaining > 0) {
// 读取字节输入流的数据,返回实际读取的字节数
nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
// 检测到文件末尾,退出循环
if (nr < 0) {
break;
}
// 计算剩余待读取的字节数
remaining -= nr;
}
// 返回实际跳过的字节数
return n - remaining;
}

我们顺便看一下availableclose方法的含义。

1
2
3
4
5
6
7
8
9
10
// 返回可以从此输入流中无阻塞读取(或跳过)的字节数的估计值,该值可能为0,或者在检测到流结束时为0。读取可能在同一个线程或另一个线程上。单次读取或跳过这么多字节不会阻塞,但可能会读取或跳过更少的字节。
// 请注意,虽然InputStream的某些实现将返回流中的字节总数,但许多实现不会返回。使用此方法的返回值来分配用于保存此流中所有数据的缓冲区永远是不正确的。
// 如果此输入流已通过调用close()方法关闭,则此方法的子类实现可能会选择抛出IOException。
// InputStream的available方法始终返回0,该方法应该由子类重写。
public int available() throws IOException {
return 0;
}

// 关闭此输入流并释放与该流关联的所有系统资源。
public void close() throws IOException {}

下面,我们看一下跟标记相关的一些方法,这些方法不太常用,我们简单过一遍。

1
2
3
4
5
6
7
8
9
10
// 标记此输入流中的当前位置。随后调用reset方法会将此流重新定位到最后标记的位置,以便后续读取重新读取相同的字节。
// readlimit参数告诉该输入流允许在标记位置失效之前读取足够多的字节。
// mark的一般约定是,如果方法markSupported返回true,则流会以某种方式记住调用mark后读取的所有字节,并准备好在调用方法reset时再次提供这些相同的字节。但是,如果在reset之前从流中读取了超过readlimit字节,则流根本不需要记住任何数据。
// 标记关闭的流不应该对流产生任何影响。
public synchronized void mark(int readlimit) {}

// 测试此输入流是否支持mark和reset方法。是否支持mark和reset是特定输入流实例的不变属性。
public boolean markSupported() {
return false;
}

image-20231212151224085

自JDK9依赖,InputStream中增加了一些新的方法来读取和复制InputStream中包含的数据。

readAllBytes:读取InputStream中的所有剩余字节。

readNBytes:从InputStream中读取指定数量的字节到数组中。

transferTo:读取InputStream中的全部字节并写入到指定的OutputStream中 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 从输入流中读取所有剩余字节。此方法会阻塞,直到读取了所有剩余字节并检测到流结束,或者引发异常。
// 当该流到达流末尾时,进一步调用该方法将返回一个空字节数组。
// 请注意,此方法适用于可以方便地将所有字节读入字节数组的简单情况。它不适用于读取具有大量数据的输入流!
// 如果从输入流读取时发生I/O错误,则可能会在读取部分(但不是全部)字节后发生此错误。因此,输入流可能不在流的末尾,并且可能处于不一致的状态。强烈建议在发生I/O错误时立即关闭流。
public byte[] readAllBytes() throws IOException {
return readNBytes(Integer.MAX_VALUE);
}

// 从输入流中读取最多指定数量的字节。此方法会阻塞,直到读取了请求的字节数、检测到流结束或引发异常为止。
// 返回数组的长度等于从流中读取的字节数。如果len为零,则不读取任何字节并返回空字节数组。否则,最多从流中读取len字节。如果遇到流末尾,则可能会读取少于len个字节。
// 当该流到达流末尾时,进一步调用该方法将返回一个空字节数组。
// 请注意,此方法适用于可以方便地将指定数量的字节读入字节数组的简单情况。此方法分配的内存总量与从len限制的流中读取的字节数成正比。因此,只要有足够的内存可用,就可以使用非常大的len值安全地调用该方法。
public byte[] readNBytes(int len) throws IOException {
// 参数检查
if (len < 0) {
throw new IllegalArgumentException("len < 0");
}

List<byte[]> bufs = null;
byte[] result = null;
// 已读取的字节数
int total = 0;
// 待读取的字节数
int remaining = len;
int n;
do {
// 创建单次读取时所需的字节数组
byte[] buf = new byte[Math.min(remaining, DEFAULT_BUFFER_SIZE)];
// 本次读取字节数量
int nread = 0;
// read to EOF which may read more or less than buffer size
while ((n = read(buf, nread,
Math.min(buf.length - nread, remaining))) > 0) {
// 更新本次读取字节数量
nread += n;
// 更新剩余待读取的字节数
remaining -= n;
}
if (nread > 0) {
// 读取的字节数超过最大字节数组长度限制MAX_BUFFER_SIZE
if (MAX_BUFFER_SIZE - total < nread) {
throw new OutOfMemoryError("Required array size too large");
}
// 更新已读取的字节数
total += nread;
// result初始化
if (result == null) {
result = buf;
} else {
// bufs初始化
if (bufs == null) {
bufs = new ArrayList<>();
bufs.add(result);
}
// 添加分块的字节数组
bufs.add(buf);
}
}
// if the last call to read returned -1 or the number of bytes
// requested have been read then break
} while (n >= 0 && remaining > 0);
// 读取字节数len小于DEFAULT_BUFFER_SIZE时,不需要采用类似于分片传输的方案,result数组即可
if (bufs == null) {
if (result == null) {
return new byte[0];
}
return result.length == total ?
result : Arrays.copyOf(result, total);
}
// 类似于分片传输,需要组装字节数组形成一个大的总字节数组
result = new byte[total];
int offset = 0;
remaining = total;
for (byte[] b : bufs) {
int count = Math.min(b.length, remaining);
System.arraycopy(b, 0, result, offset, count);
offset += count;
remaining -= count;
}

return result;
}

// 从此输入流中读取所有字节,并按照读取顺序将字节写入给定的输出流。返回时,该输入流将位于流的末尾。此方法不会关闭任何一个流。
public long transferTo(OutputStream out) throws IOException {
// 字节输出流不能为空
Objects.requireNonNull(out, "out");
// 转移的字节数
long transferred = 0;
// 转移时读写所需的字节数组
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;
// 循环读取直至流末尾
while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
// 写入输出流
out.write(buffer, 0, read);
// 更新转移的字节数
transferred += read;
}
return transferred;
}