Java

Java IO笔记(DataInputStream/DataOu

2019-12-21  本文已影响0人  moonfish1994

(最近刚来到简书平台,以前在CSDN上写的一些东西,也在逐渐的移到这儿来,有些篇幅是很早的时候写下的,因此可能会看到一些内容杂乱的文章,对此深感抱歉,以下为正文)


正文

本篇讲述的是Java IO包中的DataInputStream和DataOutputStream类。

这两个类都属于Java IO中的包装类,为基础流提供了丰富的读写方法,可以将Java中的数据类型轻松地写入流中。

下面还是先贴出源码进行简单的分析:
DataInputStream.java


package java.io;
 
public class DataInputStream extends FilterInputStream implements DataInput {
 
    /**
     * 一个带一个参数的构造方法,传入的参数为一个InputStream对象。内部本质是调用父类FilterInputStream的构造方法。
     */
    public DataInputStream(InputStream in) {
        super(in);
    }
 
    //定义了两个数组,一个是byte型数组,一个是char型数组,初始容量都为80,用于后面readUTF方法中提供缓存数组。
    private byte bytearr[] = new byte[80];
    private char chararr[] = new char[80];
 
    /**
     * 每次读取多个字节的数据,传入的参数为一个byte型数组,读取的数据会填充到该数组中区,最终返回实际读取的字节数。
     */
    public final int read(byte b[]) throws IOException {
        return in.read(b, 0, b.length);
    }
 
    /**
     * 每次读取多个字节的数据,有3个传入的参数,第一个参数为一个byte型数组,用于装取读取到的字节数据,第二和第三个参数都是int型数据,分别表示读取的起点,
     * 和读取的长度。
     */
    public final int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
 
    /**
     * 每次读取多个字节的数据,传入的参数为一个byte型数组,用于装取读取的字节数据。与上面read方法的区别在于该方法必须等到传入的字节数组填满或者读取到文件
     * 结尾或者抛出异常时才会返回,最终返回实际读取的字节数。
     */
    public final void readFully(byte b[]) throws IOException {
        readFully(b, 0, b.length);
    }
 
    /**
     * 每次读取多个字节的数据,传入的第一个参数为一个byte型数组,用于装取读取的字节数据,第二个参数和第三个参数都为int型数据,分别代表着读取的起点和读取
     * 的长度。
     */
    public final void readFully(byte b[], int off, int len) throws IOException {
    //对传入的参数进行安全检测,如果len<0,则抛出相应异常。
        if (len < 0)
            throw new IndexOutOfBoundsException();
    //通过一个循环读取指定长度len的数据,直到读取完毕或者读到文件结尾时才返回。
        int n = 0;
        while (n < len) {
            int count = in.read(b, off + n, len - n);
            if (count < 0)
                throw new EOFException();
            n += count;
        }
    }
 
    /**
     * 该方法用于跳过指定长度的字节数。最终返回实际跳过的字节数。
     */
    public final int skipBytes(int n) throws IOException {
        int total = 0;//实际跳过的字节数
        int cur = 0;  //当前跳过的字节数
 
        while ((total<n) && ((cur = (int) in.skip(n-total)) > 0)) {
            total += cur;
        }
        return total;
    }
 
    /**
     * 读取boolean型数据,原理是读取一个字节的数据,将其与0比较,非0为true,0为false,最终返回比较结果。
     */
    public final boolean readBoolean() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (ch != 0);
    }
 
    /**
     * 每次读取一个byte型数据,原理是每次读取一个字节的数据,然后将其转换成byte型数据并返回。
     */
    public final byte readByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (byte)(ch);
    }
 
    /**
     * 每次读取一个无符号的byte型数据,原理是每次读取一个字节的数据,最终将其返回。
     */
    public final int readUnsignedByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return ch;
    }
 
    /**
     * 每次读取一个short类型的的数据,原理是每次读取两个字节的数据,分别代表着short类型数据的高八位和低八位,最终将其组合成一个short型数据并返回。
     */
    public final short readShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (short)((ch1 << 8) + (ch2 << 0));
    }
 
    /**
     * 每次读取一个无符号short类型的数据,原理是每次读取两个字节的数据,分别代表着short类型数据的高八位和低八位,最终将其组合并返回。
     */
    public final int readUnsignedShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (ch1 << 8) + (ch2 << 0);
    }
 
    /**
     * 每次读取一个char类型的数据,原理是每次读取两个字节的数据,分别代表着char类型数据高八位和低八位,最终将其组合并转换成一个char类型数据并返回。
     */
    public final char readChar() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (char)((ch1 << 8) + (ch2 << 0));
    }
 
    /**
     * 每次读取一个int类型的数据,原理是每次读取4个字节的数据,每个字节的数据依次从int型数据的最高位开始填充,最终将拼装后的int型数据返回。
     */ 
    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }
 
    //定义了一个字节数组,容量为8,用于在readLong方法中调用读取多个字节的read方法时,提供缓存数组。
    private byte readBuffer[] = new byte[8];
 
    /**
     * 每次读取一个long类型的数据,其工作原理是通过readFully方法一次读取8个字节的数据,然后将读取的数据依次从long型数据的最高位开始填充,最终将填充完毕的
     * long型数据返回。
     */
    public final long readLong() throws IOException {
        readFully(readBuffer, 0, 8);
        return (((long)readBuffer[0] << 56) +
                ((long)(readBuffer[1] & 255) << 48) +
                ((long)(readBuffer[2] & 255) << 40) +
                ((long)(readBuffer[3] & 255) << 32) +
                ((long)(readBuffer[4] & 255) << 24) +
                ((readBuffer[5] & 255) << 16) +
                ((readBuffer[6] & 255) <<  8) +
                ((readBuffer[7] & 255) <<  0));
    }
 
    /**
     * 每次读取一个Float型数据,原理是先通过readInt方法读取一个int型数据,然后再将其转换成float行数据并返回。
     */
    public final float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }
 
    /**
     * 每次读取一个double型数据,原理是先通过readLong方法读取一个long型数据,然后再将其转换成double型数据并返回。
     */
    public final double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }
    
    //定义了一个char型数组,用于readLine中存储读取的数据。
    private char lineBuffer[];
 
    /**
     * 一次读取一行数据,最终将读取的数据转换为一个String类型并返回。
     */
    @Deprecated
    public final String readLine() throws IOException {
    //又定义了一个char类型数组buf,作为临时存储数据的缓存。
        char buf[] = lineBuffer;
        //当第一次调用该方法时,buf和lineBuffer都为null,所以对它们进行初始化,初始化容量为128,这里指的注意的是两个数组句柄指向了同一个数组对象。
        if (buf == null) {
            buf = lineBuffer = new char[128];
        }
    //声明了3个int型变量,room为buf的容量大小,offset记录了当前读取的位置,c用来记录每次读取的数据。
        int room = buf.length;
        int offset = 0;
        int c;
 
    //通过一个死循环来不断调用read方法读取数据,在循环外围留下了标记loop,方便在随后的switch块中直接跳出最外围循环。
loop:   while (true) {
        //通过一个switch块来判断每次读取的内容,根据不同的内容来决定不同的操作,主要用于判断是否遇到换行符。
            switch (c = in.read()) {
              case -1:
              case '\n':
        //当读取的内容为'\n'或-1时,都直接跳出循环,-1表示已经没有数据可读,'\n'表示读到换行符。
                break loop;
              case '\r':
        //当读取的内容为'\r'时,因为操作系统原因,换行符除了'\n'外还可能是'\r\n',所以我们需要继续往后读一个字节内容,看后一个字节的内容是否是
        //'\n',如果是则表示读取到换行符,跳出循环,如果不是且读取的数据不为-1(即读取到了新数据),那么通过PushbackInputStream流将第二次读取的数
        //回推回去,然后跳出循环。
                int c2 = in.read();
                if ((c2 != '\n') && (c2 != -1)) {
                    if (!(in instanceof PushbackInputStream)) {
                        this.in = new PushbackInputStream(in);
                    }
                    ((PushbackInputStream)in).unread(c2);
                }
                break loop;
              default:
        //当文件读取没有结束且没有读取到换行符时,执行此处代码。
                if (--room < 0) {
            //--room小于零时,则表示下一次读取时,存放数据的缓存数组容量已经不足,所以此时需要对其扩容(增加128字节的容量),将临时缓存buf扩容后,
            //重新为room值赋值,并将原缓存中的数据拷贝到扩容后的新缓存数组中,最后将lineBuffer也指向扩容后的数组,丢弃吊原有的缓存数组。
                    buf = new char[offset + 128];
                    room = buf.length - offset - 1;
                    System.arraycopy(lineBuffer, 0, buf, 0, offset);
                    lineBuffer = buf;
                }
        //每次读取的数据都根据offset位置,依次存放至缓存数组中去。
                buf[offset++] = (char) c;
                break;
            }
        }
    //当没有读取到任何数据时,直接返回null。
        if ((c == -1) && (offset == 0)) {
            return null;
        }
    //将缓存中的数据转化为String类型数据并返回。
        return String.copyValueOf(buf, 0, offset);
    }
 
    /**
     * 从流中以UTF编码格式读取数据,并以String类型返回。实际调用的是之后带参的readUTF的方法。
     */
    public final String readUTF() throws IOException {
        return readUTF(this);
    }
 
    /**
     * 从流中已UTF编码格式读取数据,传入的参数为一个DataInput对象,用于对流进行读取操作。
     */
    public final static String readUTF(DataInput in) throws IOException {
    //根据DataOutputStream中writeUTF方法规定,前两个字节中存放的是数据的总长度,所以通过readUnsignedShort方法可以将其读取出来并赋值给声明的int型变量
    //utflen。
        int utflen = in.readUnsignedShort();
    //定义了两个数组,一个byte型数组,一个char型数组,前者用于存放流中所有的数据,后者则是存放经过处理后的数据。
        byte[] bytearr = null;
        char[] chararr = null;
    
    //为两个缓存数组进行初始化,先判断传入的DataInput对象是否属于DataInputStream及其衍生类,如果是就直接使用内置的两个缓存数组,并且检测数组容量是否
    //满足数据长度,如果不满足则自动将其容量扩大2倍,如果不属于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];
        }
 
    //声明了3个int型变量,c表示每次读取的数据,因为一个utf8格式的数据最多可以有4个字节,所以在数据拼接时最多用到两个char型数据,char2,char3就是用于
    //数据拼接。count,和chararr_count则分别表示了两个缓存区中的读取的数据位置。
        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
 
    //通过readFully方法,将所有的数据都直接存储到byte型数组bytearr中。
        in.readFully(bytearr, 0, utflen);
 
    //这里其实可以当做对所有的数据进行的一个预处理,通过该方法可以将数据中单字节的数据存储到char型数组中去,如果遇到不是单字节数组的则直接跳出循环。
        while (count < utflen) {
        //前面经过readFully操作,bytearr中存放了所有的数据,所以此时每次从中取出一个字节的数据,通过与Oxff相与转化成int型数据(1与任何一个数相与都是
        //其本身)。
            c = (int) bytearr[count] & 0xff;
        //因为utf8单字节的表示范围是0~127,所以如果c>127则直接退出。
            if (c > 127) break;
        //正常情况下则是将读取的数据存放进char型数组中,两个索引值自动加1。
            count++;
            chararr[chararr_count++]=(char)c;
        }
 
    //下面则是对数据进行正式处理,通过一个循环遍历byte数组bytearr中的数据,进行匹配后进行不同的操作,然后将拼接的数据方法char型数组chararr中。
        while (count < utflen) {
        //每次从bytearr中取出一个字节的数据,通过与0xff相与转化成int型数据赋值给c。
            c = (int) bytearr[count] & 0xff;
        //下面将c右移4位,然后用一个switch块将其包裹,来对其进行匹配。这里要说一下UTF8的编码规则了,如果只有一个字节则其最高二进制位为0;如果是多字
        //节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头,后面会附上utf编码格式图。下面的操作实际
        //上只取utf8数据的高八位,右移四位后,可以通过其值来判断字节数(1的个数)。
            switch (c >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    // 0xxxxxxx,该种情况下表示是单字节数据,可以直接将其转成char型数据存放入chararr数组中去。
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                case 12: case 13:
                    // 110xxxxx 10xxxxxx,该种情况下是双字节数据,需要从byte型数组中取出两个字节数据,所以先将读取索引count+2。然后进行安全检测,读取位置
                    // 不能超过数据总长度。
                    count += 2;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
            //这里c是两字节的高八位,所以还需向后读取一字节数据,赋值给char2。
                    char2 = (int) bytearr[count-1];
            //根据utf8的编码规则可知,双字节的后八位必须满足10xxxxxx,所以将其与0xCO相与,如果满足,运算结果必为0x80,所以如果不等于,则抛出异常。
                    if ((char2 & 0xC0) != 0x80)
                        throw new UTFDataFormatException(
                            "malformed input around byte " + count);
            //这里就是将读取的两个字节的数据拼装成一个char型数据了,以双字节详细说明:
            //step1(c & 0x1F):c读取的其实是双字节数据的高八位,因为utf8编码格式,双字节高八位的数据编码格式为110xxxxx,当与0x1F相与后可以将前
            //3位的标志位置0,只保留数据位,然后左移6位作为char型数据的高位,至于为什么是移动6位,是因为双字节utf8编码的表示范围是128~2047,所以
            //数据位使用12位就足够表示,因此在DataOutputStream中的writeUTF中就以6位为一个移动的步数。
            //step2(char2 & 0x3F):char2为读取的双字节数据的低八位,因为utf8编码格式,双字节低八位的数据编码格式为10xxxxxx,当与0x3F相与后将高两位
            //的标志位置0,只保留数据位。到此一个char型数据的所有数据位都得到了。
            //step3(将两者或):通过一个或操作将上两步得到的数据位拼接起来得到一个char型数据存储到字符数组chararr中。
                    chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
                                                    (char2 & 0x3F));
                    break;
                case 14:
                    //1110 xxxx  10xx xxxx  10xx xxxx ,此种情况下表示是3字节数据,需从bytearr数组中取出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];
                    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 剩余的就是这两者情况了,刚开始在这里纠结了好久,因为标准utf8是可以占有1~4个字节的,然而这里并没有对占用4字节
            // 的情况进行处理,但是后来通过在Stack Overflow找到了答案,其实源码的JavaDocs中已经有了解释,在这里使用的是modified UTF-8而不是标准的
            // utf8,所以只会占用1~3个字节,在这里不得不再次感叹一下,Stack Overflow确实很强大。
                    throw new UTFDataFormatException(
                        "malformed input around byte " + count);
            }
        }
        // 最终将chararr中的数据转换成String类型并返回,这里指的注意的在源码中这里有个注释'The number of chars produced may be less than utflen'。
        return new String(chararr, 0, chararr_count);
    }
}

DataOutputStream.java

package java.io;
 
public class DataOutputStream extends FilterOutputStream implements DataOutput {
    //定义了一个int型变量written,该变量用于记录实际写入数据的字节数,当写入的数据超过int型数据的表达范围时,该值将被赋予Integer.MAX_VALUE.
    protected int written;
 
    //声明了一个byte型数组的句柄,该数组在之后的writeUTF中充当着数据缓存的作用。
    private byte[] bytearr = null;
 
    /**
     * 一个带一个参数的构造方法,传入的参数是一个OutputStream对象,内部调用FilterOutputStream对象的构造参数。
     */
    public DataOutputStream(OutputStream out) {
        super(out);
    }
 
    /**
     * 该方法用于记录在流中吸入了多少字节的数据,如果长度超过了int型表达范围,则该值一直为Integer.MAX_VALUE。
     */
    private void incCount(int value) {
        int temp = written + value;
    //这里的temp<0的情况其实就是int型变量数据溢出了。
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }
 
    /**
     * 该方法每次向写入一个字节的数据。
     */
    public synchronized void write(int b) throws IOException {
        out.write(b);
    //每写入一个字节数据后,调用incCount方法,使得计数加1。
        incCount(1);
    }
 
    /**
     * 该方法每次写入多个字节的数据,含有3个参数,第一个参数为一个byte型数组, 作为要写入流中的数据源,后两个参数都为int型数据,第一个表示开始写入的位置,
     * 第二个为写入的长度,成功往流中写入数据后调用incCount方法,记录下写入数据的长度。
     */
    public synchronized void write(byte b[], int off, int len)
        throws IOException
    {
        out.write(b, off, len);
        incCount(len);
    }
 
    /**
     * 该方法用于将缓存区中的数据写入流中。
     */
    public void flush() throws IOException {
        out.flush();
    }
 
    /**
     * 每次向流中写入一个boolean型数据,本质其实是根据java中默认的非零为true,0为false,向流中写入一个数值,读取时根据这个规则再还原成对应的boolean型值。
     */
    public final void writeBoolean(boolean v) throws IOException {
        out.write(v ? 1 : 0);
        incCount(1);
    }
 
    /**
     * 每次向流中写入一个byte型数据。
     */
    public final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }
 
    /**
     * 每次向流中写入一个short型数据,实质是经过两次写入操作,先写入short型数据的高八位,再写入低八位。
     */
    public final void writeShort(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
 
    /**
     * 原理同上,每次向流中写入一个char型数据,实质是经过两次写入,先入char型数据的高八位,在写入其低八位。
     */
    public final void writeChar(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
 
    /**
     * 每次向流中写入一个int型数据,实质是经过4次写入,依次从最高位开始,每八位为一组,向流中写入数据。
     */
    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }
 
    //定义了一个byte型数组,其容量为8个字节,用于后面的writeLong方法中作为数据缓存使用。
    private byte writeBuffer[] = new byte[8];
 
    /**
     * 一次向流中写入一个long型数据。将long型数据从最高位开始以8位为一组,依次写入缓存数组中,最后通过write方法写入流中。
     */
    public final void writeLong(long v) throws IOException {
        writeBuffer[0] = (byte)(v >>> 56);
        writeBuffer[1] = (byte)(v >>> 48);
        writeBuffer[2] = (byte)(v >>> 40);
        writeBuffer[3] = (byte)(v >>> 32);
        writeBuffer[4] = (byte)(v >>> 24);
        writeBuffer[5] = (byte)(v >>> 16);
        writeBuffer[6] = (byte)(v >>>  8);
        writeBuffer[7] = (byte)(v >>>  0);
        out.write(writeBuffer, 0, 8);
        incCount(8);
    }
 
    /**
     * 一次向流中写入一个float型数据,本质是将float型数据转化成int型数据,然后通过writeInt方法写入流中。
     */
    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }
 
    /**
     * 一次向流中写入一个double型数据,本质是将double型数据转化成long型数据,然后通过writeLong方法写入流中。
     */
    public final void writeDouble(double v) throws IOException {
        writeLong(Double.doubleToLongBits(v));
    }
 
    /**
     * 一次向流中以byte的形式写入一个字符串类型数据,本质是通过字符串的charAt方法,将字符串中的每个字符转化成byte型数据写入流中。
     */
    public final void writeBytes(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            out.write((byte)s.charAt(i));
        }
        incCount(len);
    }
 
    /**
     * 一次向流中以char的形式写入一个字符串类型数据,本质是通过字符串的charAt方法,将字符串中的每个字符以char型(通过高低位拆分)直接写入流中。
     */
    public final void writeChars(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            int v = s.charAt(i);
            out.write((v >>> 8) & 0xFF);
            out.write((v >>> 0) & 0xFF);
        }
        incCount(len * 2);
    }
 
    /**
     * 一次向流中以utf8的格式写入一个String类型的数据。本质是调用之后带两个参数的writeUTF方法。
     */
    public final void writeUTF(String str) throws IOException {
        writeUTF(str, this);
    }
 
    /**
     * 一次向流中以utf8的格式写入一个String类型的数据。
     */
    static int writeUTF(String str, DataOutput out) throws IOException {
    //定义了四个int型变量,strlen记录了要写入String类型数据的总长度,utflen记录了以utf8格式进行编码后所占用的字节数,c用于记录str中每个字符的数据,
    //count则表示了str中已经写入流中的字符数。
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;
 
        //这里使用一个循环来记录以utf8格式来将数据写入流中所需要的字节数。这里讲究的原则是,单字节utf8数据表达的范围是0~127,utf8双字节表达范围为128~
    //2047,三字节为~65535。因此根据取出字符的数值可以得知相应字符转换成utf8格式所需要的字节数,然后将utflen累加上相应的字节数,最终得到总共需要的utf
    //8格式下的字节数。
        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;
            }
        }
    
    //这里限制了所要写入string类型数据的总长度,在转化为utf8格式后,其总长度不要超过65535,否则将会抛出相应异常。
        if (utflen > 65535)
            throw new UTFDataFormatException(
                "encoded string too long: " + utflen + " bytes");
    //定义了一个字节数组,用于充当向流中写入数据时的临时缓存。
        byte[] bytearr = null;
    //先判断传入的out对象是否是DataOuputStream类或者其子类,如果是,就使用其内部定义的bytearr数组,否则就新建一个byte数组赋值给bytearr。值得注意的是
    //如果使用内置的数组,会先对内置数组进行检测,检测其是否为null和容量是否满足需求,如果不满足也要重新创建。
        if (out instanceof DataOutputStream) {
            DataOutputStream dos = (DataOutputStream)out;
            if(dos.bytearr == null || (dos.bytearr.length < (utflen+2)))
        //这里就是内置数组为null或者容量不够时,重新创建数组对象,新建的容量会根据utflen来进行扩容,这里注意到所有的数组容量中都有+2的操作,这是
        //除了要写入数据外,该方法还会在开头出写入两个字节的数据,这两个字节的数据记录了流中数据的总长度。
                dos.bytearr = new byte[(utflen*2) + 2];
            bytearr = dos.bytearr;
        } else {
            bytearr = new byte[utflen+2];
        }
 
    //在开端写入两个字节的数据,该数据为utflen,记录了以utf8格式要写入数据的总的字节数。因为前面限制了总长度不能高于65535,所以这里只需两个字节就可以
    //记录下总长度。
        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
    
    //下面通过一个循环,先对数据进行预处理,如果是utf8中的单字节数据,则直接装换成byte型写入数据缓存中。
        int i=0;
        for (i=0; i<strlen; i++) {
           c = str.charAt(i);
           if (!((c >= 0x0001) && (c <= 0x007F))) break;
           bytearr[count++] = (byte) c;
        }
 
    //下面是正式处理要写入的数据,根据其数值范围,确定其在utf8格式下需要几个字节来表示,然后根据相应字节数通过对应的移位操作,将其以8位为一组,从最高
    //开始,依次写入数据缓存区中。
        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);
    //最终返回以utf8格式写入流中的字节数。
        return utflen + 2;
    }
 
    /**
     * 返回当前写入流中数据的字节总数。
     */
    public final int size() {
        return written;
    }
}

为了方便理解,这里附上一幅utf8编码的一些小格式:


UTF编码

通过以上对源码的简要描述,相信我们对DataInputStream/DataOutputStream有了初步的认识,下面我们通过一个简单的小例子来展示这两个类的具体用法:

package dataInOut;
 
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
 
public class DataIOTest1 {
    public static void main(String[] args) {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(
                new File("./src/file/test1.txt")));
                DataInputStream dis = new DataInputStream(new FileInputStream(
                        new File("./src/file/test1.txt")))) {
            dos.writeInt(1);
            dos.writeBoolean(true);
            dos.writeUTF("Hello World");
            dos.flush();
            int tempInt = dis.readInt();
            boolean tempBoolean = dis.readBoolean();
            String tempUTF = dis.readUTF();
            System.out.println(tempInt+" : "+tempBoolean+" : "+tempUTF);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上为本篇的全部内容。

上一篇下一篇

猜你喜欢

热点阅读