1.概述

DataInputStream即数据输入流,DataOutputStream即数据输出流,允许应用程序以与机器无关方式从底层输入流中读取Java基本数据类型。这俩个类分别继承FilterInput/OutputStream,内部采用装饰器模式对Input/OutputStream的功能进行增强。

与机器无关是因为Java基础数据类型结构简单,转化为字节并存储的逻辑也相对简单,其他机器或语言可以轻松复原。另外数据处理流通过UTF-8编码封装了对String类型的读写,UTF-8编码在各机器或者语言都是通用的,因此字符串的处理也不受平台的影响。

2.简单使用

2.1 数据写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception{

// 基于文件输出流,创建数据处理输出流
FileOutputStream fos = new FileOutputStream("you path");
DataOutputStream dos = new DataOutputStream(fos);

// 写入基本数据类型
dos.writeBoolean(true);
dos.writeChar('哈');
dos.writeByte(7);
dos.writeShort(12);
dos.writeInt(25);
dos.writeLong(39);
dos.writeFloat(4.1f);
dos.writeDouble(9.8);
dos.writeUTF("IO流");
}

查看生成的文件内容:
图片

对应结构:
图片

首次写入不同的基本数据类型,生成文件的默认编码集也不同,例如写入boolean、char字母或数字、正整数等类型生成的文件是binary编码,这种编码通过文本编辑器打开文件后,会以上图所示的16进制正常展示。

写入char汉字生成的文件是iso-8859-1编码(不支持中文表达),因此打开的文件内容是乱码。最后Double、Int负数、UTF等类型是unkown-8bit编码,文本编辑器软件仍然无法正常识别并展示,还是会出现奇奇怪怪的字符。

Mac笔记本可以通过file -I 文件路径查看文件的编码集,如果文件的编码集无法被文本编辑器正常显示(我使用的是Sublime Text,也可能是文本编辑器的原因),通过hexdump -v 文件路径命令查看原始16进制内容,得到上图的查看效果。

2.2 数据读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception{

// 基于文件输入流,创建数据处理输入流
FileInputStream fis = new FileInputStream("you path");
DataInputStream dis = new DataInputStream(fis);

// 读取基本数据类型
System.out.println(dos.readBoolean());
System.out.println(dos.readChar());
System.out.println(dos.readByte());
System.out.println(dos.readShort());
System.out.println(dos.readInt());
System.out.println(dos.readLong());
System.out.println(dos.readFloat());
System.out.println(dos.readDouble());
System.out.println(dis.readUTF());
}

注: DataOutputStream是将基本数据类型转化为字节挨个存储,因此DataInputStream在读取数据时,要遵循写入时的顺序,否则会读取失败。

3.DataOutputStream

先从写入开始研究,提供的写入方法整体比较多,但万变不离其宗,都是转化为字节然后调用内部装饰的InputStream类进行字节写入,就简单研究几个有代表性的方法。

3.1 writeBoolean

1
2
3
4
5
6
public final void writeBoolean(boolean v) throws IOException {
// boolean占用1bit,为了方便管理,这里使用1字节存储
out.write(v ? 1 : 0);
// 将写入的字节数量纪录到计数器中
incCount(1);
}

1
2
3
4
5
6
7
8
9
10
private void incCount(int value) {
// 计算递增后的值
int temp = written + value;
// 如果超过int类型最大值,则停止递增
if (temp < 0) {
temp = Integer.MAX_VALUE;
}
// 开始递增
written = temp;
}

3.2 writeInt

1
2
3
4
5
6
7
8
9
10
11
12
13
// int类型占用4字节,因此需要把int值拆成4份字节,然后从高位到低位逐次写入
public final void writeInt(int v) throws IOException {
// 从左开始取二进制的0-8位,位与运算成字节,写入输出流
out.write((v >>> 24) & 0xFF);
// 从左开始取二进制的8-16位,位与运算成字节,写入输出流
out.write((v >>> 16) & 0xFF);
// 从左开始取二进制的16-24位,位与运算成字节,写入输出流
out.write((v >>> 8) & 0xFF);
// 从左开始取二进制的24-32位,位与运算成字节,写入输出流
out.write((v >>> 0) & 0xFF);
// 递增4字节
incCount(4);
}

例如现在写入一个值为330067683的int数据类型:
图片

类似的还有char、byte、sort、long这四个基本类型的写入,都是直接将对应二进制以字节为单位进行拆分,然后调用内部装饰OutputStream类的write单字节逐次写入。long类型稍微特殊一点,是将拆分的8个字节放进数组中批量写入,只和操作系统交互一次。

3.3 writeFloat

1
2
3
4
public final void writeFloat(float v) throws IOException {
// 将浮点类型转化为int,然后调用writeInt方法
writeInt(Float.floatToIntBits(v));
}

1
2
3
4
5
6
7
8
9
10
11
12
public static int floatToIntBits(float value) {

// 调用native方法,将浮点数字转化为正数
int result = floatToRawIntBits(value);

// 如果是无效的数字(NaN),返回0x7fc00000
if ( ((result & FloatConsts.EXP_BIT_MASK) ==
FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0)
result = 0x7fc00000;
return result;
}

3.4 writeUTF

writeUTF方法并没有真正写入,而是将自身作为参数交给静态方法处理:

1
2
3
public final void writeUTF(String str) throws IOException {
writeUTF(str, this);
}

真正处理写入逻辑的静态方法:

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
102
103
// 真正使用UTF-8编码处理字符串的静态方法
static int writeUTF(String str, DataOutput out) throws IOException {

// 获取要写入的字符串的长度
int strlen = str.length();
// 转化为utf编码后的长度计数器
int utflen = 0;
// c用于遍历字符串的循环中赋值,记录当前char类型的数值
// count表示写入缓冲区的字节位置
int c, count = 0;

// 循环字符串的每个字符,判断字符对应的utf编码长度,并递增到utflen变量
for (int i = 0; i < strlen; i++) {

// 获取字符并赋值给当前变量c
c = str.charAt(i);

// 0x007F为ASCII编码表的最大值,代表当前字符在ASCII编码表范围内,可以用1个字节表示,utf长度视为1
if ((c >= 0x0001) && (c <= 0x007F)) {
utflen++;

// 值大于0x07FF就是汉字,需要用3个字节表示,utf长度视为3
} else if (c > 0x07FF) {
utflen += 3;

// 0x007F和0x07FF之间是其他特殊符号,需要用2个字节表示,utf长度视为2
} else {
utflen += 2;
}
}

// 字符串转化为utf编码的长度,不能超过65535,这是因为字符串的长度会采用2个字节在文件中标记
// 65535是2字节的最大值,16进制也就是0xFFFF
if (utflen > 65535)
throw new UTFDataFormatException(
"encoded string too long: " + utflen + " bytes");

// 临时缓冲区,用于非DataOutputStream的装饰输出流
byte[] bytearr = null;

// 如果DataOutputStream内部装饰的输出流还是是一个DataOutputStream
if (out instanceof DataOutputStream) {
// 强转引用类型
DataOutputStream dos = (DataOutputStream)out;
// 使用内部的成员变量建立缓存,长度不够就进行扩容
if(dos.bytearr == null || (dos.bytearr.length < (utflen+2)))
dos.bytearr = new byte[(utflen*2) + 2];
bytearr = dos.bytearr;
} else {
// 内部装饰的输出流不是DataOutputStream,就需要创建临时缓冲区
bytearr = new byte[utflen+2];
}

// 将utf编码长度拆成2个字节,写入缓冲区
bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);

// 开始循环每个字符,并写入缓冲区,遇到需要2个字节表示的字符时循环终止,
// 也就是说如果你写入的都是ASCII编码表的简单字符,这个循环就能完成
int i=0;
for (i=0; i<strlen; i++) {

// 如果字符范围超出ASCII编码表范围,单个字符无法用单字节表示,终止循环
c = str.charAt(i);
if (!((c >= 0x0001) && (c <= 0x007F))) break;
// 将单字符的字节值写入缓冲区
bytearr[count++] = (byte) c;
}

// 如果上个循环中途被终止,那么表示字符串中有需要2或3个字节表示的字符
for (;i < strlen; i++){

// 提取字符
c = str.charAt(i);

// 如果是ASCII编码表范围内的,直接写入单字节
if ((c >= 0x0001) && (c <= 0x007F)) {
bytearr[count++] = (byte) c;

// 字符在(0x07FF - 0xFFFF]前闭后开区间,为占用3个字节的汉字,最大值为1111111111111111(共16位)
} else if (c > 0x07FF) {
// 第一个字节存4位,或运算0xC0,就是在这4位前面拼1110,最终就是1110 xxxx
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
// 第二个字节存6位,或运算0x80,就是在这6位前面拼10,最终就是10xx xxxx
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
// 第三个字节存6位,或运算0x80,就是在这6位前面拼10,最终就是10xx xxxx
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));

// 字符在(0x007F - 0x07FF]前闭后开区间,为占用2个字节的符号,最大值为11111111111(共11位)
} else {
// 第一个字节存前5位,或运算0xC0,就是在这5位前面拼110,最终就是110x xxxx
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
// 第二个字节存后6位,或运算0x80,就是在这6位前面拼10,最终就是10xx xxxx
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
}
}

// 将缓冲数据写入输出流
out.write(bytearr, 0, utflen+2);

// 返回写入的字节数
return utflen + 2;
}

字符串的存储,本质上就是内部char数组的存储,由于char类型占2字节,可能是ASCII表符号,可能是其他特殊符号,也可能是汉字。对于特殊符号、汉字需要拆成多个字节存储,但每个字节只存储最多6位,空余的位做特殊标记,方便DataInputStream读取时能够识别二进制的类型。

4.DataInputStream

4.1 readBoolean

1
2
3
4
5
6
7
8
9
public final boolean readBoolean() throws IOException {
// 读取一个字节
int ch = in.read();
// 如果按照写入的顺序进行读取,这里只可能是0或1,不可能小于0
if (ch < 0)
throw new EOFException();
// 根据是否等于0决定布尔值
return (ch != 0);
}

4.2 readInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final int readInt() throws IOException {
// 逐次读取4个字节
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();

// 如果按照写入的顺序进行读取,这4个字节都不可能小于0
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();

// 将字节按照顺序升位后相加,得到最终结果
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}

4.3 readFloat

1
2
3
4
public final float readFloat() throws IOException {
// 读取一个int值,然后使用java自带的静态方法转化为float
return Float.intBitsToFloat(readInt());
}

4.4 readUTF

writeUTF方法并没有真正读取,而是将自身作为参数交给静态方法处理:

1
2
3
public final String readUTF() throws IOException {
return readUTF(this);
}

真正处理读取逻辑的静态方法:

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
102
103
104
public final static String readUTF(DataInput in) throws IOException {

// 先读取2个字节,确定这次读取字符串的字节长度
int utflen = in.readUnsignedShort();

// 临时数组
byte[] bytearr = null;
char[] chararr = null;

// 如果输入流是DataInputStream,从内部成员变量取出要写入的数据
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;

// 根据utflen读取字符串的全部字节,并放入bytearr数组中
in.readFully(bytearr, 0, utflen);

// 循环字符串的每一个字符,并逐次放入chararr数组中,遇到值超过ASCII编码表最大值的时候终止
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
if (c > 127) break;
count++;
chararr[chararr_count++]=(char)c;
}

// 如果字符串中存在。字符值超过ASCII编码表最大值,继续循环
while (count < utflen) {

// 将字节转化为int
c = (int) bytearr[count] & 0xff;

// 将值右移4位
switch (c >> 4) {

// 如果值c在16进制[0001 - 007F]闭区间范围内,二进制范围就是[00000001 - 01111111]
// 右移4位后范围在[0000 - 0111]闭区间,也就是0~7之间,肯定是单字节
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
count++;
chararr[chararr_count++]=(char)c;
break;

// 如果值c位移4位后是二进制1100或者1101,也就是12或13,那么肯定是2字节特殊符号的第一个字节
case 12: case 13:
// 递增2,也就是读取进度直接跨2个字节
count += 2;
// 如果超出长度,那么肯定数据有问题,抛出异常
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
// count减1就是取符号的第一个字节,校验二进制是不是110开头的
char2 = (int) bytearr[count-1];
// 如果第一个字节不是110开头,那么连续读的2个字节就不是一个特殊符号
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
// 将两个字节合并,复原成char放入chararr数组
chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
(char2 & 0x3F));
break;
// 如果值c位移4位后是二进制1110,也就是14,那么肯定是3字节汉字的第一个字节
case 14:
// 递增3,也就是读取进度直接跨3个字节
count += 3;
// 如果超出长度,那么肯定数据有问题,抛出异常
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
// 读取第一个字节和第二个字节
char2 = (int) bytearr[count-2];
char3 = (int) bytearr[count-1];
// 如果第一个字节不是1110开头,第二个字节不是10开头,那么连续读的3个字节就不是汉字,抛出异常
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count-1));
// 将这3个字节合并成char,并放入chararr数组
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);
}
}
// 将char数组转化为字符串,并返回
return new String(chararr, 0, chararr_count);
}

评论