Java-io之DataInputStream

DataInputStream 是数据输入流。它继承于FilterInputStream。
DataInputStream 是用来装饰其它输入流,它“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。应用程序可以使用DataOutputStream(数据输出流)写入由DataInputStream(数据输入流)读取的数据。

DataInputStream源码的难点主要就是两个方法

  • 阻塞读readfully()
  • 阻塞跳过skipBytes()
  • 基于Modified UTF-8规范的字符串读取readUTF()

非阻塞读read()与阻塞读readfully()

1
2
3
4
5
6
//该方法是非阻塞方法
//read(byte[])以及read(byte[], int, int)都是尝试读取length个字节
//如果流中字节数不够 也立马返回 最终读取字节数 <= length
public final int read(byte b[]) throws IOException {
return in.read(b, 0, b.length);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//该方法是阻塞方法
//读不到len个字符则一直循环读取
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;
}
}

阻塞跳过skipBytes()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//https://codeday.me/bug/20181231/480189.html
//该方法是阻塞方法
//在“达到文件/数据末尾”这种情况之外 skipBytes依旧可能出现跳过的字节数小于n的情况
//网络IO存在延迟 数据没有全部进入流 可能在发送方缓冲区中
//while循环会一直执行 直到total达到n的要求或者in.skip()失败
public final int skipBytes(int n) throws IOException {

//调用skipBytes以来 总共可以跳过的字节数
int total = 0;
//当前尝试in.skip所可以跳过的字节数
int cur = 0;

//当total没满足n要求 或者 in.skip失败前 都阻塞
while ((total<n) && ((cur = (int) in.skip(n-total)) > 0)) {
total += cur;
}
return total;
}

readUTF()中的Modified UTF-8字符串读取

接下来我们再来看readUTF()源码

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
public final static String readUTF(DataInput in) throws IOException {
// 注意:UTF-8输入流的前2个字节是数据的长度
int utflen = in.readUnsignedShort();
byte[] bytearr = null;
char[] chararr = null;
//如果in是数据流 字节数组与字符数组就用in的
//如果in不是数据流 new字节数组与字符数组
if (in instanceof DataInputStream) {
DataInputStream dis = (DataInputStream)in;
if (dis.bytearr.length < utflen){
dis.bytearr = new byte[utflen*2];
dis.chararr = new char[utflen*2];
}
chararr = dis.chararr;
bytearr = dis.bytearr;
} else {
bytearr = new byte[utflen];
chararr = new char[utflen];
}

int c, char2, char3;
int count = 0;
int chararr_count=0;

//以阻塞读方式读取utllen长度个字节
in.readFully(bytearr, 0, utflen);

//先将单字节字符转换完成
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
//c > 127 代表不再是单字节字符
if (c > 127) break;
count++;
chararr[chararr_count++]=(char)c;
}

//转换多字节字符(也包括后续出现的单字节字符)
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
/* 0xxxxxxx*/
//对应单字节字符
count++;
chararr[chararr_count++]=(char)c;
break;
case 12: case 13:
/* 110x xxxx 10xx xxxx*/
//对应双字节字符
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
(char2 & 0x3F));
break;
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx */
//对应三字节字符
count += 3;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-2];
char3 = (int) bytearr[count-1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count-1));
chararr[chararr_count++]=(char)(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
default:
/* 10xx xxxx, 1111 xxxx */
//对应错误字符
throw new UTFDataFormatException(
"malformed input around byte " + count);
}
}
// The number of chars produced may be less than utflen
return new String(chararr, 0, chararr_count);
}

说明:

(01) readUTF()的作用,是从输入流中读取Modified UTF-8 (MUTF-8)编码的数据,并以String字符串的形式返回。
(02) 知道了readUTF()的作用之后,下面开始介绍readUTF()的流程:

第1步,读取出输入流中的UTF-8数据的长度

1
int utflen = in.readUnsignedShort();

UTF-8数据的长度包含在它的前两个字节当中;我们通过readUnsignedShort()读取出前两个字节对应的正整数就是UTF-8数据的长度。

第2步,创建2个数组:字节数组bytearr 和 字符数组chararr

1
2
3
4
5
6
7
8
9
10
11
12
if (in instanceof DataInputStream) {
DataInputStream dis = (DataInputStream)in;
if (dis.bytearr.length < utflen){
dis.bytearr = new byte[utflen*2];
dis.chararr = new char[utflen*2];
}
chararr = dis.chararr;
bytearr = dis.bytearr;
} else {
bytearr = new byte[utflen];
chararr = new char[utflen];
}

首先,判断该输入流本身是不是DataInputStream,即数据输入流;若是的话,
则,设置字节数组bytearr = “数据输入流”的成员bytearr
设置字符数组chararr = “数据输入流”的成员chararr
否则的话,新建数组bytearr和chararr。

第3步,将UTF-8数据全部读取到“字节数组bytearr”中

1
in.readFully(bytearr, 0, utflen);

读取全部数据存储到字节数组中(这里博主不理解:对于超过0x7f这样的数据 如何存储进字节数组中呢?)

第4步,先对UTF-8中开头的单字节数据进行转换

1
2
3
4
5
6
7
8
9
while (count < utflen) {
// 将每个字节转换成int值
c = (int) bytearr[count] & 0xff;
// UTF-8的单字节数据的值都不会超过127;所以,超过127,则退出。
if (c > 127) break;
count++;
// 将c保存到“字符数组chararr”中
chararr[chararr_count++]=(char)c;
}

UTF-8的数据是变长的,可以是1-4个字节;在readUTF()中,我们最终是将全部的UTF-8数据保存到“字符数组(而不是字节数组)”中,再将其转换为String字符串。

由于UTF-8的单字节和ASCII相同,所以这里就将它们进行预处理,直接保存到“字符数组chararr”中。对于其它的UTF-8数据,则在后面进行处理。

注意:这里处理的是开头的单字节字符,躲在多字节字符后面的单字节字符在下面的步骤中处理,之所以这样处理,我猜想是一般数据流中以单字节数据为主,下面统一处理步骤中的”>>4”操作有性能代价,于是将数据流开头的单字节先处理掉,提高readUTF()性能。

第5步,对“第4步处理”之后的数据,接着进行处理

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
// 处理完输入流中单字节的符号之后,接下来我们继续处理。
// 程序执行到这里 下一个要处理的字节一定是 > 127的
while (count < utflen) {
//bytearr[count]变为四字节int: 0000 0000 xxxx xxxx
c = (int) bytearr[count] & 0xff;

//循环右移四位变为: 0000 0000 0000 xxxx
//相当于保留下0000 0000 xxxx xxxx的第三个字节用于下面使用
//至于为何将字节的第三个字节作为如何处理的判断依据呢 见下面的说明
switch (c >> 4) {
// 若 UTF-8 是单字节,即 bytearr[count] 对应是 “0xxxxxxx” 形式;
// 则 bytearr[count] 对应的int类型的c的取值范围是 0-7。
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
/* 0xxxxxxx*/
count++;
chararr[chararr_count++]=(char)c;
break;
// 若 UTF-8 是双字节,即 bytearr[count] 对应是 “110xxxxx 10xxxxxx” 形式中的第一个,即“110xxxxx”
// 则 bytearr[count] 对应的int类型的c的取值范围是 12-13。
case 12: case 13:
/* 110x xxxx 10xx xxxx*/
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
(char2 & 0x3F));
break;
// 若 UTF-8 是三字节,即 bytearr[count] 对应是 “1110xxxx 10xxxxxx 10xxxxxx” 形式中的第一个,即“1110xxxx”
// 则 bytearr[count] 对应的int类型的c的取值是14 。
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx */
count += 3;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-2];
char3 = (int) bytearr[count-1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count-1));
chararr[chararr_count++]=(char)(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;


// 此处是非法字节的处理
default:
/* 10xx xxxx, 1111 xxxx */
throw new UTFDataFormatException(
"malformed input around byte " + count);
}
}

(a) 我们将下面的两条语句一起进行说明

c = (int) bytearr[count] & 0xff;
switch (c >> 4) { … }
首先,我们必须要理解 为什么要这么做(执行上面2条语句)呢?
原因很简单,这么做的目的就是为了区分UTF-8数据是几位的;因为UTF-8的数据是1~4字节不等。

至于为什么这样区分呢 我们先看一下百度百科关于UTF-8的解析:

Unicode/UCS-4 bit数 UTF-8 byte数
0000 ~ 007F 0~7 0XXX XXXX 1
0080 ~ 07FF 8~11 110X XXXX 10XX XXXX 2
0800 ~ FFFF 12~16 1110XXXX 10XX XXXX 10XX XXXX 3
10000 ~ 1FFFFF 17~21 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX 4

如果对上面表格不理解的话 可以看一下UTF-8参考阅读-知乎:

简单来说:
  • Unicode 是「字符集」
  • UTF-8 是「编码规则」
其中:
  • 字符集:为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point)
  • 编码规则:将「码位」转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)

广义的 Unicode 是一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等编码……

Unicode 字符集为每一个字符分配一个码位,例如「知」的码位是 30693,记作 U+77E5(30693 的十六进制为 0x77E5)。

UTF-8 顾名思义,是一套以 8 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 4 个字节:
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
根据上表中的编码规则,之前的「知」字的码位 U+77E5 属于第三行的范围:
       7    7    E    5
0111 0111 1110 0101 二进制的 77E5
—————————————-
0111 011111 100101 二进制的 77E5
1110XXXX 10XXXXXX 10XXXXXX 模版(上表第三行)
11100111 10011111 10100101 代入模版
E 7 9 F A 5
这就是将 U+77E5 按照 UTF-8 编码为字节序列 E79FA5 的过程。反之亦然。

现在我们知道了 原来表格第三列中的”0XXXXXXX”中的”0”, 以及“110XXXXX 10XXXXXX”中的”110”,”10”是UTF-8编码的模板壳子。

那我们可以想象一个数据流的可能样子(实际”|”是不存在的):

1
2
0XXXXXXX | 0XXXXXXX | 110XXXXX 10XXXXXX | 0XXXXXXX | 1110XXXX 10XXXXXX 10XXXXXX
单字节 | 单字节 | 双字节 | 单字节 | 三字节

我们发现每种字符(单/双/三/四字节)的头(前四位)都不一样:

  • 单字节 <---> 0XXX <---> 0~7
  • 双字节 <---> 110X <---> 12~13
  • 三字节 <---> 1110 <---> 14
  • 四字节 <---> 1111 <---> 15

因此我们就找到了区分字符种类的办法 <--> 字节的首四位

接下来以二字节字符说明一下相应源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case 12: case 13:
/* 110x xxxx 10xx xxxx*/
//字节头是"110x"的代表该字符是二字节字符
//于是直接读取两个字节 count += 2;
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
//读取的第二个字节要符合模版要求: "10xx xxxx*"
//0xC0为"1100 0000" char2 & 0xC0就是只保留char2高两位查看是否是0x80(1000 0000)
char2 = (int) bytearr[count-1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
//全部校验完毕后将其转为char
//c & 0x1F 是将 110x xxxx 中的"x"表示的数据取出来
//char2 & 0x3F是将 10xx xxxx 中的"x"表示的数据取出来
//因为前面我们讲过UTF-8有个模板壳子 现在将真实的数据取出来
chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
(char2 & 0x3F));
break;

第6步,将字符数组转换成String字符串,并返回

return new String(chararr, 0, chararr_count);

关于UTF-8与Modified UTF-8

细心的老哥肯定发现了readUTF()源码将”1111 xxxx”四字节字符作为非法字符抛异常了。

oracle官网-Modified UTF-8 (MUTF-8):

单字节 Bit Values
Byte 1 0 bits 6 - 0
双字节 Bit Values
Byte 1 110 bits 10 - 6
Byte 2 10 bits 5 - 0
三字节 Bit Values
Byte 1 1110 bits 15-12
Byte 2 10 bits 11-6
Byte 3 10 bits 5-0

其实readUTF()针对的编码是Modified UTF-8而不是UTF-8, 所以”1111 xxxx”四字节字符也是非法字符。

参考阅读

Donate here.