- NIO与Socket编程技术指南
- 高洪岩
- 8731字
- 2023-07-26 14:30:45
1.3 Buffer类的使用
在JDK 1.8.0_92版本中,Buffer类的API列表如图1-7所示。
图1-7 Buffer类的API列表
本节会对这些API进行演示和讲解,目的就是让读者全面地掌握NIO核心类—Buffer的使用。
需要注意的是,Buffer.java类是抽象类,并不能直接实例化,而其子类:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer也是抽象类。这7个子类的声明信息如下:
public abstract class ByteBuffer extends Buffer public abstract class CharBuffer extends Buffer public abstract class DoubleBuffer extends Buffer public abstract class FloatBuffer extends Buffer public abstract class IntBuffer extends Buffer public abstract class LongBuffer extends Buffer public abstract class ShortBuffer extends Buffer
抽象类Buffer.java的7个子类也是抽象类,也就意味着ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer这些类也不能被直接new实例化。如果不能直接new实例化,那么如何创建这些类的对象呢?使用的方式是将上面7种数据类型的数组包装(wrap)进缓冲区中,此时就需要借助静态方法wrap()进行实现。wrap()方法的作用是将数组放入缓冲区中,来构建存储不同数据类型的缓冲区。
注意
缓冲区为非线程安全的。
下面就要开始介绍Buffer类中全部的API了。虽然Buffer类的7个子类都有与其父类(Buffer类)相同的API,但为了演示代码的简短性,在测试中只使用ByteBuffer或CharBuffer类作为API功能的演示。
1.3.1 包装数据与获得容量
在NIO技术的缓冲区中,存在4个核心技术点,分别是:
❑capacity(容量)
❑limit(限制)
❑position(位置)
❑mark(标记)
这4个技术点之间值的大小关系如下:
0≤mark≤position≤limit≤capacity
首先介绍一下缓冲区的capacity,它代表包含元素的数量。缓冲区的capacity不能为负数,并且capacity也不能更改。
int capacity()方法的作用:返回此缓冲区的容量。
示例代码如下:
public class Test1 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; short[] shortArray = new short[] { 1, 2, 3, 4 }; int[] intArray = new int[] { 1, 2, 3, 4, 5 }; long[] longArray = new long[] { 1, 2, 3, 4, 5, 6 }; float[] floatArray = new float[] { 1, 2, 3, 4, 5, 6, 7 }; double[] doubleArray = new double[] { 1, 2, 3, 4, 5, 6, 7, 8 }; char[] charArray = new char[] { 'a', 'b', 'c', 'd' }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); ShortBuffer shortBuffer = ShortBuffer.wrap(shortArray); IntBuffer intBuffer = IntBuffer.wrap(intArray); LongBuffer longBuffer = LongBuffer.wrap(longArray); FloatBuffer floatBuffer = FloatBuffer.wrap(floatArray); DoubleBuffer doubleBuffer = DoubleBuffer.wrap(doubleArray); CharBuffer charBuffer = CharBuffer.wrap(charArray); System.out.println("bytebuffer=" + bytebuffer.getClass().getName()); System.out.println("shortBuffer=" + shortBuffer.getClass().getName()); System.out.println("intBuffer=" + intBuffer.getClass().getName()); System.out.println("longBuffer=" + longBuffer.getClass().getName()); System.out.println("floatBuffer=" + floatBuffer.getClass().getName()); System.out.println("doubleBuffer=" + doubleBuffer.getClass().getName()); System.out.println("charBuffer=" + charBuffer.getClass().getName()); System.out.println(); System.out.println("bytebuffer.capacity=" + bytebuffer.capacity()); System.out.println("shortBuffer.capacity=" + shortBuffer.capacity()); System.out.println("intBuffer.capacity=" + intBuffer.capacity()); System.out.println("longBuffer.capacity=" + longBuffer.capacity()); System.out.println("floatBuffer.capacity=" + floatBuffer.capacity()); System.out.println("doubleBuffer.capacity=" + doubleBuffer.capacity()); System.out.println("charBuffer.capacity=" + charBuffer.capacity()); } }
程序运行结果如下:
bytebuffer=java.nio.HeapByteBuffer shortBuffer=java.nio.HeapShortBuffer intBuffer=java.nio.HeapIntBuffer longBuffer=java.nio.HeapLongBuffer floatBuffer=java.nio.HeapFloatBuffer doubleBuffer=java.nio.HeapDoubleBuffer charBuffer=java.nio.HeapCharBuffer bytebuffer.capacity=3 shortBuffer.capacity=4 intBuffer.capacity=5 longBuffer.capacity=6 floatBuffer.capacity=7 doubleBuffer.capacity=8 charBuffer.capacity=4
由于ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer是抽象类,因此wrap()就相当于创建这些缓冲区的工厂方法,在源代码中创建的流程示例如图1-8所示。
图1-8 创建流程
从源代码中可以发现,通过创建HeapByteBuffer类的实例来实现创建ByteBuffer类的实例。因为ByteBuffer与HeapByteBuffer是父子类的关系,所以在将HeapByteBuffer类的对象赋值给数据类型为ByteBuffer的变量时产生多态关系。
ByteBuffer类缓冲区的技术原理就是使用byte[]数组进行数据的保存,在后续使用指定的API来操作这个数组以达到操作缓冲区的目的,示例代码如图1-9所示。
图1-9 HeapByteBuffer类构造方法的流程
在HeapByteBuffer类的构造方法中,使用代码super(-1, off, off + len, buf.length, buf, 0)调用父类的构造方法将字节数组buf传给父类ByteBuffer,而且子类HeapByteBuffer还重写了父类ByteBuffer中的大部分方法,因此,在调用HeapByteBuffer类的API时,访问的是父类中的buf字节数组变量,在调用API处理buf字节数组中的数据时,执行的是HeapByteBuffer类中重写的方法。
从源代码中可以了解到,缓冲区存储的数据还是存储在byte[]字节数组中。使用缓冲区与使用byte[]字节数组的优点在于缓冲区将存储数据的byte[]字节数组内容与相关的信息整合在1个Buffer类中,将数据与缓冲区中的信息进行了整合,并进行了封装,这样便于获得相关的信息及处理数据。
capacity代表着缓冲区的大小,效果如图1-10所示。
图1-10 容量图示
缓冲区中的capacity其实就是buf.length属性值。
1.3.2 限制获取与设置
方法int limit()的作用:返回此缓冲区的限制。
方法Buffer limit(int newLimit)的作用:设置此缓冲区的限制。
什么是限制呢?缓冲区中的限制代表第一个不应该读取或写入元素的index(索引)。缓冲区的限制(limit)不能为负,并且limit不能大于其capacity。如果position大于新的limit,则将position设置为新的limit。如果mark已定义且大于新的limit,则丢弃该mark。
position和mark这两个知识点在后面的章节有详细的介绍,此处只需要理解“限制(limit)代表第一个不应该读取或写入元素的index,缓冲区的limit不能为负,并且limit不能大于其capacity”即可。
limit的应用示例如图1-11所示。
图1-11 limit应用示例
虽然图1-11中的缓冲区一共有11个位置可以存放数据,但只允许前4个位置存放数据,后面的其他位置不可以存放数据。因此,JDK API DOC中对limit的解释是:代表第一个不应该读取或写入元素的index。下面再用代码进行验证,测试源代码如下:
public class Test2 { public static void main(String[] args) { char[] charArray = new char[] { 'a', 'b', 'c', 'd', 'e' }; CharBuffer buffer = CharBuffer.wrap(charArray); System.out.println("A capacity()=" + buffer.capacity() + " limit()=" + buffer.limit()); buffer.limit(3); System.out.println(); System.out.println("B capacity()=" + buffer.capacity() + " limit()=" + buffer.limit()); buffer.put(0, 'o'); //0 buffer.put(1, 'p'); //1 buffer.put(2, 'q'); //2 buffer.put(3, 'r'); //3--此位置是第一个不可读不可写的索引 buffer.put(4, 's'); //4 buffer.put(5, 't'); //5 buffer.put(6, 'u'); //6 } }
程序运行后,在第16行出现异常,如图1-12所示。
图1-12 出现异常
在A处打印的值是两个5,说明在调用wrap()方法后,limit的值是capacity+1,因为limit取值范围是从索引0开始,而capacity是从1开始。
Limit使用的场景就是当反复地向缓冲区中存取数据时使用,比如第1次向缓冲区中存储9个数据,分别是A、B、C、D、E、F、G、H、I,如图1-13所示。
图1-13 第1次存储9个数据
然后读取全部9个数据,完成后再进行第2次向缓冲区中存储数据,第2次只存储4个数据,分别是1、2、3、4,效果如图1-14所示。
图1-14 第2次存储4个数据
当读取时却出现了问题,如果读取全部数据1、2、3、4、E、F、G、H、I时是错误的,所以要结合limit来限制读取的范围,在E处设置limit,从而实现只能读取1、2、3、4这4个正确的数据。
1.3.3 位置获取与设置
方法int position()的作用:返回此缓冲区的位置。
方法Buffer position(int newPosition)的作用:设置此缓冲区新的位置。
什么是位置呢?它代表“下一个”要读取或写入元素的index(索引),缓冲区的position(位置)不能为负,并且position不能大于其limit。如果mark已定义且大于新的position,则丢弃该mark。
position应用示例如图1-15所示。
图1-15 position应用示例
在图1-13中,position对应的index是3,说明从此位置处开始写入或读取,直到limit结束。
下面用代码来验证position是下一个读取或写入操作的index:
public class Test3 { public static void main(String[] args) { char[] charArray = new char[] { 'a', 'b', 'c', 'd' }; CharBuffer charBuffer = CharBuffer.wrap(charArray); System.out.println("A capacity()=" + charBuffer.capacity() + " limit()=" + charBuffer.limit() + " position()=" + charBuffer.position()); charBuffer.position(2); System.out.println("B capacity()=" + charBuffer.capacity() + " limit()=" + charBuffer.limit() + " position()=" + charBuffer.position()); charBuffer.put("z"); for (int i = 0; i < charArray.length; i++) { System.out.print(charArray[i] + " "); } } }
程序运行结果如下:
A capacity()=4 limit()=4 position()=0 B capacity()=4 limit()=4 position()=2 a b z d
1.3.4 剩余空间大小获取
方法int remaining()的作用:返回“当前位置”与limit之间的元素数。
方法remaining()的应用示例如图1-16所示。
图1-16 方法remaining()应用示例
方法int remaining()的内部源代码如下:
public final int remaining() { return limit - position; }
示例代码如下:
public class Test4 { public static void main(String[] args) { char[] charArray = new char[] { 'a', 'b', 'c', 'd', 'e' };
CharBuffer charBuffer = CharBuffer.wrap(charArray); System.out.println("A capacity()=" + charBuffer.capacity() + " limit()=" + charBuffer.limit() + " position()=" + charBuffer.position()); charBuffer.position(2); System.out.println("B capacity()=" + charBuffer.capacity() + " limit()=" + charBuffer.limit() + " position()=" + charBuffer.position()); System.out.println("C remaining()=" + charBuffer.remaining()); } }
程序运行结果如下:
A capacity()=5 limit()=5 position()=0 B capacity()=5 limit()=5 position()=2 C remaining()=3
1.3.5 使用Buffer mark()方法处理标记
方法Buffer mark()的作用:在此缓冲区的位置设置标记。
标记有什么作用呢?缓冲区的标记是一个索引,在调用reset()方法时,会将缓冲区的position位置重置为该索引。标记(mark)并不是必需的。定义mark时,不能将其定义为负数,并且不能让它大于position。如果定义了mark,则在将position或limit调整为小于该mark的值时,该mark被丢弃,丢弃后mark的值是-1。如果未定义mark,那么调用reset()方法将导致抛出InvalidMarkException异常。
缓冲区中的mark有些类似于探险或爬山时在关键路口设置“路标”,目的是在原路返回时找到回去的路。
mark的示例代码如下:
public class Test5 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); System.out.println("bytebuffer.capacity=" + bytebuffer.capacity()); System.out.println(); bytebuffer.position(1); bytebuffer.mark(); //在位置1设置mark System.out.println("bytebuffer.position=" + bytebuffer.position()); bytebuffer.position(2); //改变位置 bytebuffer.reset(); //位置重置 System.out.println(); // 回到位置为1处 System.out.println("bytebuffer.position=" + bytebuffer.position()); } }
程序运行结果如下:
bytebuffer.capacity=3 bytebuffer.position=1 bytebuffer.position=1
1.3.6 知识点细化测试
前面介绍了缓冲区4个核心技术点:capacity、limit、position和mark,根据这4个技术点,可以设计出以下7个实验。
1)缓冲区的capacity不能为负数,缓冲区的limit不能为负数,缓冲区的position不能为负数。
2)position不能大于其limit。
3)limit不能大于其capacity。
4)如果定义了mark,则在将position或limit调整为小于该mark的值时,该mark被丢弃。
5)如果未定义mark,那么调用reset()方法将导致抛出InvalidMarkException异常。
6)如果position大于新的limit,则position的值就是新limit的值。
7)当limit和position值一样时,在指定的position写入数据时会出现异常,因为此位置是被限制的。
1.验证第1条
验证:缓冲区的capacity不能为负数,缓冲区的limit不能为负数,缓冲区的position不能为负数。
首先测试一下“缓冲区的capacity不能为负数”,需要使用allocate()方法开辟出指定空间大小的缓冲区,示例代码如下:
public class Test1_1 { public static void main(String[] args) { try { ByteBuffer bytebuffer = ByteBuffer.allocate(-1); } catch (IllegalArgumentException e) { System.out.println("ByteBuffer容量capacity大小不能为负数"); } } }
allocate(int capacity)方法分配一个新的缓冲区。
程序运行结果如下:
ByteBuffer容量capacity大小不能为负数
然后测试一下“缓冲区的limit不能为负数”,示例代码如下:
public class Test1_2 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); try { bytebuffer = (ByteBuffer) bytebuffer.limit(-1); } catch (IllegalArgumentException e) { System.out.println("ByteBuffer限制limit大小不能为负数"); } } }
程序运行结果如下:
ByteBuffer限制limit大小不能为负数
最后测试一下“缓冲区的position不能为负数”,示例代码如下:
public class Test1_3 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); try { bytebuffer = (ByteBuffer) bytebuffer.position(-1); } catch (IllegalArgumentException e) { System.out.println("ByteBuffer位置position大小不能为负数"); } } }
程序运行结果如下:
ByteBuffer位置position大小不能为负数
2.验证第2条
验证:position不能大于其limit。
示例代码如下:
public class Test2 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.limit(2); try { bytebuffer.position(3); } catch (IllegalArgumentException e) { System.out.println("ByteBuffer的position位置不能大于其limit限制"); } } }
程序运行结果如下:
ByteBuffer的position位置不能大于其limit限制
3.验证第3条
验证:limit不能大于其capacity。
示例代码如下:
public class Test3 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); try { bytebuffer.limit(100); } catch (IllegalArgumentException e) { System.out.println("ByteBuffer的limit不能大于其capacity容量"); } } }
程序运行结果如下:
ByteBuffer的limit不能大于其capacity容量
4.验证第4条
验证:如果定义了mark,则在将position或limit调整为小于该mark的值时,该mark被丢弃。
在此处将第4条拆分成4点来分别进行验证。
1)如果定义了mark,则在将position调整为不小于该mark的值时,该mark不丢弃。
2)如果定义了mark,则在将position调整为小于该mark的值时,该mark被丢弃。
3)如果定义了mark,则在将limit调整为不小于该mark的值时,该mark不丢弃。
4)如果定义了mark,则在将limit调整为小于该mark的值时,该mark被丢弃。
首先验证一下“如果定义了mark,则在将position调整为不小于该mark的值时,该mark不丢弃”,示例代码如下:
public class Test4_1 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.position(1); bytebuffer.mark(); System.out.println("bytebuffer在 " + bytebuffer.position() + " 位置设置mark标记"); bytebuffer.position(2); bytebuffer.reset(); System.out.println(); System.out.println("bytebuffer回到" + bytebuffer.position() + "位置"); } }
程序运行结果如下:
bytebuffer在1位置设置mark标记 bytebuffer回到1位置
然后验证一下“如果定义了mark,则在将position调整为小于该mark的值时,该mark将被丢弃”,示例代码如下:
public class Test4_2 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.position(2); bytebuffer.mark(); bytebuffer.position(1); try { bytebuffer.reset(); } catch (InvalidMarkException e) { System.out.println("bytebuffer的mark标记无效"); } } }
程序运行结果如下:
bytebuffer的mark标记无效
接着验证一下“如果定义了mark,则在将limit调整为不小于该mark的值时,该mark不丢弃”,示例代码如下:
public class Test4_3 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray); System.out.println("A byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); System.out.println(); byteBuffer.position(2); byteBuffer.mark(); System.out.println("B byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); byteBuffer.position(3); byteBuffer.limit(3); System.out.println(); System.out.println("C byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); byteBuffer.reset(); System.out.println(); System.out.println("D byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); } }
程序运行结果如下:
A byteBuffer position=0 limit=3 B byteBuffer position=2 limit=3 C byteBuffer position=3 limit=3 D byteBuffer position=2 limit=3
最后验证一下“如果定义了mark,则在将limit调整为小于该mark的值时,该mark被丢弃”,示例代码如下:
public class Test4_4 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray); System.out.println("A byteBuffer position=" + byteBuffer.position() +" limit=" + byteBuffer.limit()); System.out.println(); byteBuffer.position(2); byteBuffer.mark(); System.out.println("B byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); byteBuffer.limit(1); System.out.println(); System.out.println("C byteBuffer position=" + byteBuffer.position() + " limit=" + byteBuffer.limit()); System.out.println(); try { byteBuffer.reset(); } catch (InvalidMarkException e) { System.out.println("byteBuffer mark丢失"); } } }
程序运行结果如下:
A byteBuffer position=0 limit=3 B byteBuffer position=2 limit=3 C byteBuffer position=1 limit=1 byteBuffer mark丢失
总结:limit和position不能小于mark,如果小于则mark丢弃。
5.验证第5条
验证:如果未定义mark,那么调用reset()方法将导致抛出InvalidMarkException异常。示例代码如下:
public class Test5 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); try { bytebuffer.reset(); } catch (InvalidMarkException e) { System.out.println("bytebuffer的mark标记无效"); } } }
程序运行结果如下:
bytebuffer的mark标记无效
6.验证第6条
验证:如果position大于新的limit,则position的值就是新limit的值。
示例代码如下:
public class Test6 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.position(3); System.out.println("bytebuffer limit(2)之前的位置:" + bytebuffer.position()); bytebuffer.limit(2); System.out.println(); System.out.println("bytebuffer limit(2)之后的位置:" + bytebuffer.position()); } }
程序运行结果如下:
bytebuffer limit(2)之前的位置:3 bytebuffer limit(2)之后的位置:2
7.验证第7条
验证:当limit和position值一样时,在指定的position写入数据时会出现异常,因为此位置是被限制的。
示例代码如下:
public class Test7 { public static void main(String[] args) { char[] charArray = new char[] { 'a', 'b', 'c', 'd' }; CharBuffer charBuffer = CharBuffer.wrap(charArray); System.out.println("A capacity()=" + charBuffer.capacity() + " limit()=" + charBuffer.limit() + " position()=" + charBuffer.position()); System.out.println(); charBuffer.position(1); charBuffer.limit(1); charBuffer.put("z"); } }
程序运行结果如下:
A capacity()=4 limit()=4 position()=0 Exception in thread "main" java.nio.BufferOverflowException at java.nio.CharBuffer.put(CharBuffer.java:922) at java.nio.CharBuffer.put(CharBuffer.java:950) at BufferAPITest.Details.Test7.main(Test7.java:15)
1.3.7 判断只读
boolean isReadOnly()方法的作用:告知此缓冲区是否为只读缓冲区。
示例代码如下:
public class Test6 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; short[] shortArray = new short[] { 1, 2, 3, 4 }; int[] intArray = new int[] { 1, 2, 3, 4, 5 }; long[] longArray = new long[] { 1, 2, 3, 4, 5, 6 }; float[] floatArray = new float[] { 1, 2, 3, 4, 5, 6, 7 }; double[] doubleArray = new double[] { 1, 2, 3, 4, 5, 6, 7, 8 }; char[] charArray = new char[] { 'a', 'b', 'c', 'd' }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); ShortBuffer shortBuffer = ShortBuffer.wrap(shortArray); IntBuffer intBuffer = IntBuffer.wrap(intArray); LongBuffer longBuffer = LongBuffer.wrap(longArray); FloatBuffer floatBuffer = FloatBuffer.wrap(floatArray); DoubleBuffer doubleBuffer = DoubleBuffer.wrap(doubleArray); CharBuffer charBuffer = CharBuffer.wrap(charArray); System.out.println("bytebuffer.isReadOnly=" + bytebuffer.isReadOnly()); System.out.println("shortBuffer.isReadOnly=" + shortBuffer.isReadOnly()); System.out.println("intBuffer.isReadOnly=" + intBuffer.isReadOnly()); System.out.println("longBuffer.isReadOnly=" + longBuffer.isReadOnly()); System.out.println("floatBuffer.isReadOnly=" + floatBuffer.isReadOnly()); System.out.println("doubleBuffer.isReadOnly=" + doubleBuffer.isReadOnly()); System.out.println("charBuffer.isReadOnly=" + charBuffer.isReadOnly()); } }
程序运行结果如下:
bytebuffer.isReadOnly=false shortBuffer.isReadOnly=false intBuffer.isReadOnly=false longBuffer.isReadOnly=false floatBuffer.isReadOnly=false doubleBuffer.isReadOnly=false charBuffer.isReadOnly=false
1.3.8 直接缓冲区
boolean isDirect()方法的作用:判断此缓冲区是否为直接缓冲区。那什么是“直接缓冲区”呢?先来看看使用非直接缓冲区操作数据的流程,如图1-17所示。
图1-17 使用非直接缓冲区保存数据的过程
在图1-17中可以发现,通过ByteBuffer向硬盘存取数据时是需要将数据暂存在JVM的中间缓冲区,如果有频繁操作数据的情况发生,则在每次操作时都会将数据暂存在JVM的中间缓冲区,再交给ByteBuffer处理,这样做就大大降低软件对数据的吞吐量,提高内存占有率,造成软件运行效率降低,这就是非直接缓冲区保存数据的过程,所以非直接缓冲区的这个弊端就由直接缓冲区解决了。
使用直接缓冲区操作数据的过程如图1-18所示。
图1-18 使用直接缓冲区保存数据的过程
如果使用直接缓冲区来实现两端数据交互,则直接在内核空间中就进行了处理,无须JVM创建新的缓冲区,这样就减少了在JVM中创建中间缓冲区的步骤,增加了程序运行效率。
示例代码如下:
public class Test7_1 { public static void main(String[] args) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100); System.out.println(byteBuffer.isDirect()); } }
打印结果如下:
true
成功创建出直接缓冲区。
1.3.9 还原缓冲区的状态
final Buffer clear()方法的作用:还原缓冲区到初始的状态,包含将位置设置为0,将限制设置为容量,并丢弃标记,即“一切为默认”。
clear()方法的内部源代码如下:
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
clear()方法的主要使用场景是在对缓冲区存储数据之前调用此方法。例如:
buf.clear(); //准备开始向缓冲区中写数据了,缓冲区的状态要通过clear()进行还原 in.read(buf); //从in开始读数据,将数据写入buf中
需要注意的是,clear()方法“不能真正清除”缓冲区中的数据,虽然从名称来看它似乎能够这样做,这样命名是因为它在多数情况下确实有清除数据的作用,那么怎么“清除”数据呢?例如,调用代码“buf.clear(); ”后将缓冲区的状态进行还原,包含将position(位置)归0,再执行写入新数据的代码,将最新版的数据由索引位置0开始覆盖,这样就将缓冲区中的旧值用新值覆盖了,相当于数据被清除了。
示例代码如下:
public class Test8 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.position(2); bytebuffer.limit(3); bytebuffer.mark(); bytebuffer.clear(); System.out.println("bytebuffer.position=" + bytebuffer.position()); System.out.println(); System.out.println("bytebuffer.limit=" + bytebuffer.limit()); System.out.println(); try { bytebuffer.reset(); } catch (java.nio.InvalidMarkException e) { } System.out.println("bytebuffer mark丢失"); } }
程序运行结果如下:
bytebuffer.position=0 bytebuffer.limit=3 bytebuffer mark丢失
1.3.10 对缓冲区进行反转
final Buffer flip()方法的作用:反转此缓冲区。首先将限制设置为当前位置,然后将位置设置为0。如果已定义了标记,则丢弃该标记。
flip()方法的通俗解释是“缩小limit的范围,类似于String.subString(0, endIndex)方法”。
flip()方法的内部源代码如下:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
当向缓冲区中存储数据,然后再从缓冲区中读取这些数据时,就是使用flip()方法的最佳时机,示例代码如下:
A—buf.allocate(10);
B—buf.put(8);
C—首先向buf写入数据,此步骤是重点操作;
D—buf.flip();
E—然后从buf读出数据,此步骤是重点操作。
当执行A处代码时,缓冲区出现10个空的位置,索引形式如下:
0 1 2 3 4 5 6 7 8 9
当执行B处代码时,position为0的位置存入数字8,然后position自动变成1,因为put()方法会将position进行自增,这时缓冲区中的数据如下:
0 1 2 3 4 5 6 7 8 9 8
当执行C处代码时,假设要写入的数据数组为{11, 22, 33, 44, 55},将这5个数字在position是1的位置依次存入buf中,完成后的缓冲区中的数据如下:
0 1 2 3 4 5 6 7 8 9 8 11 22 33 44 55
这时position的值是6,下一步要将缓冲区中的数据读取出来时,有效的数据应该是:
8 11 22 33 44 55
因为位置6~7~8~9中存储的值是无效的,所以调用D处代码flip()后将position的值6作为limit的值,而position被重新赋值为0,有效数据的范围为:
0 1 2 3 4 5 8 11 22 33 44 55
最后执行E处代码,将这些有效的数据读取出来。
final Buffer flip()方法常用在向缓冲区中写入一些数据后,下一步读取缓冲区中的数据之前,以改变limit与position的值。
方法flip会改变position和limit的值,示例代码如下:
public class Test11 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.position(2); bytebuffer.mark(); bytebuffer.flip(); System.out.println("bytebuffer.position=" + bytebuffer.position()); System.out.println(); System.out.println("bytebuffer.limit=" + bytebuffer.limit()); System.out.println(); try { bytebuffer.reset(); } catch (java.nio.InvalidMarkException e) { System.out.println("bytebuffer mark丢失"); } } }
程序运行结果如下:
bytebuffer.position=0 bytebuffer.limit=2 bytebuffer mark丢失
final Buffer flip()方法常用在向缓冲区中写入一些数据后,下一步读取缓冲区中的数据之前调用,以改变limit与position的值,示例代码如下:
public class Test11_1 { public static void main(String[] args) { CharBuffer charBuffer = CharBuffer.allocate(20); System.out.println("A position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 一共写入14个字 charBuffer.put("我是中国人我在中华人民共和国"); System.out.println("B position=" + charBuffer.position() + " limit=" + charBuffer.limit()); charBuffer.position(0); // 位置position还原成0 System.out.println("C position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 下面for语句的打印效果是“国”字后面有6个空格,这6个空格是无效的数据 // 应该只打印前14个字符,后6个字符不再读取 for (int i = 0; i < charBuffer.limit(); i++) { System.out.print(charBuffer.get()); } System.out.println(); // 上面的代码是错误读取数据的代码 // 下面的代码是正确读取数据的代码 System.out.println("D position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 还原缓冲区的状态 charBuffer.clear(); System.out.println("E position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 继续写入 charBuffer.put("我是美国人"); System.out.println("F position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 设置for循环结束的位置,也就是新的limit值 charBuffer.limit(charBuffer.position()); charBuffer.position(0); System.out.println("G position=" + charBuffer.position() + " limit=" + charBuffer.limit()); for (int i = 0; i < charBuffer.limit(); i++) { System.out.print(charBuffer.get()); } } }
程序运行结果如下:
A position=0 limit=20 B position=14 limit=20 C position=0 limit=20 我是中国人我在中华人民共和国空格空格空格空格空格空格 D position=20 limit=20 E position=0 limit=20 F position=5 limit=20 G position=0 limit=5 我是美国人
上面的程序在读取数据时都要执行以下代码:
charBuffer.limit(charBuffer.position()); charBuffer.position(0);
这样会显得比较烦琐,可以使用flip()方法,示例代码如下:
public class Test11_2 { public static void main(String[] args) { CharBuffer charBuffer = CharBuffer.allocate(20); System.out.println("A position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 一共写入14个字 charBuffer.put("我是中国人我在中华人民共和国"); System.out.println("B position=" + charBuffer.position() + " limit=" + charBuffer.limit()); charBuffer.position(0); // 位置position还原成0 System.out.println("C position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 下面for语句的打印效果是“国”字后面有6个空格,这6个空格是无效的数据 // 应该只打印前14个字符,后6个字符不再读取 for (int i = 0; i < charBuffer.limit(); i++) { System.out.print(charBuffer.get()); } System.out.println(); // 上面的代码是错误读取数据的代码 // 下面的代码是正确读取数据的代码 System.out.println("D position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 还原缓冲区的状态 charBuffer.clear(); System.out.println("E position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 继续写入 charBuffer.put("我是美国人"); System.out.println("F position=" + charBuffer.position() + " limit=" + charBuffer.limit()); // 使用flip()方法 charBuffer.flip(); System.out.println("G position=" + charBuffer.position() + " limit=" + charBuffer.limit()); for (int i = 0; i < charBuffer.limit(); i++) { System.out.print(charBuffer.get()); } } }
得出的运行结果是一样的。
1.3.11 判断是否有底层实现的数组
final boolean hasArray()方法的作用:判断此缓冲区是否具有可访问的底层实现数组。该方法的内部源代码如下:
public final boolean hasArray() { return (hb ! = null) && ! isReadOnly; }
示例代码如下:
public class Test12 { public static void main(String[] args) throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocate(100); byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); System.out.println(byteBuffer.hasArray()); } }
程序运行结果如下:
true
也可以对直接缓冲区进行判断,示例代码如下:
public class Test12_1 { public static void main(String[] args) throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100); byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); System.out.println(byteBuffer.hasArray()); } }
程序运行结果如下:
false
打印true值是因为在源代码:
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> { final byte[] hb;
程序中使用byte[] hb存储数据,所以hb[]对象为非空,结果就是true。
打印false代表byte[] hb数组值为null,并没有将数据存储到hb[]中,而是直接存储在内存中。
hasArray()方法的内部源代码
public final boolean hasArray() { return (hb ! = null) && ! isReadOnly; }
正是以byte[] hb是否有值来判断是否有底层数组支持。
1.3.12 判断当前位置与限制之间是否有剩余元素
final boolean hasRemaining()方法的作用:判断在当前位置和限制之间是否有元素。该方法的内部源代码如下:
public final boolean hasRemaining() { return position < limit; }
final int remaining()方法的作用:返回“当前位置”与限制之间的元素个数。该方法的内部源代码如下:
public final int remaining() { return limit - position; }
示例代码如下:
public class Test13 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); bytebuffer.limit(3); bytebuffer.position(2); System.out.println("bytebuffer.hasRemaining=" + bytebuffer.hasRemaining() + " bytebuffer.remaining=" + bytebuffer.remaining()); } }
程序运行结果如下:
bytebuffer.hasRemaining=true bytebuffer.remaining=1
这两个方法可以在读写缓冲区中的数据时使用。本例仅测试读数据时的使用情况,示例代码如下:
public class Test13_1 { public static void main(String[] args) { byte[] byteArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray); int remaining = byteBuffer.remaining(); for (int i = 0; i < remaining; i++) { System.out.print(byteBuffer.get() + " "); } System.out.println(); byteBuffer.clear(); while (byteBuffer.hasRemaining()) { System.out.print(byteBuffer.get() + " "); } System.out.println(); byteBuffer.clear(); for (; byteBuffer.hasRemaining() == true; ) { System.out.print(byteBuffer.get() + " "); } } }
程序运行结果如下:
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9
运行结果表明成功取出全部的数据。
1.3.13 重绕缓冲区
final Buffer rewind()方法的作用:重绕此缓冲区,将位置设置为0并丢弃标记。该方法
的内部源代码如下。
public final Buffer rewind() { position = 0; mark = -1; return this; }
在一系列通道“重新写入或获取”的操作之前调用此方法(假定已经适当设置了限制)。例如:
out.write(buf); //将buf的remaining剩余空间的数据输出到out中 buf.rewind(); //rewind重绕缓冲区 buf.get(array); //从缓冲区获取数据保存到array中
rewind()方法的通俗解释就是“标记清除,位置position值归0, limit不变”。
rewind()方法没有设置限制,说明此方法可以结合自定义的limit限制值。
注意:rewind()方法常在重新读取缓冲区中数据时使用。
final Buffer clear()方法的作用:清除此缓冲区,将位置设置为0,将限制设置为容量,并丢弃标记,方法内部的源代码如下。
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
方法clear()的主要使用场景是在对缓冲区进行存储数据之前调用此方法。例如:
buf.clear(); //Prepare buffer for reading in.read(buf); //Read data
此方法不能实际清除缓冲区中的数据,但从名称来看它似乎能够这样做,这样命名是因为它多数情况下确实是在清除数据时使用。
clear ()方法的通俗解释是“一切为默认”。
final Buffer flip()方法的作用:反转此缓冲区。首先将限制设置为当前位置,然后将位置设置为0。如果已定义了标记,则丢弃该标记,方法内部的源代码如下:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
flip()方法的通俗解释是“缩小limit的范围,类似于String.subString(0, endIndex)方法”。
rewind()、clear()和flip()方法在官方帮助文档中的解释如下。
❑rewind():使缓冲区为“重新读取”已包含的数据做好准备,它使限制保持不变,将位置设置为0。
❑clear():使缓冲区为一系列新的通道读取或相对put(value)操作做好准备,即它将限制设置为容量大小,将位置设置为0。
❑flip():使缓冲区为一系列新的通道写入或相对get(value)操作做好准备,即它将限制设置为当前位置,然后将位置设置为0。
这3个方法的侧重点在于:
1)rewind()方法的侧重点在“重新”,在重新读取、重新写入时可以使用;
2)clear()方法的侧重点在“还原一切状态”;
3)flip()方法的侧重点在substring截取。
示例代码如下:
public class Test14 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3, 4, 5 }; ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray); System.out.println("capacity=" + byteBuffer.capacity() + " limit=" + byteBuffer.limit() + " position=" + byteBuffer.position()); byteBuffer.position(1); byteBuffer.limit(3); byteBuffer.mark(); System.out.println("capacity=" + byteBuffer.capacity() + " limit=" + byteBuffer.limit() + " position=" + byteBuffer.position()); byteBuffer.rewind(); System.out.println("capacity=" + byteBuffer.capacity() + " limit=" + byteBuffer.limit() + " position=" + byteBuffer.position()); byteBuffer.reset(); } }
程序运行结果如下:
capacity=5 limit=5 position=0 capacity=5 limit=3 position=1 capacity=5 limit=3 position=0 Exception in thread "main" java.nio.InvalidMarkException at java.nio.Buffer.reset(Buffer.java:306) at BufferAPITest.Test14.main(Test14.java:24)
1.3.14 获得偏移量
final int arrayOffset()方法的作用:返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量,这个值在文档中标注为“可选操作”,也就是子类可以不处理这个值。该方法的内部源代码如下:
public final int arrayOffset() { if (hb == null) throw new UnsupportedOperationException(); if (isReadOnly) throw new ReadOnlyBufferException(); return offset; }
实例变量offset是在执行HeapByteBuffer类的构造方法时传入的,示例代码如下:
HeapByteBuffer(byte[] buf, int off, int len) { super(-1, off, off + len, buf.length, buf, 0); }
最后一个参数0就是对ByteBuffer类的offset实例变量进行赋值,源代码如下:
ByteBuffer(int mark, int pos, int lim, int cap, //包级访问 byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; }
示例代码如下:
public class Test15 { public static void main(String[] args) { byte[] byteArray = new byte[] { 1, 2, 3 }; ByteBuffer bytebuffer = ByteBuffer.wrap(byteArray); System.out.println("bytebuffer.arrayOffset=" + bytebuffer.arrayOffset()); } }
程序运行结果如下:
bytebuffer.arrayOffset=0
在上面的示例中,不管怎么进行操作,arrayOffset()方法的返回值永远是0,非0的情况将在后面的章节介绍。
1.3.15 使用List.toArray(T[])转成数组类型
如果List中存储ByteBuffer数据类型,则可以使用List中的toArray()方法转成ByteBuffer[]数组类型,示例代码如下:
public class Test16 { public static void main(String[] args) { ByteBuffer buffer1 = ByteBuffer.wrap(new byte[] { 'a', 'b', 'c' }); ByteBuffer buffer2 = ByteBuffer.wrap(new byte[] { 'x', 'y', 'z' }); ByteBuffer buffer3 = ByteBuffer.wrap(new byte[] { '1', '2', '3' }); List<ByteBuffer> list = new ArrayList<>(); list.add(buffer1); list.add(buffer2); list.add(buffer3); ByteBuffer[] byteBufferArray = new ByteBuffer[list.size()]; list.toArray(byteBufferArray); System.out.println(byteBufferArray.length); for (int i = 0; i < byteBufferArray.length; i++) { ByteBuffer eachByteBuffer = byteBufferArray[i]; while (eachByteBuffer.hasRemaining()) { System.out.print((char) eachByteBuffer.get()); } System.out.println(); } } }
程序运行结果如下:
3 abc xyz 123
至此,已经将Buffer类的全部API进行了介绍,熟练掌握父类Buffer的API对学习子类有非常大的帮助,因为这些API是可以被子类所继承并使用的。