12、ByteArrayInputStream和DataInputStream的源码分析和使用方法详细分析

一、ByteArrayInputStream的源码------零拷贝(Zero-Copy)的一种字节流

在传统的磁盘 I/O(比如FileInputStream.class、BufferedInputStream.class...等) 中,使用者都需要将磁盘的数据先复制到内存中来使用而无法实现零拷贝(Zero-Copy),而ByteArrayInputStream .class可以从内存中直接读取数据,因此,ByteArrayInputStream .class这个类正是为了解决内存数据读取而存在的,ByteArrayInputStream 的核心价值在于零拷贝(Zero-Copy),它允许将内存中的byte[]字节数组当作输入流来读取,是处理内存数据的常用工具,如下所示:

ByteArrayInputStream .class的UML关系图,如下所示:

ByteArrayInputStream .class的源码如下:

复制代码
package java.io;

public
class ByteArrayInputStream extends InputStream {
    //byte[] buf变量直接指向了内存中的byte[]字节数组,不会向BufferedInputStream源码中的那样,先将数据复制到自己内部的byte[]字节数组上,再进行数据处理
    protected byte buf[];
     //准备从当前内存中的byte[]数组(直接引用)读取字节的索引位置,取值范围为[0,count)
    protected int pos;
     //标记某个索引位置,0<=mark<=pos
    //该变量只会在 构造函数和mark()函数中赋值
    protected int mark = 0;
    //byte[] buf变量指向的内存中的byte[]字节数组的长度
    protected int count;
    //构造函数
    public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;//byte[] buf变量直接指向了内存中的byte[]字节数组
        this.pos = 0;
        this.count = buf.length;
    }
    //构造函数,准备从byte buf[]字节数组中读取字节的索引位置从int offset索引位置开始
    public ByteArrayInputStream(byte buf[], int offset, int length) {
        this.buf = buf;//byte[] buf变量直接指向了内存中的byte[]字节数组
        this.pos = offset;
        //读取数组的右边界索引位置 < 内存中的byte[]字节数组的长度
        this.count = Math.min(offset + length, buf.length);
        this.mark = offset;//读取数组时候的左边界索引位置>=offset
    }
    //线程同步的函数,从内存中的byte[]字节数组读取1个字节
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
    //线程同步的函数,从内存中的byte[]字节数组读取len个字节到指定的byte[]数组b中,这len个字节被放到byte[]数组b的[off,off+len)索引位置。
    public synchronized int read(byte b[], int off, int len) {
        if (b == null) {
            //如果指定的内存中的byte[]数组b为null,抛出一个NullPointerException
            throw new NullPointerException();
        //范围检测,off和len必须是非负数,b.length - off是内存中的byte[]数组还可以放的字节(ASCII码值)的数量
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        }
        //pos>=count时,返回-1,表示内存中的byte[]数组已经读取完毕
        if (pos >= count) {
            return -1;
        }
        //int avail表示本次可以从内存中的byte[]数组中读取到的字节数量
        int avail = count - pos;
        if (len > avail) {
            len = avail;
        }
        if (len <= 0) {
            return 0;
        }
        //从内存中的byte[]字节数组读取[pos,pos+len)索引位置的字节到指定的byte[]数组b中,这len个字节被放到byte[]数组b的[off,off+len)索引位置。
        System.arraycopy(buf, pos, b, off, len);
        pos += len;//跟新int pos变量的值
        return len;
    }
    //线程同步的函数
    public synchronized long skip(long n) {
        //尝试更新右指针int pos的值,对于不同的n值,有以下2种可能:
        //a、如果n<count - pos并且n>0时,才更新int pos= pos+n
        //b、当n<0,或者n>=count - pos时,更新int pos = count
        long k = count - pos;
        if (n < k) {
            k = n < 0 ? 0 : n;
        }

        pos += k;
        return k;
    }
    //线程同步的函数,返回内存中的byte[]字节数组还可以读取的字节数量
    public synchronized int available() {
        return count - pos;
    }

    public boolean markSupported() {
        return true;
    }
    //更新int mark变量为pos的值
    public void mark(int readAheadLimit) {
        mark = pos;
    }
    //线程同步的函数,重置int pos变量的值为mark的值
    public synchronized void reset() {
        pos = mark;
    }

    public void close() throws IOException {
    }
}
1.1、ByteArrayInputStream.class从内存中读取数据的过程

ByteArrayInputStream.class本质就是通过直接引用字节数组来实现的零拷贝,并通过双指针(int mark是左指针和int pos是右指针)来实现的read()函数、skip()函数、reset()函数...等,ByteArrayInputStream.class从内存中读取数据的过程如下所示:

复制代码
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class ByteArrayInputStreamTest {

    private static final int LEN = 5;
    // 对应英文字母"abcdefghijklmnopqrstuvwxyz"(ASCII编码)
    private static final byte[] ArrayLetters = {
        0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
        0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A
    };
    public static void main(String[] args) {
        //step1:创建ByteArrayInputStream字节流,引用的是内存中的ArrayLetters数组
        ByteArrayInputStream bais = new ByteArrayInputStream(ArrayLetters);

        //step2: 从ByteArrayInputStream指向的内存数组中读取5个字节
        for (int i=0; i<LEN; i++) {
            // 若能继续读取下一个字节,则读取下一个字节
            if (bais.available() > 0) {
                // 读取ByteArrayInputStream指向的内存数组中的下一个字节
                int tmp = bais.read();
                System.out.printf("%d : 0x%s\n", i, Integer.toHexString(tmp));
            }
        }
        //step3:标记ByteArrayInputStream中下一个被读取的索引位置(即--标记"0x66"),因为因为前面已经读取了5个字节,所以下一个被读取的位置是第6个字节
        // ①、 ByteArrayInputStream中的mark(0)函数中的"参数0"是没有实际意义的。
        // ②、 mark()与reset()是配套的,reset()会将字节流中右指针int pos指向的索引重置为mark()函数中左指针int mark指向的索引
        bais.mark(0);

        //step4: 跳过5个字节。跳过5个字节后,下一个被读取的索引位置应该是ByteArrayInputStream指向的内存数组中"0x6B"。
        bais.skip(5);

        //step5:从ByteArrayInputStream指向的内存数组中读取5个字节到byte[] buf数组的[0,5)索引位置。即读取"0x6B, 0x6C, 0x6D, 0x6E, 0x6F"
        byte[] buf = new byte[LEN];
        bais.read(buf, 0, LEN);
        String str1 = new String(buf);
        System.out.printf("str1=%s\n", str1);
    
        //step6、重置ByteArrayInputStream中的右指针int pos为mark()函数中左指针int mark指向的索引,即0x66。
        bais.reset();
        //step7、从"重置后的ByteArrayInputStream"中读取5个字节到buf中。即读取"0x66, 0x67, 0x68, 0x69, 0x6A"
        bais.read(buf, 0, LEN);
        String str2 = new String(buf);
        System.out.printf("str2=%s\n", str2);
    }
}

上述代码的执行过程为:

①、step1:创建ByteArrayInputStream对象,引用的是内存中的byte[] ArrayLetters数组,如下所示(int mark = 0,int pos = 0):

②、step2:从内存中的byte[] ArrayLetters数组中依次读取5个字节,读取完成后,int pos=5,int mark=0,如下所示(int mark = 0,int pos = 5):

③、step3:标记ByteArrayInputStream中下一个被读取的索引位置(即--标记"0x66"),因为因为前面已经读取了5个字节,所以下一个被读取的位置是第6个字节,如下所示(int mark = 5,int pos = 5):

mark(0)函数需要注意以下2点

a、 ByteArrayInputStream中的mark(0)函数中的"入参0"是没有实际意义的;

b、 mark()函数与reset()函数是配套的,reset()函数会将ByteArrayInputStream中右指针int pos指向的索引重置为mark()函数中左指针int mark指向的索引。

④、step4: 跳过5个字节。跳过5个字节后,下一个被读取的索引位置应该是ByteArrayInputStream指向的内存数组中"0x6B",如下所示(int mark = 5,int pos = 10):

⑤、step5: 从ByteArrayInputStream指向的内存数组中读取5个字节到byte[] buf数组的[0,5)索引位置。即读取"0x6B, 0x6C, 0x6D, 0x6E, 0x6F",如下所示(int mark = 5,int pos = 15):

⑥、step6: 重置ByteArrayInputStream中的右指针int pos为step3中调用mark()函数时左指针int mark指向的索引位置,即0x66,如下所示(int mark = 5,int pos = 5):

⑦、step7: 从"重置后的ByteArrayInputStream"中读取5个字节到buf中。即读取"0x66, 0x67, 0x68, 0x69, 0x6A",如下所示(int mark = 5,int pos = 10):

二、DataInputStream的源码------Java原生8种数据类型解析和UTF-8 解码算法实现的一种装饰器流

DataInputStream.class是 Java I/O 体系中实现了 DataInput.interface 接口的核心类,自 JDK 1.0 起就为从机器无关的二进制数据中读取基本 Java 数据类型提供了标准实现,同时也继承了FilterInputStream.class,因此,也可以用来装饰其它输入流 。其中,DataInputStream.class::readUTF()函数是UTF-8 解码算法的精妙实现,这是 Java 序列化和网络协议的基石。

DataInputStream.class的UML关系图,如下所示:

复制代码
public
class DataInputStream extends FilterInputStream implements DataInput {
    //构造函数,需要传入一个被装饰的输入流
    public DataInputStream(InputStream in) {
        super(in);
    }
    
    private byte bytearr[] = new byte[80];
    private char chararr[] = new char[80];
    //实际调用的是InputStream.class::read()函数
    public final int read(byte b[]) throws IOException {
        return in.read(b, 0, b.length);
    }
    //实际调用的是InputStream.class::read()函数
    public final int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    
    public final void readFully(byte b[]) throws IOException {
        readFully(b, 0, b.length);
    }

    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;
        }
    }
    //当前这个DataInputStream对象可以从它装饰的输入流中(可以理解为一个大型的字节数组)中跳过n个字节(n<被装饰的输入流中可用的字节数量)再进行后续操作(比如通过read()函数读取等)
    public final int skipBytes(int n) throws IOException {
        int total = 0;//累计跳过的字节总数量
        int cur = 0;//本次通过InputStream.class::skip()函数跳过的字节总数量
        
        //当InputStream.class::skip()函数的返回值<=0时,表示DataInputStream对象装饰的输入流中已经没有可读的字节了
        while ((total<n) && ((cur = (int) in.skip(n-total)) > 0)) {
            total += cur;//累加从被装饰的输入流中跳过的字节总量
        }

        return total;
    }

    //读取1个boolean类型数据
    public final boolean readBoolean() throws IOException {
        int ch = in.read();//boolean类型占一个字节
        
        if (ch < 0)
            throw new EOFException();
        return (ch != 0);
    }
    //读取1个byte类型数据
    public final byte readByte() throws IOException {
        int ch = in.read();//byte类型占1个字节
        if (ch < 0)
            throw new EOFException();
        return (byte)(ch);
    }
    //读取1个无符号的byte类型数据
    public final int readUnsignedByte() throws IOException {
        int ch = in.read();//byte类型占1个字节
        if (ch < 0)
            throw new EOFException();
        return ch;
    }
    //读取1个short类型数据
    public final short readShort() throws IOException {
        int ch1 = in.read();//short类型占2个字节
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (short)((ch1 << 8) + (ch2 << 0));
    }
    //读取1个无符号的short类型数据
    public final int readUnsignedShort() throws IOException {
        int ch1 = in.read();//无符号的short类型占2个字节
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (ch1 << 8) + (ch2 << 0);
    }
    //读取1个char类型数据
    public final char readChar() throws IOException {
        int ch1 = in.read();//char类型占2个字节
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (char)((ch1 << 8) + (ch2 << 0));
    }
    //读取1个int类型数据
    public final int readInt() throws IOException {
        int ch1 = in.read();//char类型占4个字节
        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));
    }

    private byte readBuffer[] = new byte[8];
    //读取1个long类型数据
    public final long readLong() throws IOException {
        readFully(readBuffer, 0, 8);//long类型占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));
    }
    //读取1个float类型数据
    public final float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }
    //读取1个double类型数据
    public final double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }

    public final String readUTF() throws IOException {
        return readUTF(this);
    }
    //从入参的输入流中读取UTF-8编码格式,,并以String字符串的形式返回。
    public final static String readUTF(DataInput in) throws IOException {
        //UTF-8数据的长度包含在它的前两个字节当中,通过readUnsignedShort()读取出前两个字节对应的正整数就是UTF-8数据的长度。
        int utflen = in.readUnsignedShort();
        byte[] bytearr = null;
        char[] chararr = null;
        //判断入参的输入流本身是不是DataInputStream类型
        if (in instanceof DataInputStream) {
            DataInputStream dis = (DataInputStream)in;
            if (dis.bytearr.length < utflen){
                //如果入参的输入流是DataInputStream类型并且读取的UTF-8数据的长度utflen>80,则将byte[] bytearr数组和char[] chararr数组的长度设置为utflen(读取的UTF-8数据的长度)*2
                dis.bytearr = new byte[utflen*2];
                dis.chararr = new char[utflen*2];
            }
            //如果入参的输入流是DataInputStream类型并且读取的UTF-8数据的长度<=80,byte[] bytearr数组和char[] chararr数组的长度设置为80
            chararr = dis.chararr;
            bytearr = dis.bytearr;
        } else {
            //如果入参的输入流不是DataInputStream类型,则将byte[] bytearr数组和char[] chararr数组的长度设置为utflen(读取的UTF-8数据的长度)
            bytearr = new byte[utflen];
            chararr = new char[utflen];
        }

        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
        //将UTF-8数据全部读取到字节数组byte[] bytearr中
        in.readFully(bytearr, 0, utflen);

        //对单字节数据表示的UTF-8编码先进行处理
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;//先将每个单字节表示的UTF-8编码转换成int值
            if (c > 127) break;// UTF-8的单字节数据的值不会超过127;所以,当有超过127的单字节数据时,则退出。
            count++;
            chararr[chararr_count++]=(char)c;// 再转换为char c,将c保存到字符数组char[] chararr中
        }

        while (count < utflen) {
            //现将byte类型表示的UTF-8格式编码的数据转换成int类型
            c = (int) bytearr[count] & 0xff;
            switch (c >> 4) {//判断该UTF-8格式编码的数据是几字节的模板,总共有4种不同长度字节组成的模板分别为:1字节长度组成的UTF-8编码数据、2字节长度组成的UTF-8编码数据、3字节长度组成的UTF-8编码数据、4字节长度组成的UTF-8编码数据
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    //1字节长度组成的UTF-8编码数据,该字节右移4位之后取值范围为0~7
                    /* 0xxxxxxx*/
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                case 12: case 13:
                    //2字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节的取值范围为12~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:
                    //3字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节一定为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:
                    //4字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节一定为15
                    //Java中不支持4字节长度的UTF-8编码数据解析,发现4字节长度的UTF-8编码数据,则抛出一个UTFDataFormatException
                    /* 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);
    }
}
2.1、DataInputStream.class的readUTF()函数

readUTF()函数是用来从输入流中读取UTF-8编码的数据,并以String字符串的形式返回,UTF-8的详细编码规则和解析规则,请查看我的另一篇博客:11、(ByteArrayInputStream和DataInputStream的源码分析和使用方法详细分析前言)UTF-8 编码规则

DataInputStream.class::readUTF()函数的源码如下:

复制代码
public
class DataInputStream extends FilterInputStream implements DataInput {
    ...省略部分代码...
  
    //从入参的输入流中读取UTF-8编码格式,,并以String字符串的形式返回。
    public final static String readUTF(DataInput in) throws IOException {
        //UTF-8数据的长度包含在它的前两个字节当中,通过readUnsignedShort()读取出前两个字节对应的正整数就是UTF-8数据的长度。
        int utflen = in.readUnsignedShort();
        byte[] bytearr = null;
        char[] chararr = null;
        //判断入参的输入流本身是不是DataInputStream类型
        if (in instanceof DataInputStream) {
            DataInputStream dis = (DataInputStream)in;
            if (dis.bytearr.length < utflen){
                //如果入参的输入流是DataInputStream类型并且读取的UTF-8数据的长度utflen>80,则将byte[] bytearr数组和char[] chararr数组的长度设置为utflen(读取的UTF-8数据的长度)*2
                dis.bytearr = new byte[utflen*2];
                dis.chararr = new char[utflen*2];
            }
            //如果入参的输入流是DataInputStream类型并且读取的UTF-8数据的长度<=80,byte[] bytearr数组和char[] chararr数组的长度设置为80
            chararr = dis.chararr;
            bytearr = dis.bytearr;
        } else {
            //如果入参的输入流不是DataInputStream类型,则将byte[] bytearr数组和char[] chararr数组的长度设置为utflen(读取的UTF-8数据的长度)
            bytearr = new byte[utflen];
            chararr = new char[utflen];
        }

        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
        //将UTF-8数据全部读取到字节数组byte[] bytearr中
        in.readFully(bytearr, 0, utflen);

        //对单字节数据表示的UTF-8编码先进行预处理,如果处理过程中碰到多字节数据表示的utf-8编码,就先退出预处理
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;//先将每个单字节表示的UTF-8编码转换成int值
            if (c > 127) break;// UTF-8的单字节数据的值不会超过127;所以,当有超过127的单字节数据时,则退出。
            count++;
            chararr[chararr_count++]=(char)c;// 再转换为char c,将c保存到字符数组char[] chararr中
        }
        //正式读取环节
        while (count < utflen) {
            //现将byte类型表示的UTF-8格式编码的数据转换成int类型
            c = (int) bytearr[count] & 0xff;
            switch (c >> 4) {//判断该UTF-8格式编码的数据是几字节的模板,总共有4种不同长度字节组成的模板分别为:1字节长度组成的UTF-8编码数据、2字节长度组成的UTF-8编码数据、3字节长度组成的UTF-8编码数据、4字节长度组成的UTF-8编码数据
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    //1字节长度组成的UTF-8编码数据,该字节右移4位之后取值范围为0~7
                    /* 0xxxxxxx*/
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                case 12: case 13:
                    //2字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节的取值范围为12~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:
                    //3字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节一定为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:
                    //4字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节一定为15
                    //Java中不支持4字节长度的UTF-8编码数据解析,发现4字节长度的UTF-8编码数据,则抛出一个UTFDataFormatException
                    /* 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);
    }  
}

可以使用下面的代码先向我的windows系统下的D:\utf-8_Test.txt 文件中写入3个UTF-8编码的字符"中Aé",如下所示:

复制代码
package com.chelong.bio;
import java.io.*;

public class DataInputStreamTest {
   public static void main(String[] args) throws FileNotFoundException {
      File file = new File("D:\\utf-8_Test.txt");
      try {
         // 创建 FileOutputStream 对象,指定要向其中写入数据的文件
         FileOutputStream fos = new FileOutputStream(file);
         // 创建 DataOutputStream 对象,用来向文件中写入数据
         DataOutputStream dos = new DataOutputStream(fos);
         dos.writeUTF("中Aé"); // 将字符串写入文件中
         dos.close();
         fos.close();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

通过上面的代码向我的windows系统下的D:\utf-8_Test.txt 文件中写入了3个UTF-8编码的字符"中Aé"之后,该D:\utf-8_Test.txt 文件的内容如下:

然后再使用下面的代码从上文中写入的D:\utf-8_Test.txt文件中读取UTF-8编码的3个字符"中Aé",如下所示:

复制代码
package com.chelong.bio;
import java.io.*;

public class DataInputStreamTest {
   public static void main(String[] args) throws FileNotFoundException {
      File file = new File("D:\\utf-8_Test.txt");
      try {
         // 创建 FileInputStream 对象,指定要从中读取数据的文件
         FileInputStream fis = new FileInputStream(file);
         // 创建 DataInputStream 对象,用来从文件中读取数据
         DataInputStream dis = new DataInputStream(fis);
         System.out.println("readUTF 方法读取数据:" + dis.readUTF()); // 读取字符串
         dis.close();
         fis.close();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

读取UTF-8编码的字符的代码的执行结果如下:

2.1.1、readUTF()函数的执行过程详解

当执行上文中的

复制代码
dis.readUTF()

函数时,该函数会从写入的D:\utf-8_Test.txt文件中读取UTF-8编码的字符串,整个执行过程分为以下4步:

①、通过readUnsignedShort()函数读取出前两个字节对应的正整数utflen=6(也就该文件中UTF-8数据的总长度),然后将字节数组byte[] bytearr和字符数组char[] chararr的长度都设置为80,如下所示:

②、然后初始化操作字节数组byte[] bytearr数组的指针int count = 0;和操作char[] chararr数组的指针 int chararr_count=0;再通过

复制代码
in.readFully(bytearr, 0, utflen);

函数,将UTF-8数据全部读取到字节数组byte[] bytearr中,如下所示:

③、然后对单字节数据表示的UTF-8编码先进行预处理,如果预处理过程中碰到多字节数据表示的utf-8编码,就先退出预处理;

④、最后进入正式读取UTF-8编码的环节,正式读取UTF-8编码的环节又分为以下3步:

a、先从字节数组byte[] bytearr中读取第1个字节来判断当前这个UTF-8编码的数据是几字节模板(UTF-8编码的数据有4种长度的字节模板,但是在Java中只使用1~3字节长度的字节模板)

复制代码
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
    case 14:

第1个UTF-8编码的数据"中"(UTF-8 编码为 0xE4 0xB8 0xAD)会匹配到3字节的模板,如下所示:

然后进行后面2个字节的读取,最后再合并从这3个字节中取到的所有关于UTF-8的编码的bit位,组成完整的UTF-8编码的数据

复制代码
                    //3字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节一定为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];//读取3字节模版组成的UTF-8编码数据中的第2个字节
                    char3 = (int) bytearr[count-1];//读取3字节模版组成的UTF-8编码数据中的第3个字节
                    //校验3字节模版组成的UTF-8编码数据中的第2、3个字节
                    if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                        throw new UTFDataFormatException(
                            "malformed input around byte " + (count-1));
                    //合并第1、2、3个字节组成最终的3字节模版组成的UTF-8编码数据
                    chararr[chararr_count++]=(char)(((c     & 0x0F) << 12) |
                                                    ((char2 & 0x3F) << 6)  |
                                                    ((char3 & 0x3F) << 0));
                    break;

b、读取完前3个字节之后便完成了第1个UTF-8编码的数据"中"的解析,然后开始读取字节数组byte[] bytearr的第4个字节,该字节也是用来判断当前这个UTF-8编码的数据是几字节模板的(UTF-8编码的数据有4种长度的字节模板,但是在Java中只使用1~3字节长度的字节模板)

复制代码
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:

第2个UTF-8编码的数据"A"( UTF-8 编码为0x41)会匹配到1字节的模板,如下所示:

然后对count++,将这1个字节中读取到的关于UTF-8的编码的bit位,转换为2字节的char类型数据,放入到字符数组char[] chararr中

复制代码
                    //1字节长度组成的UTF-8编码数据,该字节右移4位之后取值范围为0~7
                    /* 0xxxxxxx*/
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;

c、读取完第4个字节之后便完成了第2个UTF-8编码的数据"A"的解析,然后开始读取字节数组byte[] bytearr的第5个字节,该字节也是用来判断当前这个UTF-8编码的数据是几字节模板的(UTF-8编码的数据有4种长度的字节模板,但是在Java中只使用1~3字节长度的字节模板)

复制代码
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
    case 12: case 13:

第3个UTF-8编码的数据"é"( UTF-8 编码为0xC3 0xA9)会匹配到2字节的模板,如下所示:

然后进行后面1个字节的读取,最后再合并从这2个字节中取到的所有关于UTF-8的编码的bit位,组成完整的UTF-8编码的数据

复制代码
                    //2字节长度组成的UTF-8编码数据,第1个字节是用来确定模板类型的,第1个字节右移4位以后,第1个字节的取值范围为12~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;

2.1.2、Java中byte类型的有符号整数的表示方式

上文中的UTF-8编码的数据"中"在byte[] bytearr字节数组中是用该字节数组的[0,3)索引位置(左闭右开)的字节来表示的,这3个字节分别是-28,-72,-23,分别对应的8位二进制表示为:1110 0100、1011 1000、1010 1101,如下所示:

这种表示方式是因为Java中的有符号整数,是使用二进制补码的方式来表示的,-28的二进制补码的计算步骤分为以下3步:

①、先求 +28 的二进制,结果如下所示:

复制代码
0001 1100

②、再求反码(按位取反),如下所示:

复制代码
1110 0011

③、对步骤②中的反码 +1 ,得到补码,如下所示:

复制代码
1110 0100(最高位为 1,表示负数)

可以使用代码进行验证,如下所示:

复制代码
public class Main {
    public static void main(String[] args) {
        byte a = -28;
        System.out.println(Integer.toBinaryString(a & 0xFF));// 输出:11100100
    }
}

上述代码的运行结果如下:

-72,-23同理,在Java中也是使用补码表示的,此处不再赘述。