Java NIO源码剖析及使用实例(一):Buffer

来源:互联网 发布:mac imovie不见了 编辑:程序博客网 时间:2024/04/30 16:39

现在越来越多的公司开始使用NIO,面试中也经常被问到NIO的知识,这里给大家介绍下,包括基本使用方法以及一些实现原理等,因为NIO知识较多,会分多篇介绍。

首先来说NIO是做什么的,Java中NIO大家可以理解为new io,即新出的一个处理io流的包,它最重要的几个特性就是Channel(管道)、Buffer(缓冲区)、Selector(选择器)。相较与IO,NIO新增了缓冲区以及非阻塞读取等特效,今天首先来看看Buffer。

首先我们直接通过读源码来学习下Buffer,后面再举出具体使用的例子,首先我们看下Buffer的几个属性:

// Invariants: mark <= position <= limit <= capacityprivate int mark = -1;private int position = 0;private int limit;private int capacity;

position
首先我们看position,有助于后面理解mark。position故名思议就是记录当前的位置,他的几个重要方法是:

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

nextGetIndex方法其实在读取buffer里数据的时候取出position的值之后自增或者增加所需步长,即取出当前位置,再由具体的Buffer实现取出position对应的具体值来达到一个流式的读取效果。nextPutIndex是同样的原理,只是其应用于写数据的时候

mark
mark及后面几个参数都是继承自Buffer类里的,要了解mark,我们可以看一下Buffer源码里有关于mark值变化的部分:

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

上面两个方法可以看出来,mark的作用,其实就跟它的名字一样,就是通过mark()方法在当前位置做一个标记,然后需要时通过reset()来回到标记的位置,未标记时默认值为-1

limit
limit其实就是记录具体的读写数量,取值时受限于limit。我们在使用Buffer时会先指定Buffer缓冲区大小,比如我们指定5个byte,但是读取时实际只有3个byte,此时limit值就为3,我们取值时就只会取3个值,而不是总容量大小5个。其重要使用的地方就是上面介绍过的nextGetIndex和nextPutIndex,大家可以看到取下一个位置时是要跟limit做比较的,它还有个重要方法是:

public final boolean hasRemaining() {    return position < limit;}

这个方法其实就是获取是否还有可读取的值,即通过当前读取到的位置和limit的值来判断

capacity
capacity就是我们缓冲区的总大小。

接下来我们看个具体实例来看下如何使用Buffer,且顺带看下它的常用方法:

private static void testChannel() throws IOException {    RandomAccessFile accessFile = new RandomAccessFile("d://testNIO.txt" , "rw");    FileChannel channel = accessFile.getChannel();    ByteBuffer byteBuffer = ByteBuffer.allocate(3);    int readBytes = channel.read(byteBuffer);    while (readBytes != -1){        byteBuffer.flip();        while (byteBuffer.hasRemaining()){            System.out.printf((char)byteBuffer.get() + "");        }        byteBuffer.clear();        readBytes = channel.read(byteBuffer);    }    accessFile.close();}

testNIO.txt文件内容:

aaaccbbacccadbac

执行结果如下:

aaaccbbacccadbac

这就是一个简单的读取文件的例子,对于其中几个大家可能不太了解的方法下面我们通过源码来分析下具体实现以了解上述代码是如何工作的:
首先此处用到的是ByteBuffer,我们来看下代码中大家没使用过的一些方法:

ByteBuffer.allocate(3)

public static ByteBuffer allocate(int capacity) {    if (capacity < 0)        throw new IllegalArgumentException();    return new HeapByteBuffer(capacity, capacity);}HeapByteBuffer(int cap, int lim) {            // package-private    super(-1, 0, lim, cap, new byte[cap], 0);}ByteBuffer(int mark, int pos, int lim, int cap,   // package-private             byte[] hb, int offset){    super(mark, pos, lim, cap);    this.hb = hb;    this.offset = offset;}

通过上面可以看到,allocate(3)方法其实就是new了一个mark=-1,pos=0,limit=3,capacity=3,hb=new Byte[3]的这么一个初始HeapByteBuff缓冲区。

channel.read(byteBuffer)
这里只能看到read方法,再里面的具体实现看不到,这里就不多讲了,这个方法一看也就知道其实就是通过管道将数据读取到缓冲区。

byteBuffer.flip()

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

从源码可以看出来,flip方法其实就是将limit置为当前position值,即表示读取了多少数据,以及将position归0,这样后面我们才能读取数据。

byteBuffer.get()

public byte get() {    return hb[ix(nextGetIndex())];}final int nextGetIndex() {                          // package-private    if (position >= limit)        throw new BufferUnderflowException();    return position++;}protected int ix(int i) {    return i + offset;}

从源码可以看出,get方法其实就是去除hb数组中当前position的值,且将position加1以读取后续值,这就是为什么读取数据时需要先调用flip方法将position值归0的原因。

byteBuffer.clear()

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

看源码可以看出clear方法其实就是将Buffer中的几个属性重新初始化了,此处并没有清空缓存区的值,网上有人说此处会清空缓存区的值是错误且不负责任的!我们通过以下一个例子可以看出来其实是没有清空值的:

将testNIO.txt内容改为如下:

aaaccdd

然后去掉上面代码中的byteBuffer.flip(),执行原方法,结果如下:

cd

这是为什么呢?是因为缓冲区第一次读取数据时存放的是aaa,第二次覆盖掉第一次的,值为ccd,第三次读取时只有一个d值覆盖掉原来的ccd中的第一个c,此时缓冲区的值为dcd,我们去掉flip方法后再来读取数据,此时position为1,limit为3,因此会读出后面的cd值打印到控制台,所以说clear方法是不会清除缓存区的值的

我的博客:blog.scarlettbai.com
我的公众号:读书健身编程

1 0