欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

NIO中缓冲区的API介绍

程序员文章站 2022-04-18 18:32:26
...

[转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. ]

在java NIO中, 通道是IO传输发生时数据通过的入口, 而缓冲区是数据的来源或目标. 

Buffer是java NIO中定义的所有缓冲区类的基类.

 

Buffer的属性

1. 容量(capacity)

缓冲区能够容纳的数据元素的最大数量, capacity在创建缓冲区时指定, 之后无法改变.

2. 上界(limit)

读模式下, limit表示能读取的最后一个字节; 写模式下, limit等于capacity.

3. 位置(position)

position表示当前索引.

4. 标记(mark)

mark表示一个备忘position.

属性之间的关系:0 <= mark <= position <= limit <= capacity

 

Buffer中与属性相关的API

1. int capacity(). 该方法返回Buffer的容量.

2. clear(). 该方法并不改变Buffer中已有的任何数据, 只是将position设置为0, limit设置为capacity. 一般在向缓冲区写入数据之前调用.

3. flip(). 将Buffer从写模式反转成读模式. 该方法将limit设置为当前position, 然后将position设置为0. 一般在读取缓冲区数据之前调用.

4. boolean hasRemaining(). 当前position和limit之间是否有元素.

5. int limit(). 返回Buffer的limit.

6. limit(int newLimit). 设置Buffer的limit.

7. mark(). 标记当前position. 

8. int position(). 返回当前的position.

9. position(int newPosition). 设置position.

10. int remaining(). 返回position和limit之间的元素个数.

11. reset(). 将position设置为之前mark的位置. 如果mark无效, 将抛出InvalidMarkException异常.

12. rewind(). 将position设置为0, 并抛弃mark. 不改变limit的值. 一般用于重新读取缓冲区中的数据.

 

填充和释放缓冲区

由于Buffer的各个子类的填充和释放缓冲区的方法所需的参数不一致, 所以没有将这些方法抽象到Buffer类中. 以ByteBuffer为例, 其填充和释放缓冲区方法有:

1. byte get(). 相对get操作, 该方法返回当前position处的byte数据, 并将position加1. 如果当前position大于等于limit, get()方法将抛出BufferUnderflowException异常.

2. byte get(int index). 绝对get操作, 该方法返回index处的byte数据, 不position的值. 如果指定的index小于0, 或者大于等于limit, 将方法将抛出IndexOutOfBoundsException异常.

3. get(byte[] dst, int offset, int length). 相对批量get操作, 与多次get操作比较, 该方法具有更高的效率. 如果length大于缓冲区的剩余字节, 将抛出BufferUnderflowException异常. 如果缓冲区存有比数组能容纳的数量更多的数据, 可以使用如下的方式处理:

char[] smallArray = new char[10];
while (buffer.hasRemaining()) {
	int length = Math.min(buffer.remaining(), smallArray.length);
	buffer.get(smallArray, 0, length);
	processData(smallArray, length);
}

4. put(byte b). 相对put操作, 该方法将byte数据存入position处, 并使得position加1. 如果当前position大于等于limit, 该方法将抛出BufferOverflowException异常.

5. put(int index, byte b). 绝对put操作, 该方法将byte数据存入index处, 不改变position的值. 如果指定的index小于0, 或者大于等于limit, 将方法将抛出IndexOutOfBoundsException异常.

6. put(byte[] src, int offset, int length). 相对批量put操作, 与多次put操作比较, 该方法具有更高的效率. 如果length大于缓冲区的剩余字节, 将抛出BufferOverflowException异常.

7. compact(). 该方法将当前position和limit之间的数据拷贝到缓冲区开始部分, 并将position设置为拷贝的数据的个数, limit设置为capacity.

 

创建缓冲区

Buffer的子类如ByteBuffer, CharBuffer等都是抽象类, 提供静态方法创建缓冲区对象, 以ByteBuffer为例:

1. allocate(int capacity). 创建一个指定capacity的ByteBuffer对象. 该方法返回的ByteBuffer对象底层维护了一个byte数组, 数组的length为指定的capacity.

2. wrap(byte[] array). 使用指定的byte数组创建缓冲区. 调用put操作造成的改动会直接影响这个数组, 对数组的改动也会对该ByteBuffer对象可见.

3. wrap(byte[] array, int offset, int length). 与上个方法类似, 但是使用offset初始化ByteBuffer对象的position, 使用length+offset初始化ByteBuffer对象的limit.

ByteBuffer类提供了获取其底层实现数组的方法:

1. boolean hasArray(). 该方法的返回值表明是否可以获取到ByteBuffer对象的底层实现数组. 如果该方法返回true, 则可以安全的调用array()和arrayOffset()方法. 如果缓冲区为只读缓冲区, 则该方法返回false.

2. byte[] array(). 该方法返回ByteBuffer对象的底层实现数组.

3. int arrayOffset(). 该方法返回缓冲区数据在数组中存储的开始位置的偏移量.

 

直接缓冲区

通过缓冲区类的allocateDirect(int capacity)方法可以创建相应的直接缓冲区, 缓冲区对象的isDirect方法的返回值表明该缓冲区是否为直接缓冲区. 直接缓冲区具有更好的IO效率, 同时, 创建和销毁直接缓冲区需要更大的代价. 直接缓冲区使用的内存是通过调用本地操作系统的代码分配的, 而不是在JVM堆栈中分配的, 使用直接缓冲区可能引入平台依赖. 

总之, 使用直接缓冲区需要谨慎权衡: 如果IO效率不是系统的瓶颈, 最好不要使用直接缓冲区.

 

字节顺序

多字节数据的存储顺序在不同的平台上可能不同, 有的平台默认使用big-endian(大端字节顺序), 有的平台则默认使用little-endian(小端字节顺序). big-endian的数据的高位存储在低位地址上, little-endian正好与之相反, 平台的默认字节顺序一般是由硬件决定的.

在java中, 字节顺序由ByteOrder类封装. ByteOrder类只有2个对象(其构造函数是private的)--ByteOrder.BIG_ENDIAN和ByteOrder.LITTLE_ENDIAN, 分别代表大端和小端字节顺序. ByteOrder.nativeOrder()方法返回当前平台的字节顺序.

所有缓冲区类都具有ByteOrder order()方法, 返回缓冲区所使用的字节顺序. ByteBuffer之外的缓冲区类的字节顺序是一个只读属性(一旦创建就确定了, 且无法修改): 通过allocate或wrap方法创建的缓冲区的字节顺序和ByteOrder.nativeOrder()方法的返回值相同, 如果缓冲区是另一个缓冲区的视图, 则视图的字节顺序和原始缓冲区的自己顺序相同.

ByteBuffer类则比较特殊, 可以通过order(ByteOrder bo)方法随时改变ByteBuffer对象的字节顺序, 如果没有指定, ByteBuffer的默认字节顺序为ByteOrder.BIG_ENDIAN.

public static void main(String[] args) {
	// 默认字节顺序为ByteOrder.BIG_ENDIAN
	ByteBuffer bb = ByteBuffer.allocate(4);
	bb.putInt(0xabcd1234);
	bb.flip();
	System.out.println(Integer.toHexString(bb.getInt()));
	// 改变其字节顺序
	bb.order(ByteOrder.LITTLE_ENDIAN);
	bb.flip();
	System.out.println(Integer.toHexString(bb.getInt()));
}
 

缓冲区视图

缓冲区视图和原始缓冲区共享全部或部分数据, 但是具有独立的capacity, limit, position, mark属性.

所有缓冲区类都有以下3个API用于创建其视图, 以CharBuffer为例:

1. CharBuffer duplicate(). 创建一个缓冲区视图, 视图和原始缓冲区共享全部数据, 对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上, 并继承原始缓冲区的direct和readOnly属性. 但是具有独立的capacity, limit, position, mark.

public static void main(String[] args) {
	CharBuffer cb = CharBuffer.allocate(100);
	cb.put("xing");
	CharBuffer view = cb.duplicate();
	// 视图的capacity, limit, position等属性初始值和原始缓冲区相同
	printProperty(cb);
	printProperty(view);

	// 视图继承原始缓冲区的direct和readOnly属性
	System.out.println(view.isDirect());
	System.out.println(view.isReadOnly());
	
	// 但是视图的capacity, limit, position等属性是独立的
	view.put(" zhang");
	printProperty(cb);
	printProperty(view);
	
	// 视图和原始缓冲区共享数据, 修改了视图会影响到原始缓冲区
	cb.position(view.position());
	cb.flip();
	char[] dst = new char[cb.limit()];
	cb.get(dst, 0, dst.length);
	System.out.println(new String(dst));
}

private static void printProperty(CharBuffer cb) {
	System.out.println(cb.position() + "@" + cb.limit() + "@" + cb.capacity());
}

2. CharBuffer asReadOnlyBuffer(). 创建只读缓冲区视图. 只读缓冲区视图不允许使用put操作, 并且其isReadOnly()函数将会返回true. 其余和duplicate()方法返回的缓冲区视图完全相同.

3. CharBuffer slice(). slice方法返回的缓冲区视图和duplicate()方法返回的缓冲区视图区别在于, slice视图只和原始缓冲区共享部分数据元素, slice视图的初始属性值分别为: position是0, limit和capacity为原始缓冲区的limit-position. 其余部分和duplicate视图完全相同.

除了以上3个API, ByteBuffer类提供了其特有的创建视图缓冲区的API:

public abstract CharBuffer asCharBuffer();   
public abstract ShortBuffer asShortBuffer();  
public abstract IntBuffer asIntBuffer();  
public abstract LongBuffer asLongBuffer();   
public abstract FloatBuffer  asFloatBuffer();  
public abstract DoubleBuffer asDoubleBuffer();

由于char, short, int, long, float, double都是多字节数据, 因此ByteBuffer的字节顺序决定了多个字节以怎样的形式组织成一个数据.