1 allocate()创建缓冲区
在使用Buffer(缓冲区)之前,我们首先需要获取Buffer子类的实例对象,并且分配内存空间。为了获取一个Buffer实例对象,这里并不是使用子类的构造器new来创建一个实例对象,而是调用子类的allocate()方法。下面的程序片段就是用来获取一个整型Buffer类的缓冲区实例对象,代码如下:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;public static void allocatTest(){//调用allocate方法,而不是使用newintBuffer = IntBuffer.allocate(30);//输出buffer的主要属性值Logger.info("------------after allocate------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...allocatTest |> ------------after allocate------------------allocatTest |> position=0allocatTest |> limit=20allocatTest |> capacity=20}
例子中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(30),创建了一个Intbuffer实例对象,并且分配了30 * 4个字节的内存空间。
一个缓冲区在新建后,处于写入的模式,position写入位置为0,最大可写上限limit为的初始化值(这里是20),而缓冲区的容量capacity也是初始化值。
2 put()写入到缓冲区
在调用allocate方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象。要写入缓冲区,需要调用put方法。put方法很简单,只有一个参数,即为所需要写入的对象。不过,写入的数据类型要求与缓冲区的类型保持一致。
接着前面的例子,向刚刚创建的intBuffer缓存实例对象中,写入的5个整数,代码如下:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了创建缓冲区的代码,具体看源代码工程public static void putTest(){for (int i = 0; i< 5; i++){//写入一个整数到缓冲区intBuffer.put(i);}//输出缓冲区的主要属性值Logger.info("------------after put------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...putTest |> ------------after putTest------------------putTest |> position=5putTest |> limit=20putTest |> capacity=20}
从结果可以看到,position变成了5,指向了第6个可以写入的元素位置。而limit最大写入元素的上限、capacity最大容量的值,并没有发生变化。
3 flip()翻转
向缓冲区写入数据之后,是否可以直接从缓冲区中读取数据呢?
呵呵,不能。这时缓冲区还处于写模式,如果需要读取数据,还需要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读取模式。
接着前面的例子,演示一下flip()方法的使用:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了缓冲区的创建、写入的代码,具体看源代码工程public static void flipTest(){//翻转缓冲区,从写模式翻转成读模式intBuffer.flip();//输出缓冲区的主要属性值Logger.info("------------after flip ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...flipTest |> ------------after flipTest ------------------flipTest |> position=0flipTest |> limit=5flipTest |> capacity=20}
调用flip方法后,之前写入模式下的position值5,变成了可读上限limit值5;
而新的读取模式下的position值,简单粗暴地变成了0,表示从头开始读取。对flip()方法的从写入到读取转换的规则,
详细的介绍如下:
- 首先,设置可读的长度上限limit。将写模式下的缓冲区中内容的最后写入位置position值,作为读模式下的limit上限值。
- 其次,把读的起始位置position的值设为0,表示从头开始读。
- 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置。在读模式下,如果继续使用旧的mark标记,会造成位置混乱。
有关上面的三步,其实可以查看flip方法的源代码,Buffer.flip()方法的源代码如下:
public final Buffer flip() {limit = position; //设置可读的长度上限limit,为写入的positionposition = 0; //把读的起始位置position的值设为0,表示从头开始读mark = UNSET_MARK; // 清除之前的mark标记return this;}
至此,大家都知道了,如何将缓冲区切换成读取模式。新的问题来了,在读取完成后,如何再一次将缓冲区切换成写入模式呢?可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为写模式。
Buffer的模式转换,大致如图3-1所示。
4 get()从缓冲区读取
调用flip方法,将缓冲区切换成读取模式。这时,可以开始从缓冲区中进行数据读取了。读数据很简单,调用get方法,每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
接着前面flip的使用实例,演示一下缓冲区的读取操作,代码如下:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了缓冲区的创建、写入、翻转的代码,具体看源代码工程public static void getTest(){//先读2个for (int i = 0; i< 2; i++){int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的主要属性值Logger.info("------------after get 2 int ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());//再读3个for (int i = 0; i< 3; i++){int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的主要属性值Logger.info("------------after get 3 int ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...}
输出结果
getTest |> ------------after get 2 int ------------------getTest |> position=2getTest |> limit=5getTest |> capacity=20getTest |> ------------after get 3 int ------------------getTest |> position=5getTest |> limit=5getTest |> capacity=20
从程序的输出结果,我们可以看到,读取操作会改变可读位置position的值,而limit值不会改变。如果position值和limit的值相等,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了。此时再读,会抛出BufferUnderflowException异常。这里强调一下,在读完之后,是否可以立即进行写入模式呢?不能。现在还处于读取模式,我们必须调用Buffer.clear()或Buffer.compact(),即清空或者压缩缓冲区,才能变成写入模式,让其重新可写。另外,还有一个问题:缓冲区是不是可以重复读呢?答案是可以的。
5 rewind()倒带
已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了缓冲区的创建、写入、读取的代码,具体看源代码工程public static void rewindTest() {//倒带intBuffer.rewind();//输出缓冲区属性Logger.info("------------after rewind ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...}
这个范例程序的执行结果如下:
rewindTest |> ------------after rewind ------------------rewindTest |> position=0rewindTest |> limit=5rewindTest |> capacity=20
rewind ()方法,主要是调整了缓冲区的position属性,具体的调整规则如下:
(1)position重置为0,所以可以重读缓冲区中的所有数据。
(2)limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取多少个元素。
(3)mark标记被清理,表示之前的临时位置不能再用了。
Buffer.rewind()方法的源代码如下:
public final Buffer rewind() {position = 0; //重置为0,所以可以重读缓冲区中的所有数据mark = -1; // mark标记被清理,表示之前的临时位置不能再用了return this;}
通过源代码,我们可以看到rewind()方法与flip()很相似,区别在于:rewind()不会影响limit属性值;而flip()会重设limit属性值。在rewind倒带之后,就可以再一次读取,重复读取的示例代码如下:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了缓冲区的读取、倒带的代码,具体看源代码工程public static void reRead() {for (int i = 0; i< 5; i++) {if (i == 2) {//临时保存,标记一下第3个位置intBuffer.mark();}//读取元素int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的属性值Logger.info("------------after reRead------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...}
这段代码,和前面的读取示例代码基本相同,只是增加了一个mark调用。
6 mark( )和reset( )
Buffer.mark()方法的作用是将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark的值恢复到position中。也就是说,Buffer.mark()和Buffer.reset()方法是配套使用的。两种方法都需要内部mark属性的支持。
7 clear( )清空缓冲区
在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法会将position清零,limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。
接着上面的实例,演示一下clear方法。代码如下:
package com.crazymakercircle.bufferDemo;//...public class UseBuffer{static IntBufferintBuffer = null;//省略了之前的buffer操作代码,具体看源代码工程public static void clearDemo() {Logger.info("------------after clear------------------");//清空缓冲区,进入写入模式intBuffer.clear();//输出缓冲区的属性值Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//...}
这个程序运行之后,结果如下:
main |>清空clearDemo |> ------------after clear------------------clearDemo |> position=0clearDemo |> limit=20clearDemo |> capacity=20
在缓冲区处于读取模式时,调用clear(),缓冲区会被切换成写入模式。调用clear()之后,我们可以看到清空了position的值,即设置写入的起始位置为0,并且写入的上限为最大容量。
8 使用Buffer类的基本步骤
总体来说,使用Java NIO Buffer类的基本步骤如下:
(1)使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。
(2)调用put方法,将数据写入到缓冲区中。
(3)写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。(4)调用get方法,从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear() 或Buffer.compact()方法,将缓冲区转换为写入模式。