概述
Buffer类位于java.nio包中,Bufffer类及其子类的UML图如下
缓冲区的分类
- 操作系统在内存区域中进行I/O操作,由于在操作系统看来内存是一个巨大的字节序列,因此只有字节缓冲区能够参与I/O操作。字节缓冲区要么是直接的,要么是非直接的,如果是直接的,JVM会尽最大努力直接在此缓冲区上进行本机I/O操作,避免用户缓冲区与内核缓冲区之间的复制以提高效率
class CreateBuffer{
public static void main(String[] args){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024);
byte[] bytes = "Hello world".getBytes();
ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes);
if(byteBuffer.isDirect()){
System.out.println("byteBuffer是直接缓冲区");
}else{
System.out.println("byteBuffer是非直接缓冲区");
}
if(byteBuffer1.isDirect()){
System.out.println("byteBuffer1是直接缓冲区");
}else{
System.out.println("byteBuffer1是非直接缓冲区");
}
if(byteBuffer2.isDirect()){
System.out.println("byteBuffer2是直接缓冲区");
}else{
System.out.println("byteBuffer2是非直接缓冲区");
}
}
}
------------------------------------------------------------------------
byteBuffer是直接缓冲区
byteBuffer1是非直接缓冲区
byteBuffer2是非直接缓冲区
从上面代码运行的结果可以看出,Java中通过allocateDirect方法创建直接缓冲区,参数为缓冲区的容量;通过allocate和wrap方法创建非直接缓冲区,wrap方法使用一个缓冲区对应类型的数组作为参数,即将这个数组作为支持缓冲区的数组。
缓冲区的四个重要属性
position :缓冲区中下一个要被读或写的元素索引
limit :缓冲区中第一个不能被读或写的元素索引
mark :一个备忘位置,mark在被标记前是未定义的
capacity :缓冲区的最大容量。在缓冲区创建时设定,一经设定永远不能改变
我们看一下缓冲区刚创建时这四个属性的状态
Buffer类的主要API
package java.nio;
public abstract class Buffer {
public final int capacity(); //获取缓冲区的容量
public final int position(); //获取缓冲区的当前位置
public Buffer position(int newPosition); //设置缓冲区的当前位置
public final int limit(); //获取缓冲区的上限
public Buffer limit(int newLimit); //设置缓冲区的上限
public Buffer mark(); //标记当前位置
public Buffer reset(); //将当前位置置为标记位置
public Buffer clear(); //擦除缓冲区,并不真正擦除缓冲区中的数据,而是将缓冲区的四个属性设置为缓冲区刚创建时的状态
public Buffer flip(); //当准备清空缓冲区时,需要将limit置为当前的position,将position置为零;等效于buffer.limit(buffer.position()).position(0);
public Buffer rewind(); //和flip差不多,只是仅将position置为零
public final int remaining(); //返回limit-position
public final boolean hasRemaining(); //判断缓冲区中是否还有可用元素
public abstract boolean isReadOnly(); //判断缓冲区是否是只读缓冲区
public abstract boolean hasArray(); //判断缓冲区是否由可访问的数组支持,当且仅当该缓冲区是由数组支持且不是只读时返回true
public abstract Object array(); //当hasArray返回true时才能调用,否则抛出异常,可以获取支持该缓冲区的数组的引用
public abstract int arrayOffset(); //缓冲区由一个数组支持,该函数返回缓冲区数据在数组中存储的开始位置的偏移量
public abstract boolean isDirect(); //判断该缓冲区是否为直接缓冲区
public abstract Buffer slice(); //切割缓冲区,只复制缓冲区的一部分,准确的说,这部分的区间是[position,limit-1],切割后得到的新缓冲区和初始缓冲区共享这部分元素
public abstract Buffer duplicate(); //复制缓冲区,这个函数复制得到的新缓冲区称为视图缓冲区,并且和初始缓冲区共享数据元素
}
接下来我们通过具体的例子对上面的API进行练习
缓冲区的填充和释放
public abstract class CharBuffer
extends Buffer
implements Comparable<CharBuffer>, Appendable, CharSequence, Readable
{
//单个元素移动
public abstract char get();
public abstract char get(int index);
public abstract CharBuffer put(char c);
public abstract CharBuffer put(int index, char c);
//批量数据移动
public CharBuffer get(char[] dst, int offset, int length);
public CharBuffer get(char[] dst);
public CharBuffer put(CharBuffer src);
public CharBuffer put(char[] src, int offset, int length);
public final CharBuffer put(char[] src);
//这行注释以上的get、put方法是所有缓冲区通用的,下面这两行是CharBuffer特有的
public CharBuffer put(String src, int start, int end); //下标不包含end
public final CharBuffer put(String src);
}
为什么不在Buffer类中实现get和put方法呢?
因为不同类型的缓冲区get()的返回值类型、put()的参数类型不同,因此必须在具体的子类中实现
//填充和释放缓冲区
class MovePractice {
private static int index;
private static String[] strings = {"huo","gao","han","zhang","yan"};
public static void main(String[] args){
CharBuffer charBuffer = CharBuffer.allocate(100); //创建间接缓冲区
while(fillBuffer(charBuffer)){
charBuffer.flip(); //在释放缓冲区之前调用flip()方法,使得limit为当前的position,然后将position置零
drainBuffer(charBuffer);
charBuffer.clear(); //释放完缓冲区后对缓冲区进行擦除,以便下一次对缓冲区进行填充
}
}
public static void drainBuffer(CharBuffer buffer){ //释放缓冲区
while(buffer.hasRemaining()){ //判断缓冲区中是否还有剩余元素
System.out.print(buffer.get()+" "); //获取缓冲区中的元素,position值自增1
}
System.out.println();
}
public static boolean fillBuffer(CharBuffer buffer){ //填充缓冲区
if(index >= strings.length){
return false;
}
for(int i = 0;i < buffer.limit() && i < strings[index].length();i++){ //i不能超过缓冲区的上限limit
buffer.put(strings[index].charAt(i)); //向缓冲区中填充元素,position值自增1
}
index++;
return true;
}
}
------------------------------------------------------------------------
h u o
g a o
h a n
z h a n g
y a n
//批量移动:尽量加上索引控制,减少错误
class Move{
public static int move1(CharBuffer src,char[] des){ //小缓冲区到大数组
src.flip();
int len = src.remaining(); //要在这里保存移动了的元素数量,因为下一行代码会改变position的值
src.get(des,0,len);
return len;
}
public static int move2(CharBuffer src,char[] des){ //大缓冲区到小数组
src.flip();
int len = Math.min(src.remaining(),des.length); //要在这里保存移动了的元素数量,因为下一行代码会改变position的值
src.get(des,0,len);
return len;
}
public static void main(String[] args) {
String str = "Hello World";
CharBuffer charBuffer = CharBuffer.wrap(str.toCharArray()); //创建间接缓冲区
char[] array1 = new char[15];
char[] array2 = new char[5];
char[] array3 = null;
charBuffer.put(str); //这里之所以加入put操作,不是因为charBuffer中没有元素,而是因为在move1方法中开始就调用flip方法(此时position = 0),这会导致limit = 0,从而导致缓冲区的可用空间为0
int length1 = move1(charBuffer, array1); //小缓冲区到大数组
int length2 = move2(charBuffer, array2); //大缓冲区到小数组
for (int i = 0; i < length1; i++) {
System.out.print(array1[i] + " ");
}
System.out.println();
for (int i = 0; i < length2; i++) {
System.out.print(array2[i] + " ");
}
System.out.println();
}
}
------------------------------------------------------------------------
H e l l o W o r l d
H e l l o
批量移动的优点:批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据
缓冲区的压缩
//缓冲区的压缩
class CompressBuffer{
public static void main(String[] args){
CharBuffer charBuffer = CharBuffer.wrap("Hello world".toCharArray());
char[] arr = null;
if(charBuffer.hasArray()){ //获取支持该缓冲区的数组引用
arr = charBuffer.array();
}
if(arr != null){
System.out.println(Arrays.toString(arr)); //未压缩缓冲区之前打印缓冲区
}
charBuffer.get();
charBuffer.get(); //此时position=2,缓冲区中的前两个元素已经被释放
charBuffer.compact(); //调用compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪
if(arr != null){
System.out.println(Arrays.toString(arr));
}
}
}
------------------------------------------------------------------------
[H, e, l, l, o, , w, o, r, l, d]
[l, l, o, , w, o, r, l, d, l, d]
分析上面的代码:
缓冲区的比较
两个缓冲区相等当且仅当两个对象类型相同且从position到limit的元素数目相同且从position到limit的元素序列必须一致,capacity不需要相同
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
public boolean equals(Object ob);
public int compareTo(ByteBuffer that);
}
所有的缓冲区都用equals方法来测试两个缓冲区是否相等,用compareTo方法来比较缓冲区
class CompareBuffer{
public static void main(String[] args){
byte[] bytes1 = new byte[16];
byte[] bytes2 = new byte[32];
bytes1[5] = (byte)'H';
bytes1[6] = (byte)'e';
bytes1[7] = (byte)'l';
bytes1[8] = (byte)'l';
bytes1[9] = (byte)'o';
bytes2[4] = (byte)'H';
bytes2[5] = (byte)'e';
bytes2[6] = (byte)'l';
bytes2[7] = (byte)'l';
bytes2[8] = (byte)'o';
ByteBuffer byteBuffer1 = ByteBuffer.wrap(bytes1);
ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes2);
byteBuffer1.position(5).limit(10);
byteBuffer2.position(4).limit(9);
if(byteBuffer1.equals(byteBuffer2)){
System.out.println("byteBuffer1和byteBuffer2相等");
}else{
System.out.println("byteBuffer1和byteBuffer2不相等");
}
if(byteBuffer1.compareTo(byteBuffer2) == 0){
System.out.println("byteBuffer1和byteBuffer2相等");
}else{
System.out.println("byteBuffer1和byteBuffer2不相等");
}
}
}
------------------------------------------------------------------------
byteBuffer1和byteBuffer2相等
byteBuffer1和byteBuffer2相等
缓冲区的复制
通过复制得到的缓冲区称为视图缓冲区(因为通过复制得到的缓冲区管理的是被复制缓冲区中的数据),通过下面这个例子来理解这句话
//缓冲区的复制和分割缓冲区的创建
class DuplicateBuffer{
public static void main(String[] args){
CharBuffer cb1 = CharBuffer.wrap("Hello world".toCharArray()); //字节数组作为参数创建的缓冲区是可写可读的
CharBuffer cb2 = CharBuffer.wrap("Hello world"); //字符串作为参数创建的缓冲区是只读的
duplicateBuffer(cb1); //这次调用说明了直接复制得到的缓冲区和初始缓冲区共享数组元素
System.out.println();
duplicateBuffer(cb2);
System.out.println();
readOnlyCopy(cb1); //对cb1进行只读复制(即使初始缓冲区是可写可读的,但是经复制得到的缓冲区是只读的),只读复制得到的缓冲区和初始缓冲区也共享元素
System.out.println();
//分割缓冲区的创建(分割缓冲区的数组为初始缓冲区的(limit-position)这部分)
cb1.position(2).limit(6); //注意在duplicateBuffer(cb1)调用中,cb1中的内容已经变为Hwllo world,并且cb1的position已经到达了limit,通过重置position和limit,我们得到的分割缓冲区将是缓冲区cb1中2-5(四个元素)
SliceBuffer(cb1);
}
public static void duplicateBuffer(CharBuffer charBuffer){ //直接复制
if(charBuffer.isReadOnly()){
System.out.println("charBuffer缓冲区是只读的!");
}else{
System.out.println("charBuffer缓冲区是可写可读的!");
}
CharBuffer charBuffer1 = charBuffer.duplicate();
char[] arr = null;
if(charBuffer1.hasArray()){
arr = charBuffer1.array();
System.out.println("缓冲区charBuffer1的数组是可写可读的");
}else{
System.out.println("缓冲区charBuffer1的数组是只读的");
}
if(arr != null){
arr[1] = 'w';
System.out.println("arr不为空!");
}else{
System.out.println("arr为空!");
}
while(charBuffer.hasRemaining()){
System.out.print(charBuffer.get()+" ");
}
System.out.println();
}
public static void readOnlyCopy(CharBuffer charBuffer){ //只读复制
CharBuffer charBuffer1 = charBuffer.asReadOnlyBuffer();
if(charBuffer.isReadOnly()){
System.out.println("charBuffer是只读的");
}else{
System.out.println("charBuffer是可写可读的");
}
if(charBuffer1.isReadOnly()){
System.out.println("charBuffer1是只读的");
}else{
System.out.println("charBuffer1是可写可读的");
}
}
public static void SliceBuffer(CharBuffer charBuffer){
CharBuffer charBuffer1 = charBuffer.slice();
char[] arr = null;
if(charBuffer1.hasArray()){
arr = charBuffer1.array();
int offset = charBuffer1.arrayOffset();
System.out.println("分割缓冲区是从数组的第"+offset+"个元素开始的"); //通过这个偏移量以及数组的容量,可以判断数组的哪一部分可能被该分割缓冲区使用
}
System.out.println(Arrays.toString(arr));
}
}
------------------------------------------------------------------------
charBuffer缓冲区是可写可读的!
缓冲区charBuffer1的数组是可写可读的
arr不为空!
H w l l o w o r l d
charBuffer缓冲区是只读的!
缓冲区charBuffer1的数组是只读的
arr为空!
H e l l o w o r l d
charBuffer是可写可读的
charBuffer1是只读的
分割缓冲区是从数组的第2个元素开始的
[H, w, l, l, o, , w, o, r, l, d]
字节缓冲区
看了这么多函数,相信很多兄弟有点忘记了我们为什么要使用缓冲区,当然是为了I/O!既然是为了I/O,就肯定要和操作系统打交道,我们在文章一开始就提到了操作系统将内存看作一个巨大的字节数组,因此只有字节缓冲区有资格参与I/O,所以我们来进一步了解一下字节缓冲区的特性
字节顺序
说到字节,自然要考虑到字节顺序,因为这会影响编码的结果(对03 7F和7F 03用同一种编码方式进行编码的结果肯定是不一样的)
- 小端:低字节保存在低地址部分
- 大端:高字节保存在低地址部分
public final class ByteOrder {
public static final ByteOrder BIG_ENDIAN;
public static final ByteOrder LITTLE_ENDIAN;
public static ByteOrder nativeOrder(); //返回本机的字节顺序
public String toString();
}
//同时每个缓冲区类都能够通过order()方法来查看当前的字节顺序
class ByteOrderTest{
public static void main(String[] args){
System.out.println(ByteOrder.nativeOrder()); //返回本机的字节顺序
CharBuffer charBuffer = CharBuffer.wrap("Hello world".toCharArray());
System.out.println(charBuffer.order()); //除了ByteBuffer以外的所有缓冲区类的字节顺序是只读的,不能修改
ByteBuffer byteBuffer = ByteBuffer.wrap("Hello".getBytes());
System.out.println(byteBuffer.order()); //ByteBuffer的默认字节顺序是大端的,Java的默认字节顺序也是大端的
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); //ByteBuffer的字节顺序是可以修改的
System.out.println(byteBuffer.order());
}
}
------------------------------------------------------------------------
LITTLE_ENDIAN
LITTLE_ENDIAN
BIG_ENDIAN
LITTLE_ENDIAN
视图缓冲区
由于I/O是字节数据的传输,但是我们有的时候需要以不同的方式来解读这些字节数据,需要将其映射为其它的数据类型,因此就要建立字节缓冲区的视图缓冲区(“视图”可以理解为对同一事物在不同角度的看法)
ByteBuffer类提供了以下方法将字节缓冲区映射为其他原始数据类型
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
public abstract ShortBuffer asShortBuffer();
public abstract IntBuffer asIntBuffer();
public abstract LongBuffer asLongBuffer();
public abstract FloatBuffer asFloatBuffer();
public abstract DoubleBuffer asDoubleBuffer();
public abstract CharBuffer asCharBuffer();
}
//视图缓冲区
class Direacted{
public static void main(String[] args){ //由于直接缓冲区是在操作系统级别分配的内存,属于内核态,因此它的备份数组是用户态无法访问的,故hasArray的返回值为false
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10).order(ByteOrder.LITTLE_ENDIAN); //在此处大端小端不同,将来转换为别的数据类型进行编码时得到的结果不同
System.out.println(byteBuffer.isReadOnly()+" "+byteBuffer.hasArray()+" "+ byteBuffer.order()+" "+ByteOrder.nativeOrder()); //可以看出ByteBuffer默认大端,Intel处理器为小端
byteBuffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
byteBuffer.flip(); //由于视图缓冲区和基础缓冲区共享position到limit之间的数据,所以在这里将重置基础缓冲区的popsition和limit,新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型中组成一个数据类型的字节数(向下舍入)
CharBuffer charBuffer = byteBuffer.asCharBuffer(); //创建直接字节缓冲区的字符视图缓冲区
while(byteBuffer.hasRemaining()){
System.out.print(byteBuffer.get()+" ");
}
System.out.println();
while(charBuffer.hasRemaining()){
System.out.print(charBuffer.get()+" ");
}
System.out.println();
System.out.println(charBuffer.isDirect()+" "+charBuffer.isReadOnly()+" "+charBuffer.hasArray()); //由直接缓冲区创建的视图缓冲区也是直接缓冲区(因为它操作的数组空间是越过JVM堆栈创建的)
System.out.println();
}
}
------------------------------------------------------------------------
false false LITTLE_ENDIAN LITTLE_ENDIAN
72 101 108 108 111
效 汬
true false false