DataInputStream

DataInputStream 用途

DataInputStream 与 BufferedInputStream 一样,继承自 FilterInputStream ,它提供了对流进行数据读写的功能:支持从流中直接读出 int ,long , double ,utf8 等等功能。下面谈一下它的一些实现细节。

读取 int , long

无论32位还是64位机器/编译器下,int 都是32位,long 都是 64 位,那么从流中读取一个 int ,一个 long 应该会这样实现:

int readInt()
{
    byte byte4 = new byte[4];
    readFully(byte4);
    int intvalue = 0;
	int leftBits = 32;
	for(byte b : byte4)
	{
		intvalue += (b & 0xff) << (leftBits -= 8);
	}
	return intvalue;
}

int readLong()
{
    byte byte8 = new byte[8];
    readFully(byte8);
    long longvalue = 0;
	int leftBits = 64;
	for(byte b : byte8)
	{
		longvalue += (b & 0xffff) << (leftBits -= 8);
	}
	return longvalue;
}
对于 readInt 的实现是没有问题的,但是 readLong 呢?

主要有两个问题:

1.缺陷:循环内部,每个 b 都是一个字节,八位;而 b&0xffff表示取 b 的低 16 位(如果有多于或等于 16 位的话)。因此,这里使用 b & 0xff 就足够了,不需要使用 0xffff。

2.错误:(b&0xffff)<<(leftBits-=8)这句表示将 b 的低八位左移 leftBits-8 位,而编译器并不知这个值应该被保存为 long 型。实际上,此值被编译器默认为保存为 int ,所以当左移后对应的值超出了 int 表示范围则无法得到正确的值,故正确的写法应该是:

long readLong()
{
    byte[] byte8 = new byte[8];
	readFully(byte8);
	long longvalue = 0;
	int leftBits = 64;
	long curL = 0;
	for(byte b : byte8)
	{
	    curL = b & 0xff;
		longvalue += (curL <<= (leftBits -= 8));
	}
	return longvalue;
}

读取 utf

DataInputStream 读取 utf8 流(方法 readUTF)是这样实现的:

1.读取流开头的两个字节作为无符号整数,此值表示后面有效的 utf 字节流总个数;

2.继续往后读 utf 字节流,按 utf-8 编码规则反解析得到字符;

这里可能会觉得很奇怪,readUTF 为什么取出开头两字节作为流长度呢?这其实是 DataOutputStream.writeUTF 决定的:

在写入字符串时,先取出每个字符,根据每个字符的编码值大小求得需要几个字节存储该编码值(如 [0 ,127] 需要一个字节,[128 , 0x07FF] 两字节,(0x07FF , ?) 三字节),累计得到需要写入的 utf8 字节流的长度,作为两个字节写入。再边转化每个字符边写入。摘录代码如下:

static int writeUTF(String str, DataOutput out) throws IOException {
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;

        /* use charAt instead of copying String to char array */
        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        if (utflen > 65535)
            throw new UTFDataFormatException(
                "encoded string too long: " + utflen + " bytes");

        byte[] bytearr = null;
        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 {
            bytearr = new byte[utflen+2];
        }

        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);

        int i=0;
        for (i=0; i
   
   
    
    = 0x0001) && (c <= 0x007F))) break;
           bytearr[count++] = (byte) c;
        }

        for (;i < strlen; i++){
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                bytearr[count++] = (byte) c;

            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            }
        }
        out.write(bytearr, 0, utflen+2);
        return utflen + 2;
    }
   
   

所以,DataInputStream 与 DataOutputStream 配套使用才能保证正确性。

这里有几个问题:

1).2 字节保存字节流长度是否显得不够 ?

2). 为什么写 utf8 字节流需要保存长度?这样做的动机是什么?

3). 上面 DataOutputStream 的 writeUTF 的实现中,为什么不直接使用 String.getByte 方法获得 utf8 字节流呢?

一一回答之:

1). 2 字节保存字节流长度确实略显不够,但是 writeUTF 的实现中已经限制了写入的 utf8 字节流的长度不大于65535。这是两个字节表示无符号整数的范围。所以程序的逻辑是没有问题的。那么,如果我非要写入长度大于 65535 的 utf8 字节流呢?那就可以连续多次调用 writeUTF ,分多次写入,这样就能写入任意长度的 utf8 字节流。这有一个意想不到的好处:隔离字节流。

2). 实际上,在 1) 中已经回答了这个问题,保存长度有一个作用,就是当往流存储器(如文件)写入多次 utf8 流时这就相当有用了:可以分多次读出这些流。由于每次写入字节流时都在前面附加了字节流的长度,所以每次读出时使用这个长度往后读字节流能够保证正确性。另外,还有一个好处,设想,有一个流存储器无法很快速地得到它里面存放的字节流的总长度:可以通过遍历字节流取得总长度,然而,更快的做法是读出流最前面的两个字节即是!

3). 主要是性能问题没有使用 String.getByte 方法。String.getByte 是一个"综合性"的方法,它可以取出任意编码的字符串的字节流,它使用了很多的类,如 StringCoding,StringEncoder,CharBuffer,ByteBuffer,等等,而且实现的比较复杂。而 writeUTF 中使用最本质最底层的方法,可以预计 String.getByte 方法获得 utf8 字符串的字节流的最底层必然也会有这样的方法,writeUTF 使用的方法必然要比层层包裹下的实现更为高效。


读一行 readLine

该方法是从字节流中读出一行,返回字符串。该方法已经被 deprecated。我们先看它的实现:

遍历读(read)字节流中的每一个字节放入缓存数组 buf,如果 buf 满了,则对 buf 扩充 128 个字节,再拷入原 buf 数组。对当前读入的字节做如下处理:

1). 判断如果是 \n 则直接将读到的字节流构成 String 并返回。

2). 如果是 \r 则读下一个字节,如果是 \n 按 1) 方式返回;否则将读到的这个非 \n 字节压入字节流(PushbackInputStream.unread)以期望下次继续读。

该实现有两个问题:

1).比较低效:一次一次地调用 read 方法:对于读文件而言,每一个 read 就是一个系统调用,十分耗费时间。上面的实现使用了128 作为缓存长度,其实可以每次读128个字节流,判断这个流里面有没有 \r 或者 \n:

如果有,则直接返回 \r\n 或 \r  或 \n 前面的字节流,且移动流读取指针的位置指向 \r\n 或 \r 或 \n 的后面一个字节。如果没有则继续向后读 128 个字节流。如此循环。

2). 实现有错误:没有兼顾字节流的编码,对于非 asscii 编码的字节流无法正确解析。如 UTF8 编码下,一个字符可能占三个字节,而 readLine 的处理方式是一个字节一个字节地将原三个字节读散了,于是这三个字节代表的语义被充分误解了:原来代表一个字符,结果现在强行被代表三个字符,这显然是不正确的!因为这一点,该方法被抛弃了。在实现的源代码里面,作了这样的注释:尽量使用 BufferedReader 的 readLine 方法。此类此方法待时再作分析。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值