JAVA-NIO(1)

本系列博客转载自:海纳的知乎专栏
https://www.zhihu.com/people/hinus/activities
JAVA NIO系列,本人补充一些“作业”
nio(1):buffer、
https://zhuanlan.zhihu.com/p/27296046
作业:查看 clear, rewind, remaining, isRemaining等方法的实现,并理解。

本节课是小密圈《进击的Java新人》第十六周第一节课。从这节课开始,我会连续地介绍一下Java中的nio库。
nio中包含了很多东西,我个人认为最核心的是selector,那里我会使用大量的篇幅去介绍。但在那之前,我们还是从最简单的东西入手。今天只讲一下buffer。

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

缓冲区基础

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

容量(Capacity) 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit) 缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。
位置(Position) 下一个要被读或写的元素的位置。Position会自动由相应的 get( )和 put( )函数更新。
标记(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从读变成写了: (转注:这里应该是写buffer模式切换到读buffer模式)
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等方法的实现,并理解。

==========================================================================
下面开始做作业:
以上方法都来自java.nio.Buffer抽象类

clear 方法作用是把缓冲区的标识重置,里边的内容并不真正清空。
 public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }



rewind方法用于反复读取某一段缓冲区,比如:
out.write(buf);    // Write remaining data
buf.rewind();      // Rewind buffer
buf.get(array);  

将缓冲区内容写入通道后,position的位置从0变到了写入字节数的位置,然后调用buf.rewind();后,position=0,mark=-1;表示这块缓冲区你又可以读去了(向channel中写)。和clear的唯一区别是limit位置不变(rewind方法要求limit已经被正确设定了),clear会将limit位置更新为容量。
源代码:
public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }



remaining方法用于得到缓冲区剩余多少元素未处理,返回的是缓冲中limit-position的值。
public final int remaining() {
        return limit - position;
    }


hasRemaining方法 (我的jdk1.7是hasRemaining,没有isRemaining,原作者应该是笔误)
该方法用于判断缓冲中是否有元素未处理。如果limit>position表示还有元素,否则表示都已经处理过了。
public final boolean hasRemaining() {
        return position < limit;
    }



除了上面的这些方法还有一个比较常用的方法compact()
该方法的作用是未读取过得数据拷贝到缓冲区的最开始。然后position设置成这些元素的个数的位置,用于后边写缓冲区。经常应用的场景是,一个缓冲区,读取了一部分数据之后呢,又想往这个缓冲区写数据,为了不破坏未读取的数据,需要前移未读取数据,在这些数据后面再写入新数据。

这个方法在ByteBuffer是抽象方法,在其子类中实现。

HeapByteBuffer.java

public ByteBuffer compact() {
        //本地方法,用于拷贝数组,可以看出将剩余元素拷贝到自身开始位置
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        //position设置成剩余元素个数位置
        position(remaining());
        //可写的范围,设置成容量
        limit(capacity());
        //是上一次的读写位置mark无效。
        discardMark();
        return this;

    }


第一节课的作业做完了,留着以后复习。

下一篇: http://zxp209.iteye.com/admin/blogs/2396809

猜你喜欢

转载自zxp209.iteye.com/blog/2396769