JDK的BIO类解读

说到IO就要对应到输入和输出,字节流和字符流,字符流也是通过字节流来实现的,我们来慢慢分析。


先说说字节流的输入类,输入我们就要拿到输入流来读取数据,对应到java中,java.io.InputStream这个类是一个抽象类,定义了字节输入的基本的方法,这个类

中有三个read方法,如下所示

public int read(byte b[]) throws IOException {

        return read(b, 0, b.length);


}

public int read(byte b[], int off, int len) throws IOException {}

public abstract int read() throws IOException;

三个抽象方法中第一个抽象方法是调用第二个抽象方法实现的,阅读第二个抽象方法的代码,这个方法的主要逻辑就是从byte数组的偏移量off开始读取len长度哥字节放入到byte数组中然后返回读取到的长度,他是一个字节一个字节循环读取的,那么一个字节怎么读取呢,他调用的时另一个read方法,也就是上面的第三个read方法,可以看到第三个read方法是一个抽象的方法,没有实现,那由谁来实现呢,自然是由子类来实现了,这个read方法当读取到末尾的时候就返回-1,java.io.InputStream类只是一个抽象,定义一些公共的方法,将具体的读取目标进行抽象,具体是从哪里读,磁盘还是网络由子类实现,我们接下来分析一下他的一个子类java.io.FileInputStream,见名知意,他就是读取本地磁盘的文件,FileInputStream继承了InputStream,我们看看他的read方法,代码如下:

    public int read() throws IOException {

        Object traceContext = IoTrace.fileReadBegin(path);

        int b = 0;

        try {

            b = read0();

        } finally {

            IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);

        }

        return b;


    }

看到他是调用另一个read0方法是实现的,再看看read0方法:

private native int read0() throws IOException;

这是一个native的方法,读取本地磁盘文件的这个操作肯定不是我们JVM这个应用进程该干的事情,肯定是由操作系统来发起的,所以这个方法就发起了一个系统调用,这个过程可能需要一段时间来完成,这是一个阻塞的方法,在进行系统调用的这段时间里我们的这个线程会被挂起,不会获得CPU的执行时间,操作系统调用磁盘硬件厂商提供的驱动读取磁盘文件,将读取完的数据放入到操作系统内核的内存中,读取完成后再将数据移动到我们JVM进程的内存中,这就发生了一次从操作系统内核空间到我们JVM内存空间的一次内存拷贝。这段过程也是阻塞进行的,可以看到一次IO操作还是很耗费时间的,因为有很多的阻塞存在,这样子一个基于流的读取操作就完成了。

假如我们有一个file.txt文件,用UTF-8来编码的,我们在里面输入一个字符a,然后测试一下读取,测试代码如下:

public static void main(String[] args) throws Exception{

File file = new File("/Users/yuanzq/Desktop/work/liequ/io/src/bio/file.txt");

FileInputStream fis = new FileInputStream(file);

int b ;

while((b = fis.read()) != -1){

System.out.println((byte)b);

}

fis.close();


}

输出结果为:97

可见字符a一个字节来表示的,97正好是小写字母a的ascii码值,接着测试输入一个汉字’啊’,输出结果为:

-27

-107

-118

可见汉字’啊‘UTF-8是用三个字节来表示的,至于字符的处理等下我们分析Reader类的时候会详细说明。

上面测试的磁盘文件的IO,下面来看一下网络IO,java.io.FileInputStream有一个子类java.net.SocketInputStream,我们在编写一个网络应用程序的时候读取数据会调用socket.getInputStream()拿到InputStream对象来处理输入,自己跟一下代码就会发现返回的就是这个java.net.SocketInputStream对象,这个类又4个read方法

public int read() throws IOException

public int read(byte b[]) throws IOException

public int read(byte b[], int off, int length) throws IOException

int read(byte b[], int off, int length, int timeout) throws IOException

第一个read方法就是对InputStream的抽象的read方法实现,他调用了第三个read方法,而第二个和第三个read方法会最后调用到第四个read方法上,阅读第四个read方法的代码,会发现最后调用的是一个native的socketRead0方法,将byte数组传过去,这个方法就发起了一次系统调用和文件IO类似的阻塞由操作系统完成。


private native int socketRead0(FileDescriptor fd,

                               byte b[], int off, int len,

                               int timeout)throws IOException;


看来不论是磁盘IO还是网络IO对于我们乱说原理都是一样的,我们这里说的是输入,输出肯定也是类似的.

OutputStream定义了抽象的write方法

public abstract void write(int b) throws IOException;


FileOutputStream:

private native void write(int b, boolean append) throws IOException;


FileOutputStream的子类SocketOutputStream:


    public void write(int b) throws IOException {

        temp[0] = (byte)b;

        socketWrite(temp, 0, 1);


    }

private native void socketWrite0(FileDescriptor fd, byte[] b, int off,


                                     int len) throws IOException;





下面我们来看看JDK中的字符流的实现,对于输入来说有一个抽象类java.io.Reader这个类和InputStream类相似的定义了读取的抽象,他的read方法代码如下:

    public int read() throws IOException {

        char cb[] = new char[1];

        if (read(cb, 0, 1) == -1)

            return -1;

        else

            return cb[0];


    }


    public int read(char cbuf[]) throws IOException {

        return read(cbuf, 0, cbuf.length);


    }


   public int read(java.nio.CharBuffer target) throws IOException {

        int len = target.remaining();

        char[] cbuf = new char[len];

        int n = read(cbuf, 0, len);

        if (n > 0)

            target.put(cbuf, 0, n);

        return n;


    }


abstract public int read(char cbuf[], int off, int len) throws IOException;


最终调用的都是最后的这个abstract的read方法,这个方法由子类来实现,注意看他们的参数已经不是byte了,而变成了char,下面来看一下他的子类,InputStreamReader和FileReader,其中FileReader是InputStreamReader的子类, FileReader的代码如下所示:

  

    public FileReader(String fileName) throws FileNotFoundException {

        super(new FileInputStream(fileName));

    }


    public FileReader(File file) throws FileNotFoundException {

        super(new FileInputStream(file));

    }


    public FileReader(FileDescriptor fd) {

        super(new FileInputStream(fd));


    }


可以看出FileReader只是一个桥梁,他就像InputStreamReader字符输入流和InputStream字节输入流建立了一个桥梁而已。实际的读取实现方法封装在InputStreamReader类中

InputStreamReader也是通过StreamDecoder包装的InputStream,只是读取字节字节转换为一个字符,实际的转化要根据设置的编码类型,InputStreamReader类的构造方法如下所示,他初始化了一个StreamDecoder的本地变量:

    public InputStreamReader(InputStream in) {

        super(in);

        try {

            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object

        } catch (UnsupportedEncodingException e) {

            // The default encoding should always be available

            throw new Error(e);

        }


    }

由此猜测InputStreamReader的read方法的实现应该也是调用这个StreamDecoder类实现的,看看源码

    public int read(char cbuf[], int offset, int length) throws IOException {

        return sd.read(cbuf, offset, length);


    }


    public int read() throws IOException {

        return sd.read();


    }

果然如此,那么StreamDecoder就变成了一个很重要的类,StreamDecoder也继承了Reader这个抽象类,下面来分析这个类,forInputStreamReader的代码如下所示:


    public static StreamDecoder forInputStreamReader(InputStream in,

                                                     Object lock,

                                                     String charsetName)

        throws UnsupportedEncodingException

    {

        String csn = charsetName;

        if (csn == null)

            csn = Charset.defaultCharset().name();

        try {

            if (Charset.isSupported(csn))

                return new StreamDecoder(in, lock, Charset.forName(csn));

        } catch (IllegalCharsetNameException x) { }

        throw new UnsupportedEncodingException (csn);


    }


首先创建了一个默认的字符Charset对象然后返回StreamDecoder对象,这个对象中真正包装了InputStream输入流对象,假设我们现在取到的charsetName是UTF-8,局部变量csn的值就为UTF-8,Charset.forName(csn)创建了一个Charset对象,我们跟下源码Charset.forName方法:


    public static Charset forName(String charsetName) {

        Charset cs = lookup(charsetName);

        if (cs != null)

            return cs;

        throw new UnsupportedCharsetException(charsetName);


    }


他调用了lookup方法,继续跟踪:

    private static Charset lookup(String charsetName) {

        if (charsetName == null)

            throw new IllegalArgumentException("Null charset name");


        Object[] a;

        if ((a = cache1) != null && charsetName.equals(a[0]))

            return (Charset)a[1];

        // We expect most programs to use one Charset repeatedly.

        // We convey a hint to this effect to the VM by putting the

        // level 1 cache miss code in a separate method.

        return lookup2(charsetName);


    }

首先从cache1中提取,这个cache1是什么呢,是什么时候初始化的呢:

private static volatile Object[] cache1 = null; // "Level 1” cache

    private static void cache(String charsetName, Charset cs) {

        cache2 = cache1;

        cache1 = new Object[] { charsetName, cs };


    }

我们只找到cache这个方法对cache1进行了初始化,所以猜测他应该是查询完成之后初始化的,第一次调用肯定是null,那么从lookup就接着往后走,到了lookup2方法,代码如下:

    private static Charset lookup2(String charsetName) {

        Object[] a;

        if ((a = cache2) != null && charsetName.equals(a[0])) {

            cache2 = cache1;

            cache1 = a;

            return (Charset)a[1];

        }


        Charset cs;

        if ((cs = standardProvider.charsetForName(charsetName)) != null ||

            (cs = lookupExtendedCharset(charsetName))           != null ||

            (cs = lookupViaProviders(charsetName))              != null)

        {

            cache(charsetName, cs);

            return cs;

        }


        /* Only need to check the name if we didn't find a charset for it */

        checkName(charsetName);

        return null;

    }

先从cache2中取,cache2又是什么的,看定义是第二个级别的缓存,和cache1一样都是使用一个数组实现的,也都是调用完成后初始化的,看上面代码的逻辑如果从cache2中匹配到了要查找的字符对象那么就cache1和cache2交换,这样子便提高了查找的速度,下次查找同样的字符集就lookup中即可完成:

private static volatile Object[] cache2 = null; // "Level 2" cache


这里第一次访问肯定都是null,接着往下走,看接下来这段代码:

if ((cs = standardProvider.charsetForName(charsetName)) != null ||

            (cs = lookupExtendedCharset(charsetName))           != null ||

            (cs = lookupViaProviders(charsetName))              != null)

调用了三个方法查找字符集,




找到之后就返回了一个Charset对象,回到StreamDecoder类继续跟踪他的构造方法,第一个构造方法通过前面创建好的Charset创建了另一个很重要的对象CharsetDecoder,接着调用下一个构造方法,最终的构造方法将创建好的CharsetDecoder,Charset对象保存为类变量


    StreamDecoder(InputStream in, Object lock, Charset cs) {

        this(in, lock,

         cs.newDecoder()

         .onMalformedInput(CodingErrorAction.REPLACE)

         .onUnmappableCharacter(CodingErrorAction.REPLACE));


    }



    StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {

        super(lock);

        this.cs = dec.charset();

        this.decoder = dec;


        // This path disabled until direct buffers are faster

        if (false && in instanceof FileInputStream) {

        ch = getChannel((FileInputStream)in);

        if (ch != null)

            bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);

        }

        if (ch == null) {

        this.in = in;

        this.ch = null;

        bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);

        }

        bb.flip();                      // So that bb is initially empty


    }

接下来的通过注释可以看出是不会走的路径,紧着像下走ch对象肯定是null的,就将InputStream对象保存到为实例变量(这里很重要,我们获取字节数据靠的就是这个InputStream对象),将this.ch设置为空,接着对创建一个8M空间的ByteBuffer对象保存为实例变量作为缓冲区。


接下来看看CharsetDecoder的几个read方法InputStreamReaader这个类的几个read方法都是调用的StreamDecoder来实现的,StreamDecoder的read方法如下,其他的read方也都是调用这个read方法实现的,下面我们就来分析这个read方法,offset代表偏移量就是从cbuf数组的哪个位置开始读,length代表要读取得字符长度

    public int read(char cbuf[], int offset, int length) throws IOException {

        int off = offset;

        int len = length;

        synchronized (lock) {

            ensureOpen();

            if ((off < 0) || (off > cbuf.length) || (len < 0) ||

                ((off + len) > cbuf.length) || ((off + len) < 0)) {

                throw new IndexOutOfBoundsException();

            }

            if (len == 0)

                return 0;


            int n = 0;


            if (haveLeftoverChar) {

                // Copy the leftover char into the buffer

                cbuf[off] = leftoverChar;

                off++; len--;

                haveLeftoverChar = false;

                n = 1;

                if ((len == 0) || !implReady())

                    // Return now if this is all we can produce w/o blocking

                    return n;

            }


            if (len == 1) {

                // Treat single-character array reads just like read()

                int c = read0();

                if (c == -1)

                    return (n == 0) ? -1 : n;

                cbuf[off] = (char)c;

                return n + 1;

            }


            return n + implRead(cbuf, off, off + len);

        }

    }


进入synchronized锁,他所住的对象就是当前创建的InputStreamReader对象,在StreamDecoder的构造函数中调用父类Reader来持有的。经过一系列的检查之后,判断了一个boolean类型的haveLeftoverChar的值,默认第一次调用这个值肯定是false的,注意StreamDecoder有一个无参数的read()方法,这个方法代表的就是读取一个两个字节字符,他调用了read0方法,它的代码如下:

private int read0() throws IOException {

        synchronized (lock) {


            // Return the leftover char, if there is one

            if (haveLeftoverChar) {

                haveLeftoverChar = false;

                return leftoverChar;

            }


            // Convert more bytes

            char cb[] = new char[2];

            int n = read(cb, 0, 2);

            switch (n) {

            case -1:

                return -1;

            case 2:

                leftoverChar = cb[1];

                haveLeftoverChar = true;

                // FALL THROUGH

            case 1:

                return cb[0];

            default:

                assert false : n;

                return -1;

            }

        }

    }

他定义了一个两个字符的数组,调用上面所述的带有三个参数的read方法来实现读取,当读取的到的字符是两个字节的时候就将这个haveLeftoverChar赋值为true,将leftoverChar变量赋值为读取到的第二个字节,根据这些猜测这个boolean类型的值应该是为了兼容先前已经调用过这个read0方法的代码里面读取到的数据是两个字节的情况。


接下来继续分析,如果读取的长度是1,那么就调用上面所说的read0方法来实现,接下来的代码很重要:

return n + implRead(cbuf, off, off + len);

他调用了implRead方法,传入的参数分别是,字符数组,偏移量,偏移量+要读取的长度,这个方法的代码比较长,就不贴出来了,慢慢分析:

首先将字符数组包装成了一个CharBuffer对象,接着进入了一个自选的方法,首先调用创建的CharsetDecoder的decode方法解码处理目前有的字节数据,接着调用了一个readBytes方法,这个方法就是真正的调用InputStream对象的read方法读取数据了:

            int lim = bb.limit();

            int pos = bb.position();

            assert (pos <= lim);

            int rem = (pos <= lim ? lim - pos : 0);

            assert rem > 0;

            int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);

            if (n < 0)

                return n;

            if (n == 0)

                throw new IOException("Underlying input stream returned zero bytes");

            assert (n <= rem) : "n = " + n + ", rem = " + rem;


            bb.position(pos + n);


他会尽力去读取,直到将构造方法中创建的这个8M的ByteBuffer读满或者读取到了结尾,然后返回,看到这里豁然开朗,原来最终还是调用的InputStream方法处理的,读取完之后返回,剩下的就是返回的StreamDecoder方法中去将这些字节的数据转换为字符了。





说完了Reader,那么猜测Writer应该也是一样的逻辑,OutputStreamWriter类中包装了StreamEncoder对象将字符编码成一个个字节到一个ByteBuffer中最后还是调用的OutputStream的write方法实现的。

















































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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值