nio(1):buffer

本节课是小密圈《进击的Java新人》第十六周第一节课。从这节课开始,我会连续地介绍一下Java中的nio库。

nio中包含了很多东西,我个人认为最核心的是selector,那里我会使用大量的篇幅去介绍。但在那之前,我们还是从最简单的东西入手。今天只讲一下buffer。

在没有使用nio之前,我们只能自己维护一个byte数组或者是char数组来进行批量读写,或者使用BufferedReader,BufferedInputStream来做读写缓冲。在nio里,就可以使用buffer了。学习使用这个buffer要有点耐心,彻底了解它的机制再去用,这玩意的设计不是很友好。我在第一次用的时候,也是发现它的接口各种不符合期望,只好沉下心来一点点把它的原理搞清楚了才会用的。所以,学习这一课,一定不能焦躁。

缓冲区基础

本质上,缓冲区是就是一个数组。所有的缓冲区都具有四个属性来提供关于其所包含的数组的信息。它们是:

  1. 容量(Capacity) 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。
  2. 上界(Limit) 缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。
  3. 位置(Position) 下一个要被读或写的元素的位置。Position会自动由相应的 get( )和 put( )函数更新。
  4. 标记(Mark) 一个备忘位置。用于记录上一次读写的位置。一会儿,我会通过reset方法来说明这个属性的含义。

我们以字节缓冲区为例,ByteBuffer是一个抽象类,不能直接通过 new 语句来创建,只能通过一个static方法 allocate 来创建:

ByteBuffer byteBuffer = ByteBuffer.allocate(256);

以上的语句可以创建一个大小为256字节的ByteBuffer,此时,mark = -1, pos = 0, limit = 256, capacity = 256。capacity在初始化的时候确定了,运行时就不会再变化了,而另外三个变量是随着程序的执行而不断变化的。

缓冲区的存取

缓冲区用于存取的方法定义主要是put , get。

put 方法有多种重载,我们看其中一个:

    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }

    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

这个方法是把一个byte变量 x 放到缓冲区中去。position会加1。再来看一下get方法,也是从position的位置去取缓冲区中的一个字节:

    public byte get() {
        return hb[ix(nextGetIndex())];
    }

    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }

那比如,我要是想在一个Buffer中放入了数据,然后想从中读取的话,就要把position调到我想读的那个位置才行。为此,ByteBuffer上定义了一个方法:

    public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }

这里面用到了limit,想一下上面的定义,limit代表可写或者可读的总数。一个新创建的bytebuffer,它可写的总数就是它的capacity。如果写入了一些数据以后,想从头开始读的话,这时候的limit应该就是当前ByteBuffer中数据的总长度。下面的这个图比较直观地说明了这个问题:


为了达到从写数据的情况变成读数据的情况,还需要修改limit,这就要用到limit方法:

    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }

我们可以这样写,就把byteBuffer从读变成写了:

byteBuffer.limit(byteBuffer.position())
byteBuffer.position(0);

当然,由于这个操作非常频繁,jdk就为我们封装了一个这样的方法,叫做flip:

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

使用这个反转方法,思路一定要清晰,稍有不慎,就会带来莫名其妙的错误,比如,连续调用flip会对ByteBuffer有什么样的影响呢?这其实会使得Buffer的limit变成0,从而既不能读也不能写了。

limit的设计确实可以加速数据溢出情况的检查,但是造成使用上和理解上的困难,我还是觉得得不偿失。我一直觉得limit这个设计很蠢(个人意见,如果有误,请各位指正)。

缓冲区标记

今天最后一个内容,是讲一下mark的作用。在理解了position的作用以后,mark就很容易理解了,它就是记住当前的位置用的:

    public final Buffer mark() {
        mark = position;
        return this;
    }

我们在调用过mark以后,再进行缓冲区的读写操作,position就会发生变化,为了再回到当初的位置,我们可以调用reset方法恢复position的值:

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

好了。今天的内容就这些了。作业:

查看 clear, rewind, remaining, isRemaining等方法的实现,并理解。

上一节课:线程池的结构和原理

下一节课:nio(2):channel

目录:课程目录

编辑于 2017-06-12

文章被以下专栏收录