Java-NIO之缓冲区
通道 和 缓冲区 是 NIO 中的核心对象。数据从通道读入缓冲区,然后从缓冲区写入到通道中。缓冲区本质上是一块可以写入/读取数据的内存
。这块内存被包装成NIO Buffer
对象,并提供了一组方法,用来方便的访问该块内存。
缓冲区
缓冲区 (Buffer) 是一个对象, 它包含一些要写入或者刚读出的数据。在 NIO 库中,所有数据都是用缓冲区处理的在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组,它提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
缓冲区组件
状态变量是缓冲区的内部统计机制的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。
在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()
状态变量
可以用三个值指定缓冲区在任意时刻的状态: position
limit
capacity
Position
缓冲区实际上就是美化了的数组, position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。例如,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。Limit
limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position 总是小于或者等于 limit。Capacity
缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。limit 决不能大于 capacity。事例说明
假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示:
回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。
position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示:
由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。
- 第一次读取
现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示:
limit 没有改变。
- 第二次读取
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2:
- limit 没有改变。
flip
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:
- 它将 limit 设置为当前 position。
- 它将 position 设置为 0。
前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区:
我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。
- 第一次写入
在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4,而 limit 不变,如下所示:
- 第二次写入
我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5,并保持 limit 不变,如下所示:
clear
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情:
- 它将 limit 设置为与 capacity 相同。
- 它设置 position 为 0。
下图显示了在调用 clear() 后缓冲区的状态:
缓冲区API
让我们来看一下可以如何使用一个缓冲区和操作缓冲区。
1 | package java.nio; |
些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:1
2
3
4
5buffer.mark( );
buffer.position(5);
buffer.reset( );
##可简写 buffer.mark().position(5).reset( );
常见定义
存取
数据保存到磁盘,先将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。
get() 方法
ByteBuffer 类中有四个 get() 方法:- byte get();
- ByteBuffer get( byte dst[] );
- ByteBuffer get( byte dst[], int offset, int length );
- byte get( int index );
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 );
如果存取中的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位置。
翻转
在缓冲区填充满以后,我们需要通知使用者使用,即读取缓冲区的数据,那怎么知道我们要读取那些数据呢?
- limit的引入,就是解决这个问题的,通过一个flip()方法进行翻转。limit=position,position=0。这样就可以从position读取到limit。即buffer.limit(buffer.position()).position(0) 效果等于buffer.flip();
- 还有一个函数和flip类似,rewind,这个函数不影响limit,只是把position重置为0,意思就是重读。如果连续两次flip,那么我们得到的就是一个position和limit都为0的缓冲区,对这样的缓冲区使用get,是会抛出BufferUnderflowException异常。
释放
缓冲区的基本操作都有了。我们可以循环读取缓冲区的数据,但是如何知道到了上界呢?
函数 hasRemaining() 和 remaining()就是这样的函数。我们可以这样使用:1
2
3
4
5
6
7
8
9
10int 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。
- compact函数。这个压缩就是把已经读过的数据抛弃,使用后面的数据覆盖(移动至索引0),并且把limit设置为capacity。
- 压缩类似于先进先出的FIFO队列。压缩完成后,注意position移动到了未处理数据之后,等待继续填充。position也就是移动的元素的个数。
这里比较不好理解。如果画个图,可能就容易理解了。
- 说明:
数据元素 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
10public 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。
两个缓冲区被认为相等的充要条件是:
- 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
- 两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相
同。 - 在每个缓冲区中应被 Get()函数返回的剩余数据元素序列必须一致。
- 说明了两个属性不同的缓冲区也可以相等。可能看起来是完全相同的缓冲区,但测试时会发现并不相等,如下图。
注意除了equals还有compareTo函数。compareTo函数必须是两个相同的缓冲区,否则会抛出异常ClassCastException,而equals只是返回false。
批量移动
对于之前的单个元素赋值,可能觉得略显繁琐。这里提供了一些批量移动操作1
2
3
4
5
6
7
8
9
10
11public 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);
}