Java-NIO之缓冲区

  通道缓冲区 是 NIO 中的核心对象。数据从通道读入缓冲区,然后从缓冲区写入到通道中缓冲区本质上是一块可以写入/读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

缓冲区

  1. 缓冲区 (Buffer) 是一个对象, 它包含一些要写入或者刚读出的数据。在 NIO 库中,所有数据都是用缓冲区处理的在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组,它提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程

  2. 最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

缓冲区组件

  1. 状态变量是缓冲区的内部统计机制的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。

  2. 在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()

状态变量

可以用三个值指定缓冲区在任意时刻的状态: position limit capacity

  1. Position
    缓冲区实际上就是美化了的数组, position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。例如,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。
    同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。

  2. Limit
    limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position 总是小于或者等于 limit。

  3. Capacity
    缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。limit 决不能大于 capacity。

  4. 事例说明
    假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示:

初始化为8个字节

回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点

变量状态

position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示:

Position=0

由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。

  • 第一次读取

现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示:

Position+=3

limit 没有改变。

  • 第二次读取

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2:

Position+=2

  • limit 没有改变。

flip

现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:

  1. 它将 limit 设置为当前 position。
  2. 它将 position 设置为 0。

前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区:

Buffer=0,limit=5

我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多

  • 第一次写入

在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4,而 limit 不变,如下所示:

Position +=4, limit=5

  • 第二次写入

我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5,并保持 limit 不变,如下所示:

Position=5, limit=5

clear

最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情

  1. 它将 limit 设置为与 capacity 相同。
  2. 它设置 position 为 0。

下图显示了在调用 clear() 后缓冲区的状态:
clear后状态

缓冲区API

让我们来看一下可以如何使用一个缓冲区和操作缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package java.nio;
public abstract class Buffer {
public final int capacity();
public final int position();
public final Buffer position(int newPositio);
public final int limit()
public final Buffer limit(int newLimit);
public final Buffer mark();
public final Buffer reset();
public final Buffer clear();
public final Buffer flip();
public final Buffer rewind();
public final int remaining();
public final boolean hasRemaining();
public abstract boolean isReadOnly();
}

些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:

1
2
3
4
5
buffer.mark( );
buffer.position(5);
buffer.reset( );

##可简写 buffer.mark().position(5).reset( );

常见定义

存取

  数据保存到磁盘,先将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。

  1. get() 方法
    ByteBuffer 类中有四个 get() 方法:

    • byte get();
    • ByteBuffer get( byte dst[] );
    • ByteBuffer get( byte dst[], int offset, int length );
    • byte get( int index );
  2. put()方法
    ByteBuffer 类中有五个 put() 方法:

    • ByteBuffer put( byte b );
    • ByteBuffer put( byte src[] );
    • ByteBuffer put( byte src[], int offset, int length );
    • ByteBuffer put( ByteBuffer src );
    • ByteBuffer put( int index, byte b );
  3. 如果存取中的index参数超出限制,那么,是会抛出IndexOutOfBoundsException。如果是put超出,那么将抛出BufferOverflowException异常。

填充

  填充主要使用put方法。这里的put要注意的是,如果是ByteBuffer不能这样使用buffer.put(‘H’)。因为我们put的并不是char类型,而必须是byte,所以要强制转换 buffer.put((byte)’H’); 还有,put可以使用索引下标。例如:buffer.put(0,(byte)’H’)。如果不适用下标索引,那么put到的都是放在position位置。

翻转

  在缓冲区填充满以后,我们需要通知使用者使用,即读取缓冲区的数据,那怎么知道我们要读取那些数据呢?

  1. limit的引入,就是解决这个问题的,通过一个flip()方法进行翻转limit=position,position=0。这样就可以从position读取到limit。即buffer.limit(buffer.position()).position(0) 效果等于buffer.flip();
  2. 还有一个函数和flip类似,rewind,这个函数不影响limit,只是把position重置为0,意思就是重读。如果连续两次flip,那么我们得到的就是一个position和limit都为0的缓冲区,对这样的缓冲区使用get,是会抛出BufferUnderflowException异常。

释放

缓冲区的基本操作都有了。我们可以循环读取缓冲区的数据,但是如何知道到了上界呢?
函数 hasRemaining() 和 remaining()就是这样的函数。我们可以这样使用:

1
2
3
4
5
6
7
8
9
10
int i = 0;
while(buffer.hasRemaining()){
array[i++] = buffer.get();
...
}
or
int count = buffer.remaining();
for(int i = 0 ; i < count; i++){
array[i] = buffer.get();
}

当然这样的效率都不会很高。 注意:缓冲区并不保证并发。
当我们使用完缓冲区,就可以清空了。使用 clear函数。
clear函数并不改变元素。只是将 limit = capacity; position=0而已。

压缩

  有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0。

  1. compact函数。这个压缩就是把已经读过的数据抛弃,使用后面的数据覆盖(移动至索引0),并且把limit设置为capacity。
  2. 压缩类似于先进先出的FIFO队列。压缩完成后,注意position移动到了未处理数据之后,等待继续填充。position也就是移动的元素的个数。
    这里比较不好理解。如果画个图,可能就容易理解了。

压缩前的buffer
压缩后的buffer

  • 说明:

    数据元素 2-5 被复制到 0-3 位置。位置 4 和 5 不受影响,
    但现在正在或已经超出了当前位置,因此是“死的”。它们可以被之后的 put()调用重写。
    还要注意,位置已经被设为被复制的数据元素的数目。也就是说,缓冲区现在被定位在缓> 冲区中最后一个“存活”元素后插入数据的位置。
    最后,上界属性被设置为容量的值,因此缓冲区可以被再次填满。调用 compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪

标记

  标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在 mark()函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset()函数将位置设为当前的标记值。如果标记值未定义,调用 reset()将导致 InvalidMarkException 异常.(一些缓冲区函数会抛弃已经设定的标记rewind()、clear()以及flip()总是抛弃标记。如果新设定的值比当前的标记小,调用limit()或 position()带有索引参数的版本会抛弃标记。)

- eg:
buffer.position(2).mark().position(4);

设有一个标记的缓冲区

一个缓冲区位置被重设为标记

比较

  这里缓冲区的比较,实际上是比较当前位置,到上界的元素是否相同。

1
2
3
4
5
6
7
8
9
10
public abstract class ByteBuffer extends Buffer implements Comparable{
// This is a partial API listing
public boolean equals (Object ob)
public int compareTo (Object ob)
}

//两个缓冲区可用下面的代码来测试是否相等:
if (buffer1.equals (buffer2)) {
//......
}

如果每个缓冲区中剩余的内容相同,那么 equals( )函数将返回 true,否则返回 false。
两个缓冲区被认为相等的充要条件是:

  1. 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
  2. 两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相
    同。
  3. 在每个缓冲区中应被 Get()函数返回的剩余数据元素序列必须一致。
  4. 说明了两个属性不同的缓冲区也可以相等。可能看起来是完全相同的缓冲区,但测试时会发现并不相等,如下图。
    两个被认为是相等的缓冲区
    两个被认为不相等的缓冲区

注意除了equals还有compareTo函数。compareTo函数必须是两个相同的缓冲区,否则会抛出异常ClassCastException,而equals只是返回false。

批量移动

对于之前的单个元素赋值,可能觉得略显繁琐。这里提供了一些批量移动操作

1
2
3
4
5
6
7
8
9
10
11
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable {
// This is a partial API listing
public CharBuffer get(char [] dst);
public CharBuffer get(char [] dst, int offset, int length);
public final CharBuffer put(char[] src);
public CharBuffer put(char [] src, int offset, int length);
public CharBuffer put(CharBuffer src);
public final CharBuffer put(String src);
public CharBuffer put(String src, int start, int end);
}

这里提供的函数get,就可以获取我们所需的缓冲区中的内容。 buffer.get(myArray)等价于buffer.get(myArray,0,myArray.length) 。
注意,如果数组元素太大,而我们又没有指定大小,意味着缓冲区要填充满数组,但是缓冲区太小,这样,就会抛出BufferUnderflowException异常。对于数组太大太小我们可以这样处理(对于put同理):

1
2
3
4
5
6
7
8
9
10
11
//数组元素太大
char [] bigArray = new char[1000];
int length = buffer.remaining();
buffer.get(bigArray,0,length);

//数组很小
char[] smallArray = new char[10];
while(buffer.hasRemaining()){
int length = Math.min(buffer.remaining(),smallArray.length);
buffer.get(smallArray,0,length);
}

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器