ByteArrayInputStream和DataInputStream的源码分析和使用方法详细分析
ByteArrayInputStream的源码——零拷贝(Zero-Copy)的一种字节流

在传统的磁盘 I/O(比如FileInputStream.class、BufferedInputStream.class...等) 中,使用者都需要将磁盘的数据先复制到内存中来使用而无法实现零拷贝(Zero-Copy),而ByteArrayInputStream .class可以从内存中直接读取数据,因此,ByteArrayInputStream .class这个类正是为了解决内存数据读取而存在的,ByteArrayInputStream 的核心价值在于零拷贝(Zero-Copy),它允许将内存中的byte[]字节数组当作输入流来读取,是处理内存数据的常用工具,如下所示:

ByteArrayInputStream .class的UML关系图,如下所示:

ByteArrayInputStream .class的源码如下:

package java.io; public class ByteArrayInputStream extends InputStream { //byte[] buf变量直接指向了内存中的byte[]字节数组,不会向BufferedInputStream源码中的那样,先将数据复制到自己内部的byte[]字节数组上,再进行数据处理 protected byte buf[]; //准备从当前内存中的byte[]数组(直接引用)读取字节的索引位置,取值范围为[0,count) protected int pos; //标记某个索引位置,0<=mark<=pos //该变量只会在 构造函数和mark()函数中赋值 protected int mark = 0; //byte[] buf变量指向的内存中的byte[]字节数组的长度 protected int count; //构造函数 public ByteArrayInputStream(byte buf[]) { this.buf = buf;//byte[] buf变量直接指向了内存中的byte[]字节数组 this.pos = 0; this.count = buf.length; } //构造函数,准备从byte buf[]字节数组中读取字节的索引位置从int offset索引位置开始 public ByteArrayInputStream(byte buf[], int offset, int length) { this.buf = buf;//byte[] buf变量直接指向了内存中的byte[]字节数组 this.pos = offset; //读取数组的右边界索引位置 < 内存中的byte[]字节数组的长度 this.count = Math.min(offset + length, buf.length); this.mark = offset;//读取数组时候的左边界索引位置>=offset } //线程同步的函数,从内存中的byte[]字节数组读取1个字节 public synchronized int read() { return (pos < count) ? (buf[pos++] & 0xff) : -1; } //线程同步的函数,从内存中的byte[]字节数组读取len个字节到指定的byte[]数组b中,这len个字节被放到byte[]数组b的[off,off+len)索引位置。 public synchronized int read(byte b[], int off, int len) { if (b == null) { //如果指定的内存中的byte[]数组b为null,抛出一个NullPointerException throw new NullPointerException(); //范围检测,off和len必须是非负数,b.length - off是内存中的byte[]数组还可以放的字节(ASCII码值)的数量 } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } //pos>=count时,返回-1,表示内存中的byte[]数组已经读取完毕 if (pos >= count) { return -1; } //int avail表示本次可以从内存中的byte[]数组中读取到的字节数量 int avail = count - pos; if (len > avail) { len = avail; } if (len <= 0) { return 0; } //从内存中的byte[]字节数组读取[pos,pos+len)索引位置的字节到指定的byte[]数组b中,这len个字节被放到byte[]数组b的[off,off+len)索引位置。 System.arraycopy(buf, pos, b, off, len); pos += len;//跟新int pos变量的值 return len; } //线程同步的函数 public synchronized long skip(long n) { //尝试更新右指针int pos的值,对于不同的n值,有以下2种可能: //a、如果n<count - pos并且n>0时,才更新int pos= pos+n //b、当n<0,或者n>=count - pos时,更新int pos = count long k = count - pos; if (n < k) { k = n < 0 ? 0 : n; } pos += k; return k; } //线程同步的函数,返回内存中的byte[]字节数组还可以读取的字节数量 public synchronized int available() { return count - pos; } public boolean markSupported() { return true; } //更新int mark变量为pos的值 public void mark(int readAheadLimit) { mark = pos; } //线程同步的函数,重置int pos变量的值为mark的值 public synchronized void reset() { pos = mark; } public void close() throws IOException { } }
1.1、ByteArrayInputStream.class从内存中读取数据的过程

ByteArrayInputStream.class本质就是通过直接引用字节数组来实现的零拷贝,并通过双指针(int mark是左指针和int pos是右指针)来实现的read()函数、skip()函数、reset()函数...等,ByteArrayInputStream.class从内存中读取数据的过程如下所示:

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; public class ByteArrayInputStreamTest { private static final int LEN = 5; // 对应英文字母"abcdefghijklmnopqrstuvwxyz"(ASCII编码) private static final byte[] ArrayLetters = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A }; public static void main(String[] args) { //step1:创建ByteArrayInputStream字节流,引用的是内存中的ArrayLetters数组 ByteArrayInputStream bais = new ByteArrayInputStream(ArrayLetters); //step2: 从ByteArrayInputStream指向的内存数组中读取5个字节 for (int i=0; i<LEN; i++) { // 若能继续读取下一个字节,则读取下一个字节 if (bais.available() > 0) { // 读取ByteArrayInputStream指向的内存数组中的下一个字节 int tmp = bais.read(); System.out.printf("%d : 0x%s\n", i, Integer.toHexString(tmp)); } } //step3:标记ByteArrayInputStream中下一个被读取的索引位置(即--标记"0x66"),因为因为前面已经读取了5个字节,所以下一个被读取的位置是第6个字节 // ①、 ByteArrayInputStream中的mark(0)函数中的“参数0”是没有实际意义的。 // ②、 mark()与reset()是配套的,reset()会将字节流中右指针int pos指向的索引重置为mark()函数中左指针int mark指向的索引 bais.mark(0); //step4: 跳过5个字节。跳过5个字节后,下一个被读取的索引位置应该是ByteArrayInputStream指向的内存数组中“0x6B”。 bais.skip(5); //step5:从ByteArrayInputStream指向的内存数组中读取5个字节到byte[] buf数组的[0,5)索引位置。即读取“0x6B, 0x6C, 0x6D, 0x6E, 0x6F” byte[] buf = new byte[LEN]; bais.read(buf, 0, LEN); String str1 = new String(buf); System.out.printf("str1=%s\n", str1); //step6、重置ByteArrayInputStream中的右指针int pos为mark()函数中左指针int mark指向的索引,即0x66。 bais.reset(); //step7、从“重置后的ByteArrayInputStream”中读取5个字节到buf中。即读取“0x66, 0x67, 0x68, 0x69, 0x6A” bais.read(buf, 0, LEN); String str2 = new String(buf); System.out.printf("str2=%s\n", str2); } }

上述代码的执行过程为:
①、step1:创建ByteArrayInputStream对象,引用的是内存中的byte[] ArrayLetters数组,如下所示(int mark = 0,int pos = 0):

②、step2:从内存中的byte[] ArrayLetters数组中依次读取5个字节,读取完成后,int pos=5,int mark=0,如下所示(int mark = 0,int pos = 5):

③、step3:标记ByteArrayInputStream中下一个被读取的索引位置(即--标记"0x66"),因为因为前面已经读取了5个字节,所以下一个被读取的位置是第6个字节,如下所示(int mark = 5,int pos = 5):

mark(0)函数需要注意以下2点
a、 ByteArrayInputStream中的mark(0)函数中的“入参0”是没有实际意义的;
b、 mark()函数与reset()函数是配套的,reset()函数会将ByteArrayInputStream中右指针int pos指向的索引重置为mark()函数中左指针int mark指向的索引。
④、step4: 跳过5个字节。跳过5个字节后,下一个被读取的索引位置应该是ByteArrayInputStream指向的内存数组中"0x6B",如下所示(int mark = 5,int pos = 10):

⑤、step5: 从ByteArrayInputStream指向的内存数组中读取5个字节到byte[] buf数组的[0,5)索引位置。即读取“0x6B, 0x6C, 0x6D, 0x6E, 0x6F”,如下所示(int mark = 5,int pos = 15):

⑥、step6: 重置ByteArrayInputStream中的右指针int pos为step3中调用mark()函数时左指针int mark指向的索引位置,即0x66,如下所示(int mark = 5,int pos = 5):

⑦、step7: 从“重置后的ByteArrayInputStream”中读取5个字节到buf中。即读取“0x66, 0x67, 0x68, 0x69, 0x6A”,如下所示(int mark = 5,int pos = 10):

二、DataInputStream的源码——Java原生8种数据类型解析和UTF-8 解码算法实现的一种装饰器流

DataInputStream.class是 Java I/O 体系中实现了 DataInput.interface 接口的核心类,自 JDK 1.0 起就为从机器无关的二进制数据中读取基本 Java 数据类型提供了标准实现,同时也继承了FilterInputStream.class,因此,也可以用来装饰其它输入流 。其中,DataInputStream.class::readUTF()函数是UTF-8 解码算法的精妙实现,这是 Java 序列化和网络协议的基石。
DataInputStream.class的UML关系图,如下所示:

public class DataInputStream extends FilterInputStream implements DataInput { //构造函数,需要传入一个被装饰的输入流 public DataInputStream(InputStream in) { super(in); } private byte bytearr[] = new byte[80]; private char chararr[] = new char[80]; //实际调用的是InputStream.class::read()函数 public final int read(byte b[]) throws IOException { return in.read(b, 0, b.length); } //实际调用的是InputStream.class::read()函数 public final int read(byte b[], int off, int len) throws IOException { return in.read(b, off, len); } public final void readFully(byte b[]) throws IOException { readFully(b, 0, b.length); } public final void readFully(byte b[], int off, int len) throws IOException { if (len < 0) throw new IndexOutOfBoundsException(); int n = 0; while (n < len) { int count = in.read(b, off + n, len - n); if (count < 0) throw new EOFException(); n += count; } } //当前这个DataInputStream对象可以从它装饰的输入流中(可以理解为一个大型的字节数组)中跳过n个字节(n<被装饰的输入流中可用的字节数量)再进行后续操作(比如通过read()函数读取等) public final int skipBytes(int n) throws IOException { int total = 0;//累计跳过的字节总数量 int cur = 0;//本次通过InputStream.class::skip()函数跳过的字节总数量