前言
前面已经大概的了解了NIO和NIO的三个组件
Java网络编程(4)NIO的理解与NIO的三个组件
并且编程了一个简单的聊天系统
现在详细的理解Buffer的作用与使用
目录
- Buffer
- 简单案例
- 四个属性
- 实例化
- 其他重要方法
5.1. 四个属性的方法
5.2. 翻转flip()
5.3. 清除clear()
5.4. get方法
5.5. put方法
5.6. ByteBuffer转换
5.7. 一些获得Buffer信息的方法 - ByteBuffer类型化
- Buffer与Channel的交互
- 总结
Buffer
首先应该知道NIO的结构:
在NIO结构中,缓存区Buffer位与通道Channel和客户端之间,本质上是一个读写数据的内存块,是一个容器对象,底层维护着一个数组,该对象有一些属性和方法用于使用该内存块,Channel提供数据流动的渠道,但读写的数据都必须经过Buffer
Buffer类:
最顶层的是Buffer抽象类,有四个重要的属性和一些方法,有7个子类,对应着Java的7种基本数据类型(没有Boolean)
能与Channel交互的只有ByteBuffer,所以使用其他的子类最后还要转换为ByteBuffer
简单案例
package com.company.NIOBuffer;
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
//实例化Buffer
IntBuffer intBuffer =IntBuffer.allocate(5);
//capacity()返回Buffer容量
for (int i=0;i<intBuffer.capacity();i++){
//put放入
intBuffer.put(i*2);
}
//翻转读写操作
intBuffer.flip();
//hasRemaining()返回布尔值告诉是否达到缓冲区上界
while (intBuffer.hasRemaining()){
//get()返回当前位置的值
System.out.println(intBuffer.get());
}
}
}
四个属性
- 容量( Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变,永远不能为负数。
- 上界( Limit)
本质是缓冲区底层数组的下标index,缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数,limit永远不能为负数且不能大于Capacity。
- 位置( Position)
下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
- 标记( Mark)
一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined),mark如果在大于position或者limit时,会被丢弃
顺序:mark <= position <= limit <= capacity
Buffer维护底层数组:
这四个属性在各种方法中都会使用到,可以Debug看看这些属性在方法中的变化
实例化
Buffer、ByteBuffer等类都是抽象类
抽象类无法实例化,每种子类都提供了四个实例化方法
下面以ByteBuffer为例
ByteBuffer提供了四个静态工厂方法得到ByteBuffer实例
这四个方法:
-
allocate(int capacity)
从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器(HeapByteBuffer实例) -
allocateDirect(int capacity)
是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区 -
wrap(byte[] array)
这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组(本质也是HeapByteBuffer实例) -
wrap(byte[] array, int offset, int length)
在上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset)
上面的案例:
IntBuffer intBuffer =IntBuffer.allocate(5);
使用allocate(5)方法创建了容量为5的Buffer
hb是我们的底层数组,mark标记(没有使用到),position当前位置为0,limit上界为5,capacity数组大小为5
wrap方法实例化:需要一个数组
int[] ints=new int[]{1,2,3,4,5,6};
IntBuffer buffer = IntBuffer.wrap(ints);
可以看到已经把数组放入Buffer中
wrap方法带参数实例化:
IntBuffer buffer = IntBuffer.wrap(ints,2,2);
设置的两个参数是限制position和limit的,数组还是全部放进Buffer了
其他重要方法
四个属性的方法
- capacity()
返回Buffer容量 - position()
获得缓冲区当前位置 - position(int)
设置缓冲区的位置 - limit()
返回缓冲区限制 - limit(int)
设置缓冲区的限制 - mark()
在缓冲区当前位置设置标记
翻转flip()
操作缓冲区的很重要的方法flip(),翻转读写操作
上例中:
//capacity()返回Buffer容量
for (int i=0;i<intBuffer.capacity();i++){
//put放入
intBuffer.put(i*2);
}
//翻转读写操作
intBuffer.flip();
//hasRemaining()返回布尔值告诉是否达到缓冲区上界
while (intBuffer.hasRemaining()){
//get()返回当前位置的值
System.out.println(intBuffer.get());
}
将i*2放入了Buffer,全部放入Buffer的属性:
flip后:
很明显,翻转flip方法就是将position重置
看源代码:
执行了3步
接下来进行读操作就可以重新开始
清除clear()
package com.company.NIOBuffer;
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
//实例化Buffer
IntBuffer intBuffer =IntBuffer.allocate(5);
//capacity()返回Buffer容量
for (int i=0;i<intBuffer.capacity();i++){
//put放入
intBuffer.put(i*2);
}
//翻转读写操作
intBuffer.flip();
//hasRemaining()返回布尔值告诉是否达到缓冲区上界
while (intBuffer.hasRemaining()){
//get()返回当前位置的值
System.out.println(intBuffer.get());
}
intBuffer.clear();
while (intBuffer.hasRemaining()){
//get()返回当前位置的值
System.out.println(intBuffer.get());
}
}
}
clear方法将缓冲区的各个标记重置到初始位置,不会清除缓冲区数据
清除前:
清除后:
清除将position初始化了,所以可以再读一遍数据
看源代码:
确实是重置了3个标记属性
get方法
不同的子类get的数据类型不同,IntBuffer得到的当然是Int类数据
get有四种类型:
- get():相对方法,读取当前位置的数据,然后position +1
- get(int[] dst):相对体积方法,将此缓冲区传输到给定的目标数组中的数据
- get(int[] dst, int offset, int length) :从当前位置开始相对读,读length个数据,并写入dst下标从offset到offset+length的区域
- get(int index) : 绝对方法,从指定下标开始读取
put方法
当然还是不同子类不同类型
- put(int i):相对写,向position的位置写入一个数据,并将postion+1,为下次读写作准备
- put(int index, int i):绝对写,向Buffer底层的数组中下标为index的位置插入数据,不改变position
- put(int[] src):用相对写,把src数组写入此缓冲区
- put(int[] src, int offset, int length):从src数组中的offset到offset+length区域读取数据并使用相对写写入此缓冲区
- put(IntBuffer src):将另一个缓冲区的数据写入本缓冲区
3种情况会报错:src为本缓冲区、缓冲区只读、src元素比本缓冲区元素多
ByteBuffer转换
asIntBuffer()等,将ByteBuffer当做转换的Buffer使用
byteBuffer.asIntBuffer().put(5);
System.out.println(byteBuffer.getInt());
通过这种方法可以将ByteBuffer转换成我们想要的Buffer
一些获得Buffer信息的方法
- remaining() :int
返回当前位置与限制之间的元素数 - hasRemaining() : boolean
告知当前位置与限制之间是否有元素 - isReadOnly() : boolean
此缓冲区是否为只读缓冲区 - hasArray()
告知此缓冲区是否有可访问的底层数组 - array()
返回底层数组(也就是上面截图的hb) - asReadOnlyBuffer()
将Buffer设置为只读Buffer
等等还有很多
ByteBuffer类型化
ByteBuffer有一些不同,ByteBuffer中可以put不同类型数据
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
byteBuffer.putChar('a');
但是取出的时候也要设置该类型,不然就会报错
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
System.out.println(byteBuffer.get());
}
byteBuffer.flip();
System.out.println(byteBuffer.getChar());
当放入多个属性时,要按照放入的顺序取出
ByteBuffer byteBuffer = ByteBuffer.allocate(200);
byteBuffer.putChar('a');
byteBuffer.putInt(1);
byteBuffer.putDouble(22.22);
byteBuffer.flip();
System.out.println(byteBuffer.getChar());
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getDouble());
Buffer与Channel的交互
Java网络编程(4)NIO的理解与NIO的三个组件
上一篇有Buffer的使用案例
总结
- Buffer是在Channel通道与客户端之间的内存块,读写数据必须经过Buffer
- Buffer底层是一个数组,有四个属性操作数组:mark、position、limit、capacity
- Buffer是个抽象类,有7个抽象类子类,分别对应Java的7种基本数据类型(没有boolean),其中能与Channel交互的只有ByteBuffer
- Buffer实例化有四种方法,还有一些其他重要的方法,这些方法本质上是通过四个属性操作底层数组实现