NIO的Buffer本质上是一个内存块,既可以写入数据,也可以从中读取数据。Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。
NIO的Buffer内部是一个内存块(数组),与普通的内存块(Java数组)不同的是:NIO Buffer对象提供了一组比较有效的方法,用来进行写入和读取的交替访问。
tips: Buffer类是一个非线程安全类。(Buffers are not safe for use by multiple concurrent threads.)
1 Buffer类API介绍
Buffer类是一个抽象类,对应于Java的主要数据类型。在NIO中,有8种缓冲区类,ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。前7种Buffer类型覆盖了能在IO中传输的所有Java基本数据类型,第8种类型是一种专门用于内存映射的ByteBuffer类型。
使用最多的是ByteBuffer(二进制字节缓冲区)类型
1.1 Buffer类的重要属性
Buffer的子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer基类中,而是定义在具体的子类中。例如,ByteBuffer子类就拥有一个byte[]类型的数组成员final byte[] hb
,可以作为自己的读写缓冲区,数组的元素类型与Buffer子类的操作类型相对应。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
final byte[] hb;
}
复制代码
为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)
1.1.1 capacity 属性,position属性,limit属性
- capacity 属性
Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。
Buffer类的capacity属性一旦初始化,就不能再改变。
Buffer类的对象在初始化时会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小就不能改变了。
- position属性
Buffer类的position属性表示当前的位置。
position属性的值与缓冲区的读写模式有关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。
在写模式下,position值的变化规则如下:
(1)在刚进入写模式时,position值为0,表示当前的写入位置为从头开始。
(2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
(3)初始的position值为0,最大可写值为limit-1。当position值达到limit时,缓冲区就已经无空间可写了。
在读模式下,position值的变化规则如下:
(1)当缓冲区刚开始进入读模式时,position会被重置为0。
(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
(3)在读模式下,limit表示可读数据的上限。position的最大值为最大可读上限limit,当position达到limit时表明缓冲区已经无数据可读。
当新建了一个缓冲区实例时,缓冲区处于写模式,这时是可以写数据的。在数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式,flip为翻转的意思。
在从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:
(1)limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。
(2)position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。
也就是说flip把limit设为position,再把position设为0。
- limit属性
Buffer类的limit属性表示可以写入或者读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关。
在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。
在读模式下,limit值的含义为最多能从缓冲区读取多少数据。
一般来说,在进行缓冲区操作时是先写入再读取的。当缓冲区写入完成后,就可以开始从Buffer读取数据,调用flip()方法(翻转),这时limit的值也会进行调整。将写模式下的position值设置成读模式下的limit值,也就是说,将之前写入的最大数量作为可以读取数据的上限值。
Buffer在翻转时的属性值调整主要涉及position、limit两个属性,下面举一个简单的例子:
(1) 首先,创建缓冲区。新创建的缓冲区处于写模式,其position值为0,limit值为最大容量capacity。
(2)然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。
(3)最后,使用flip方法将缓冲区切换到读模式。limit的值会先被设置成写模式时的position值,所以新的limit值是5,表示可以读取数据的最大上限是5。之后调整position值,新的position会被重置为0,表示可以从0开始读。
标记属性:mark(标记)属性
在缓冲区操作过程当中,可以将当前的position值临时存入mark属性中;需要的时候,再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理。
这里主要涉及两个方法:
- mark方法:将当前的position值临时存入mark属性中
- reset方法:恢复到position属性中
1.2 Buffer类中的重要方法
详细介绍Buffer类的几个常用方法,包含Buffer实例的创建、写入、读取、重复读、标记和重置等。
1.2.1 allocate():创建Buffer实例
@Test
public void testAllocate(){
// 参数:指定capacity属性,不能小于0
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
System.out.println("position = " + byteBuffer.position());
System.out.println("capacity = " + byteBuffer.capacity());
System.out.println("limit = " + byteBuffer.limit());
}
复制代码
输出结果:
position = 0
capacity = 100
limit = 100
复制代码
一个缓冲区在新建后处于写模式,position属性(代表写入位置)的值为0,缓冲区的capacity值是初始化时allocate方法的参数值(这里是100),而limit最大可写上限值也为allocate方法的初始化参数值。
1.2.2 put方法:写入数据
在调用allocate()方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,
如果要把对象写入缓冲区,就需要调用put()方法。put()方法很简单,只有一个参数,即需要写入的对象,只不过要求写入的数据类型与缓冲区的类型保持一致。
@Test(expected = BufferOverflowException.class)
public void testPut(){
IntBuffer buffer = IntBuffer.allocate(5);
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("position = " + buffer.position());
System.out.println("capacity = " + buffer.capacity());
System.out.println("limit = " + buffer.limit());
// 转成数数组
int[] array = buffer.array();
Arrays.stream(array).forEach(System.out::print);
// 前面已经写入5个数据了,尝试在写入就会抛出异常: BufferOverflowException
buffer.put(6);
}
复制代码
输出:
position = 5
capacity = 5
limit = 5
12345
复制代码
1.2.3 flip()
向缓冲区写入数据之后,是否可以直接从缓冲区读取数据呢?不能!这时缓冲区还处于写模式,如果需要读取数据,要将缓冲区转换成读模式。
@Test
public void testFlip() {
IntBuffer buffer = IntBuffer.allocate(20);
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("调用flip之前");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
// 调用
buffer.flip();
System.out.println("调用flip之后");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}
复制代码
输出:
调用flip之前
position = 5 ;capacity = 20 ;limit = 20
调用flip之后
position = 0 ;capacity = 20 ;limit = 5
复制代码
调用flip()方法后,新模式下可读上限limit的值变成了之前写模式下的position属性值,也就是5;而新的读模式下的position值简单粗暴地变成了0,表示从头开始读取。
对flip()方法从写入到读取转换的规则,:
- 设置可读上限limit的属性值。将写模式下的缓冲区中内容的最后写入位置position值作为读模式下的limit上限值。
- 其次,把读的起始位置position的值设为0,表示从头开始读。
- 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记,就会造成位置混乱。
源码也很简单就是干了这三件事
public final Buffer flip() {
// 设置可读上限limit,设置为写模式下的position值
limit = position;
// 把读的起始位置position的值设为0,表示从头开始读
position = 0;
// 清除之前的mark标记
mark = -1;
// 返回当前实例
return this;
}
复制代码
1.2.4 get方法:读取数据
get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
@Test
public void testGet1() {
IntBuffer buffer = IntBuffer.allocate(20);
System.out.println("刚初始化buffer:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("调用flip之前:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.flip();
System.out.println("调用flip之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
int i = buffer.get();
System.out.println("调用get之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}
复制代码
输出:
刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后:
position = 1 ;capacity = 20 ;limit = 5
复制代码
分析:
- 声明的buffer的capacity是20个数据,此时position = 0 ;capacity = 20
- 放了5个数据之后,position就从零一定移动到了5
- 调用flip之后,准备读取数据,就会把position置为0,limit置为刚刚的5(因为现在只有5个数据可读)
- 调用get读取数据(读取出当前position位置的数据,此时就是0位置出的数据那就是1),position就从0移动了1个位置,position = 1了
读取操作会改变可读位置position的属性值,而可读上限limit值并不会改变。在position值和limit值相等时,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了,此时再读就会抛出BufferUnderflowException异常。
get还有一个重载方法,可以读取指定索引的数据,但是这个方法不会移动position
@Test
public void testGet2() {
IntBuffer buffer = IntBuffer.allocate(20);
System.out.println("刚初始化buffer:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("调用flip之前:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.flip();
System.out.println("调用flip之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
int i = buffer.get(2);
System.out.println("调用get之后,读取的数据是:" + i);
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}
复制代码
输出
刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后,读取的数据是:3
position = 0 ;capacity = 20 ;limit = 5
复制代码
此时position没有移动,读取出的数据就是的position为2的数据也就是3
1.2.5 clear()清空 / compact()压缩方法
flip是将缓冲区转换成读模式,而这两个它们可以将缓冲区转换为写模式。
-
clear: 把极限设为容量,再把位置设为0。
-
compact()方法:删除缓冲区内从0到当前位置position的内容,然后把从当前位置position到极限limit的内容拷贝到0到limit-position的区域内,当前位置position和极限limit的取值也做相应的变化
compact: 就是把已经读取出来的空的部分删除,把之前未读取的数据推到buffer的开始位置
@Test
public void testCompact() {
IntBuffer buffer = IntBuffer.allocate(20);
System.out.println("刚初始化buffer:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("调用flip之前:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.flip();
System.out.println("调用flip之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
int i = buffer.get();
System.out.println("调用get之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.compact();
System.out.println("调用compact之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
buffer.put(100);
System.out.println("再次写入数据:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
}
复制代码
输出:
刚初始化buffer:
position = 0 ;capacity = 20 ;limit = 20
调用flip之前:
position = 5 ;capacity = 20 ;limit = 20
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
调用get之后:
position = 1 ;capacity = 20 ;limit = 5
调用compact之后:
position = 4 ;capacity = 20 ;limit = 20
再次写入数据:
position = 5 ;capacity = 20 ;limit = 20
复制代码
分析:
- 声明的buffer的capacity是20个数据,此时position = 0 ;capacity = 20
- 放了5个数据之后,position就从零一定移动到了5
- 调用flip之后,准备读取数据,就会把position置为0,limit置为刚刚的5(因为现在只有5个数据可读)
- 调用get读取数据,position就从0移动了1个位置,position = 1了
- 调用compact之后,删除缓冲区内从0到当前位置position的内容,然后把从当前位置position到极限limit的内容拷贝到0到limit-position的区域内(就会压缩掉刚刚已经读取的出了那个数据的位置),然后把limit设置为capacity,这样就可以继续写入数据(这里就会从position=4的位置继续写入),所以再次写入100这个数据的之后,position=4变成了position=5
1.2.6 rewind():倒带(重新读取)
已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带
@Test
public void testRewind() {
IntBuffer buffer = IntBuffer.allocate(20);
buffer.put(1);
buffer.put(2);
buffer.put(3);
buffer.put(4);
buffer.put(5);
// 调用flip,准备读取数据
buffer.flip();
System.out.println("调用flip之后:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
for (int i = 0; i < buffer.limit(); i++) {
System.out.println(buffer.get());
}
// 读取之后的
System.out.println("读取之后的三个属性信息:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
//
buffer.rewind();
System.out.println("rewind之后的三个属性信息:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
// 再次读取
System.out.println("再次读取:");
for (int i = 0; i < buffer.limit(); i++) {
System.out.println(buffer.get());
}
}
复制代码
输出:
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
1
2
3
4
5
读取之后的三个属性信息:
position = 5 ;capacity = 20 ;limit = 5
rewind之后的三个属性信息:
position = 0 ;capacity = 20 ;limit = 5
再次读取:
1
2
3
4
5
复制代码
摘出核心的两个输出:
调用flip之后:
position = 0 ;capacity = 20 ;limit = 5
rewind之后的三个属性信息:
position = 0 ;capacity = 20 ;limit = 5
复制代码
调用rewind之后的三个属性信息,又回到了调用调用flip之后的值。
rewind ()方法主要是调整了缓冲区的position属性与mark属性,具体的调整规则如下:
- position重置为0,所以可以重读缓冲区中的所有数据。
- limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量。
- mark被清理,表示之前的临时位置不能再用了。
源码也很是简单:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
复制代码
1.2.7 mark()和reset()
- mark()方法将当前position的值保存起来放在mark属性中,让mark属性记住这个临时位置
- reset()方法将mark的值恢复到position中。
@Test
public void testMark(){
IntBuffer buffer = IntBuffer.allocate(5);
buffer.put(1);
buffer.put(2);
buffer.mark();
buffer.put(3);
buffer.put(4);
buffer.put(5);
// 输出数据
Arrays.stream(buffer.array()).forEach(System.out::print);
System.out.println("\nreset之前的三个属性信息:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
// 调用reset
buffer.reset();
System.out.println("reset之后的三个属性信息:");
System.out.println("position = " + buffer.position() + " ;capacity = " + buffer.capacity() + " ;limit = " + buffer.limit());
// 再次写入两个数据
buffer.put(6);
buffer.put(7);
// 输出数据
Arrays.stream(buffer.array()).forEach(System.out::print);
}
复制代码
输出
12345
reset之前的三个属性信息:
position = 5 ;capacity = 5 ;limit = 5
reset之后的三个属性信息:
position = 2 ;capacity = 5 ;limit = 5
12675
复制代码
分析:
- 当写入到2这个数据的时候,调用了mark方法,此时position=3,就会把position的值保存到mark属性中
- 紧接着又写入了3,4,5三个数据,此时position = 5
- 调用了reset方法,就会把position 恢复到之前保存的3
- 然后有写入了两个数据6,7,此时就会从position = 3开始写,所以就会把3和4给覆盖为6和7,所以最后buffer中的数据为:12675
2 使用Buffer类的基本步骤
(1)使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象。
(2)调用put()方法将数据写入缓冲区中。
(3)写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式。
(4)调用get()方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入。