关于 java.io.DataInputStream 和 java.io.DataOutputStream 的部分笔记,这两个类共同实现对数据类型为Java原生数据类型的数据的字节输入、输出流操作。本文演示代码段的执行环境基于JDK版本1.7。
概述
DataInputStream和DataOutputStream这两个类实现了Java原生数据类型的写入和读取操作。DataInputStream以一种不依赖于特定运行平台的方式从底层输入流中将Java类型的数据读出进行计算处理。但是DataInputStream不保证多线程环境下的安全性,需要开发人员自己来保证多线程环境下可以正确无误的使用该类中的方法。
DataOutputStream则用来将Java的原生数据类型数据写入到底层的输出流中。
继承关系
1 | // DataInputStream |
实现接口
类名 | 实现接口 |
---|---|
DataInputStream | Closeable, DataInput, AutoCloseable |
DataOutputStream | Closeable, DataOutput, Flushable, AutoCloseable |
DataInputStream
部分方法
public final void readFully(byte b[], int off, int len)
1 | public final void readFully(byte b[], int off, int len) throws IOException { |
读取字节流数据并保存到入参缓存数组b中,直到把入参b填充完全时才会结束。第2 ~ 3行代码用来做参数校验,如果长度小于0,那么会返回一个异常信息给方法调用方,防止越界。第5 ~ 10行代码执行具体的字节数据读取,第6行返回当前读取完成后实际成功读取的字节数,如果实际读取的字节数小于0,那么返回一个文件结束异常,此时b中包含实际读取的内容数据。第9行用于记录累计读取的字节数,如果累计读取字节数达到了入参读取长度,那么结束循环读取操作,b中包含长度为len的实际读取的内容。由于这个方法是个阻塞方法,因此如果因为一些原因导致当前底层输入流在没有到达流结束位置但暂时无法提供新的数据时,这个方法会一直等待,直到新的数据到达被读取保存到b中或者一个IOException、EOFException异常被返回。
在浏览代码时发现,DataInputStream含有继承自父类InputStream的方法read(byte b[], int off, int len)和read(byte b[])。这两个方法和对应的readFully方法的入参完全一致,最终实现的效果也是一样的,都是从底层输入流中读取字节内容并保存到入参字节数组b中。但是这两个方法有一些区别:
- readFully在无异常执行结束后,数组b中实际填充的字节长度等于入参长度值(默认为数组b的长度),而read方法执行后数组b实际填充的字节长度小于等于入参长度值;
- readFully方法通过抛出EOFException标识已经到达结束位置,而read方法则通过返回-1(int类型)来标识结束;
- readFully方法如果入参缓存数组长度大于底层输入流的完整长度,那么最后会返回一个EOFException异常来结束方法调用,而read方法会正常结束调用;
- 通过使用readFully可以保证读取到长度明确的流内容,这可以保证读取内容的完整性。而read方法则无法保证实际读取的内容长度,需要方法调用方执行另外的判断操作来保证。
public final int skipBytes(int n)
1 | public final int skipBytes(int n) throws IOException { |
跳过底层输入流中指定长度的字节内容,并将下次开始读取的位置从当前位置向后偏移n个单位长度。需要注意的是,实际调用时,实际跳过的长度可能小于n,因为当输入流中剩余未读的数据长度可能小于入参跳过长度n。
public final short readShort()
1 | public final short readShort() throws IOException { |
读出一个有符号short型数据。借鉴参考的数学公式是:
(short)((a << 8) | (b & 0xff))
在依次读出两个字节的数据后,将第一次读出的数据(即左高八位)向左移动8个进制位,至此完成了左高八位数据的处理,然后将第二次读出的数据(即右低八位)与11111111(共32位,左高24位均为0)按位求与。这样执行后,第二次读出的数据将只保留低八位的二进制位,也就是右低八位的数据。最后将左移后的左高八位数据和按位求与后的右低八位数据按位求或(这两个数据都是以32位形式存在),得到最终的32位比特数值,最后的强制转换执行高位截取,只保留右低十六位的比特位,即最终需要返回的short型数据。
public final int readUnsignedShort()
1 | public final int readUnsignedShort() throws IOException { |
读出一个无符号short型数据。借鉴参考的数学公式是:
(((a & 0xff) << 8) | (b & 0xff))
和readShort方法不同的是,readUnsignedShort将第一次读出的数据也和0xff做了按位求与操作。因为在java中int型数据不存在有符号无符号这一说法,所以这么做以后就把第一次读出的数据的高24位全部置为0,消除了符号位可能带来的影响。紧接着将其左移8位完成左高八位数据的处理。然后将第二次读出的数据执行和readShort方法一样的操作,最后将两个数据按位求或,即得到最终需要返回的无符号的short型数据。
public final long readLong()
1 | public final long readLong() throws IOException { |
读出一个无符号short型数据。借鉴参考的数学公式是:
(((long)(a & 0xff) << 56) | ((long)(b & 0xff) << 48) | ((long)(c & 0xff) << 40) | ((long)(d & 0xff) << 32) |
((long)(e & 0xff) << 24) | ((long)(f & 0xff) << 16) | ((long)(g & 0xff) << 8) | ((long)(h & 0xff)))
读出一个long类型数据并返回。第2行代码首先一次读出8个长度的byte数组内容,然后由第3 ~ 10行代码完成数据类型的转换。在代码中,都有一个操作,那就是readBuffer[n] & 255。这是因为readBuffer数组中存储的是byte类型的数据,而底层依赖调用的read方法返回的int类型数据,也就是说,在从底层输入流中读出一个int类型的数据后,会转成byte存入readBuffer数组中。如果从底层输入流中读出的是一个大于127的int值,这个值在完成byte转换后就变成了一个负数而不是转换前的大于127的值,所以为了保持一致性,就将byte类型的数据首先与255(也就是0XFF)按位相与,这样就可以正确的将一个byte类型的数据转成int类型。由于int和long类型数据都不存在有无符号这一说法,所以int可以正确无误的转成long型。最后按照从左到右的顺序依次构建long型数据的二进制表示并返回最终转换后的结果。
public final static String readUTF(DataInput in)
1 | public final static String readUTF(DataInput in) throws IOException { |
读出一段指定长度的UTF-8格式的字符串并返回。第2行代码从输入流中读出一个无符号short型数值,这个数值表示接下来需要从底层输入流中读取的数据量。第22行代码执行后,就会从底层输入流中读取确定长度的数据。第24 ~ 29行代码把数值小于128的数据转成了基础的ASCII码字符。第31 ~ 72行数据用来把数据转换成Unicode格式的字符,采用的是修订版的UTF-8标准。每次遍历一个数组元素,将其转成int型数值后右移4位判断转换关系:第34 ~ 38行代码将码值在U+0000 - U+007F之间的字符转换成了对应的UTF格式,这部分数据只需要用一个字节即可存储。第39 ~ 51行用来处理码值在U+0080 - U+07FF之间的字符,第46行的判断用来检查第二个字节是否满足10XXXXXX的格式。第52 ~ 66行代码用来处理码值在U+0800 - U+FFFF之间的字符。如果不满足上述三种情况,那么认为读取的数据是异常的并抛出异常。在最后将转换后的char字符数组转成一个完整的String字符串并返回。
DataOutputStream
部分方法
private void incCount(int value)
1 | private void incCount(int value) { |
记录更新已经写入的字节数。因为written的类型是int,所以如果发生了溢出以后,那么就一律标定为int类型的最大值。
public synchronized void write(int b)
1 | public synchronized void write(int b) throws IOException { |
继承自OutputStream的write方法,如果写入完成后无异常,那么就更新已经写入的字节数信息。
基本类型写入方法
1 | //写入一个boolean值,1 - true ,0 - false |
public final void writeBytes(String s)
1 | public final void writeBytes(String s) throws IOException { |
写入一个String到底层输出流中,需要注意的是,这个方法仅适用于单字节字符,即构成String的每个字符都是单字节的。如果是写入类似于Unicode这样的多字节字符,那么由于writeBytes()仅写入了低八位数据,所以会导致写入和读取的内容不一致的问题。除此之外,在DataInputStream中,并没有对应的读取方法来获取一个写入的String字符串,所以该方法需要慎用。
public final void writeChars(String s)
1 | public final void writeChars(String s) throws IOException { |
依次将入参String的每个字符写入到底层输出流中。如果执行过程无异常抛出,那么更新当前写入的字节数,需要注意的是,本次方法调用实际写入的字节数为入参String长度的两倍。
static int writeUTF(String str, DataOutput out)
1 | static int writeUTF(String str, DataOutput out) throws IOException { |
将一段String字符串以修订版的UTF-8格式写入输出流中,并返回实际写入的字节长度。第7 ~ 16行代码用于统计容纳入参String字符串需要的字节长度。第22 ~ 30行代码用于初始化bytearr数组,这个数组用来存储最后需要写入到输出流中的内容。如果bytearr数组长度不容容纳入参String字符串,那么就需要执行扩容操作,将bytearr数组长度扩展为容纳入参String字符串需要的字节长度的两倍。第32、33行用来存储实际写入的字节长度,长度最大是65535,即$ 2^{16} $。第36 ~ 40行向bytearr写入单字节长度的字符串,在遇到第一个非单字节长度字符时结束。第42 ~ 55行用来向bytearr写入变长字节长度的字符。其中,第44 ~ 46行向bytearr写入单字节长度的字符,第47 ~ 51行向bytearr写入三个字节长度的字符,通过按位与、或操作将单个字符转换成 “1110xxxx 10yyyyyy 10zzzzzz”的格式,有效数据位都存储在了x,y,z占据的二进制位中。第52 ~ 53行向bytearr写入两个字节长度的字符,将当前字符转换成“110yyyyy 10zzzzzz”的格式,有效数据位都存储在了x,y占据的二进制位中。第56行代码用于将转换后的bytearr中存储的内容写入到输出流中。第57行返回实际写入的字节数,因为utflen代表的是存储入参String需要的字节数,而在写入过程中还向输出流中写入了两个字节长度的实际写入内容长度值,所以需要将utflen的值加2作为最终返回值。
public final int size()
1 | public final int size() { |
返回当前已经写入的字节数。
涉及基础知识点
byte转int的标准操作:
1
2byte b = (byte)255;
int i = b & 255;byte的有符号数范围是[-128, 127],无符号数范围是[0, 255]。所以第1行代码中,将255强转成byte类型后,就由32比特的00000000 00000000 00000000 11111111被截断成了11111111,所以变量b存储的是二进制为11111111的值(转成十进制为-1),这种情况在将十进制数值大于127的数字转成byte时普遍存在。所以在将byte转成int时,先将byte类型的数值转成int类型,然后再与255(0XFF)按位相与,这样就保证了数值在不同类型中的不变性。
UTF-8标准的数据结构:
在UTF-8标准中,一个Unicode字符的比特会被分隔成多个部分,并分配到UTF-8的字符串中保存。可参考如下表格:
表1:UTF-8和Unicode的部分对应关系
码点范围 (十六进制) UTF-8 (二进制) 注释 000000 - 00007F 0zzzzzzz ASCII字符范围,字节由零开始 000080 - 0007FF 110yyyyy 10zzzzzz 第一个字节由110开始,接着的字节由10开始 000800 - 00D7FF
00E000 - 00FFFF1110xxxx 10yyyyyy 10zzzzzz 第一个字节由1110开始,接着的字节由10开始 010000 - 10FFFF 11110www 10xxxxxx 10yyyyyy 10zzzzzz 将由11110开始,接着的字节由10开始 一个Unicode字符的二进制序列会被存储在UTF编码的实际存储空间中,对应表1中x,y,z,w标识的二进制位。如果Unicode字符的码值在000000 - 00007F 之间,那么UTF-8会用8个比特位表示一个Unicode字符,其中,最高位固定为0,剩余7位用来存储实际内容。如果Unicode字符的码值在000080 - 0007FF之间,UTF-8会用16个比特位(即两个字节)表示一个Unicode字符,第一个字节的高3位固定为110,第二个字节的高2位固定为10,两个字节的剩余11个比特位用来存储实际内容。如果Unicode字符的码值在000800 - 00D7FF和00E000 - 00FFFF之间,则UTF-8会用24个比特位(即三个字节)来标识一个Unicode字符,第一个字节的高4位固定为1110,后续字节的高2位均固定为10,三个字节剩余的16个比特位用来存储实际内容。
Java int的溢出处理:
在java语言中,一个int值由四个字节(32个比特位)组成,而计算机中数值是用补码表示的,所以在执行计算时会出现溢出的情况。如果执行的加法运算,那么发生溢出时会执行刷新最左边的符号位,运算前的正数在计算后会变成最小负数,运算前如果是负数计算后会变成最大正数。如果执行的是乘法运算,那么在计算时如果认为可能会发生溢出,那么在计算时会用long型来存储中间计算结果。在最终计算完成后,采取高位截取的方式返回一个int结果,该结果可能并不等于实际的计算结果。
参考文献
- Stack Overflow. DataInputStream.read() vs DataInputStream.readFully() [E]
- Stack Overflow. Converting from byte to int in java [E]
- Stack Overflow. UTF-8 & Unicode, what’s with 0xC0 and 0x80? [E]
- 牛奋Debug. byte & 0xff [E]
- 陈奎. 对 byte & 0xFF 的理解 [E]
- Wikipedia. UTF-8 [E]
- njuCZ. java int溢出总结 [E]
- James Gosling等. The Java Language Specification (Java SE 7 Edition) [M]
- 许令波. 深入分析 Java 中的中文编码问题 [E]