本文主要介绍缓冲区的特点,作用以及基本操作。
一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的 。
- 属性
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:
容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit)
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
标记(Mark)
一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。
这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity
- 分类
下图是 Buffer 的类层次图。在顶部是通用 Buffer 类。Buffer 定义所有缓冲区类型共有的操作,无论是它们所包含的数据类型还是可能具有的特定行为。
- 基本操作
实际上,缓冲区的一些相关操作也是基于改变四个属性来执行的,具体每个方法改变了那些属性,下面会详细介绍(基本操作以ByteBuffer为例)。
1、创建缓冲区
所有的缓冲区类都不能直接使用new关键字实例化,它们都是抽象类,但是它们都有一个用于创建相应实例的静态工厂方法
创建缓冲区有两种方法:allocate()和wrap();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//如果想用一个指定大小的数组作为缓冲区的数据的存储器,可以使用wrap()方法:
byte[] bytes = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
2、存取(get()/put())
Get 和 put 可以是相对的或者是绝对的。相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛出异常。对于 put() ,如果运算会导致位置超出上界,就会BufferOverflowException 异常。对于 get(),如果位置不小于上界,就会抛出BufferUnderflowException 异常。绝对存取不会影响缓冲区的位置属性,但是如果您所提供的索引超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。
3、翻转(flip()读写状态转换)
将处于存数据状态的缓冲区变为处于准备取数据的状态,使用flip()方式翻转。
flip()方法的源码:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
可以看出flip()方法改变了limit和position这两个属性,将上界(limit)置为当前位置,将位置(position)置为0;
还有一个方法rewind()方法与flip()方法相似,但区别在于rewind()方法未改变上界(limit)的属性,源码如下:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
示例:利用flip()方法进行读写状态转换
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//存入数据
buffer.put((byte)'A');
buffer.put((byte)'B');
//翻转
buffer.flip();
//读取二次数据
System.out.println((char)buffer.get());
System.out.println((char)buffer.get());
4、释放(hasRemaining()/remaining())
释放就是将缓冲区中的数据全部取出的过程,可以将缓冲区数据批量放到一个数组中,此处可以用到两个方法:hasRemaining()和remaining():hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界;remaining()函数将告知您从当前位置到上界还剩余的元素数目。例如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] array = new byte[1024];
利用hasRemaining()方法
for(int i = 0;buffer.hasRemaining();i++){
array[i] = buffer.get(i);
}
//利用remaining()方法
int count = buffer.remaining();
for(int i = 0;i < count;i++){
array[i] = buffer.get(i);
}
一旦缓冲区对象完成填充并释放,它就可以被重新使用了。Clear()函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回 0。
5、压缩(complict())
压缩就是将已读取了的数据丢弃,保留未读取的数据并将保留的数据重新填充到缓冲区的顶部,然后继续向缓冲区写入数据;示例说明:
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//存入数据
buffer.put((byte)'A');
buffer.put((byte)'B');
buffer.put((byte)'C');
System.out.println("first:"+new String(buffer.array()));
//翻转
buffer.flip();
//读取二次数据
System.out.println((char)buffer.get());
System.out.println((char)buffer.get());
//压缩
buffer.compact();
System.out.println("压缩后get:"+new String(buffer.array()));
//填充数据
buffer.put((byte)'E');
buffer.put((byte)'F');
//翻转
buffer.flip();
//读取数据
System.out.println("压缩后put:"+new String(buffer.array()));
输出结果:
first:ABC
A
B
压缩后get:CBC
压缩后put:CEF
6、标记(mark())
标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在 mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调用 reset( )将导致 InvalidMarkException 异常。reset()方法和mark()方法的源码:
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
public final Buffer mark() {
mark = position;
return this;
}
7、比较(equals()/compareTo())
两个缓冲区被认为相等的充要条件是:
-
两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
-
两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
-
在每个缓冲区中应被 Get()函数返回的剩余数据元素序列必须一致
缓冲区也支持用 compareTo( )函数以词典顺序进行比较。这一函数在缓冲区参数小于,等于,或者大于引用 compareTo( )的对象实例时,分别返回一个负整数,0 和正整数。
compareTo( )不允许不同对象间进行比较。但 compareTo( )更为严格:如果您传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals( )只会返回false。不像 equals( ),compareTo( )不可交换:顺序问题。
8、批量移动
有两种形式的 get( )可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。第二种形式使用 offset 和 length 参数来指定目标数组的子区间。
byte[] bytes = "hello world".getBytes();
//创建一个与byte数组大小一致的缓冲区buffer
ByteBuffer buffer1 = ByteBuffer.allocate(bytes.length);
//将byte数组写入缓冲区
buffer1.put(bytes);
//翻转缓冲区
buffer1.flip();
//将数据批量读到array中
byte[] arr = new byte[bytes.length];
while(buffer1.hasRemaining()){
buffer1.get(arr,0,buffer1.remaining());
}
//输出从缓冲区读的数据
System.out.println(new String(arr));
写数据到缓冲区时,若bytes.length > buff.capacity()则会抛出java.nio.BufferOverflowException;从缓冲区中读数据时,若array.length < buff.limit()则会抛出java.lang.IndexOutOfBoundsException。
9、复制缓冲区
asReadOnlyBuffer():复制一个只读缓冲区,duplicate():复制一个可读可写的缓冲区,slice():复制一个从源缓冲position到limit的新缓冲区;
Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。
分割缓冲区与复制相似,但 slice()创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量(limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。
- 直接缓冲区
直接缓冲区被用于与通道和固有 I/O 例程交互。它们通过使用固有代码来告知操作系统直接释放或填充内存区域,对用于通道直接或原始存取的内存区域中的字节元素的存储尽了最大的努力。
直接字节缓冲区通常是 I/O 操作最好的选择。在设计方面,它们支持 JVM 可用的最高效I/O 机制。非直接字节缓冲区可以被传递给通道,但是这样可能导致性能损耗。通常非直接缓冲不可能成为一个本地 I/O 操作的目标。如果向一个通道中传递一个非直接 ByteBuffer对象用于写入,通道可能会在每次调用中隐含地进行下面的操作:
1.创建一个临时的直接 ByteBuffer 对象。
2.将非直接缓冲区的内容复制到临时缓冲中。
3.使用临时缓冲区执行低层次 I/O 操作。
4.临时缓冲区对象离开作用域,并最终成为被回收的无用数据。
直接缓冲区时 I/O 的最佳选择,但可能比创建非直接缓冲区要花费更高的成本。直接缓冲区使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈。建立和销毁直接缓冲区会明显比具有堆栈的缓冲区更加破费,这取决于主操作系统以及 JVM 实现。直接缓冲区的内存区域不受无用存储单元收集支配,因为它们位于标准 JVM 堆栈之外。
直接 ByteBuffer 是通过调用具有所需容量的 ByteBuffer.allocateDirect()函数产生的,就像我们之前所涉及的 allocate()函数一样。注意用一个 wrap()函数所创建的被包装的缓冲区总是非直接的。
- 小结
由于缓冲区的工作与通道紧密联系。通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标,接下来,将介绍通道的相关知识。