NIO 三 缓冲区数据读写

一 自增读写

  先打预防针,缓冲区的读写复用是通过数据写入覆盖实现的,所谓数据清理不过是移动核心参数值,实际上数据写入后会永久存在,这意味着在读写缓冲区数据时,必须要时刻了解position、limit、mark和capacity。

  读写数据时对position参数的依赖非常强,很多时候都可以将position作为下一次读写数据对应的数组的下标索引值。

  Buffer的直接派生类均提供了一种position自增的读写方法:

  put([对应缓冲区数据类型的值类型] data)/get()

  自增读写指的是读取或写入N个数据时,position会自动增加N长度,如果可读写数据长度不足N,那么get方法会抛出BufferUnderflowException,put方法会抛出BufferOverflowException。

1.1 单数据自增读写

  单数据自增读写指的是每次通过缓冲区对象读写单个数据,并且position自增1,如ByteBuffer中提供了如下方法:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public abstract ByteBuffer put(byte b);
	public abstract byte get();
	...
}

  ByteBuffer的派生类实现了此方法,如HeapByteBuffer:

class HeapByteBuffer extends ByteBuffer {
	...
	public ByteBuffer put(byte x) {
		hb[ix(nextPutIndex())] = x;
		return this;
    }
	public byte get() {
        return hb[ix(nextGetIndex())];
    }
	...
}

  其中nextPutIndex和nextGetInex方法是基类Buffer提供的:

public abstract class Buffer {
	...
	final int nextPutIndex() {
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
	final int nextGetIndex() {
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
	...
}

  由此也印证了put/get因可读写数据长度不足导致的异常以及position自增。测试一下:

public class ByteBufferTest {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        for (int i = 0; i < byteBuffer.limit(); i++) {
            byteBuffer.put((byte) i);
            System.out.println("position:" + byteBuffer.position());
        }
        byteBuffer.flip();
        for (int i = 0; i < byteBuffer.limit(); i++) {
            System.out.println("position:" + byteBuffer.position());
            System.out.println(byteBuffer.get() + " position:" + byteBuffer.position());
        }
    }
}

输出结果:
capacity:3 limit:3 position:0
position:1
position:2
position:3
position:0
0 position:1
position:1
1 position:2
position:2
2 position:3

1.2 批量数据自增读写

  批量数据的自增读写实际可以认为是单数据自增读写的一个封装版本,这里还以ByteBuffer举例。

1.2.1 定长读写

  可以指定批量读写的长度,在ByteBuffer中此类读写方法的定义如下:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public ByteBuffer put(byte[] src, int offset, int length) {
		// 第一步就开始检查能够批量读写
        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        int end = offset + length;
		// 实际上还是通过put方法来遍历插入数据的,因此具有position递增特性
        for (int i = offset; i < end; i++)
            this.put(src[i]);
        return this;
    }
	public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
		// 第一步就开始检查能够批量读写
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
		// 实际上还是通过get方法来遍历读取数据的,因此具有position递增特性
        for (int i = offset; i < end; i++)
            dst[i] = get();
        return this;
    }
	...
}

  这里需要强调一下,批量的自增读写方法都要求传入供读写的数组对象,缓冲区读写开始位置的偏移量,需读写数据的长度。通过上面的源码也可以看出来,其中偏移量指的是本次读写于缓冲区中的起始位置,通过checkBounds方法来进行检查,此方法由基类Buffer提供:

public abstract class Buffer {
	...
	static void checkBounds(int off, int len, int size) {
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }
	...
}

  这意味着如果缓冲区中可读写的数据长度小于本次读写的数据长度时,将抛出越界异常。如果参数检查无误,那么后续的处理逻辑实际上就是遍历的读写数据,综上批量的自增读写方法实际上等价于下面的写法:

// 等价写
for (int i = offset; i < offset + length; i++) {
	put(src[i]);
}

// 等价读
for (int i = offset; i < offset + length; i++) {
	src[i] = get();
}
1.2.2 完全读写

  不同于定长读写,这里指将参数数组中的全部数据写入缓冲区,或从缓冲区读取数组长度的数据:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public final ByteBuffer put(byte[] src) {
        return put(src, 0, src.length);
    }
	public ByteBuffer get(byte[] dst) {
        return get(dst, 0, dst.length);
    }
	...
}

  从源码中可以看出,完全模式的读写仅仅是定长读写的一种简便写法,偏移量始终为0,读写数据的长度就是参数数组的长度。

1.3 剩余数据写入

  ByteBuffer还提供了一种position自增的批量写入模式,和批量的完全写模式差不多,但是参数是另一个缓冲区对象:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public ByteBuffer put(ByteBuffer src) {
        if (src == this)
            throw new IllegalArgumentException();
        if (isReadOnly())
            throw new ReadOnlyBufferException();
        int n = src.remaining();
        if (n > remaining())
            throw new BufferOverflowException();
        for (int i = 0; i < n; i++)
            put(src.get());
        return this;
    }
	...
}

  从源码中可以看出来只有非当前缓冲区引用、非只读模式、参数缓冲区的剩余数据长度小于目标缓冲区的可写入数据长度(即limit - position)时,才会执行数据写入动作,此时通过自增的put方法向目标缓冲区中写入,通过自增的get方法从参数缓冲区对象中读取,因此通过此方法写入数据后,目标缓冲区和参数缓冲区对象的position都会发生变化:

 public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        ByteBuffer byteBufferOther = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        byteBuffer.put(1, (byte)1);
        byteBuffer.put(2, (byte)2);
        byteBufferOther.put(byteBuffer);
        System.out.println("byteBuffer position:" + byteBuffer.position() + " byteBufferOther position:" + byteBufferOther.position());
}

输出结果如下:
capacity:3 limit:3 position:0
byteBuffer position:3 byteBufferOther position:3

二 目标读写

  前文中介绍的读写实际上是依赖自行维护值的position实现的,调用者不需要考虑如何移动position,那么当需要读写指定位置上的数据时,自增读写便无法应对。

  ByteBuffer中提供了带有数据位置参数的读写方法,声明如下:

```java
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	public abstract ByteBuffer put(int index, byte b);
	public abstract byte get(int index);
	...
}

  依然通过实现类HeapByteBuffer来查阅具体实现:

class HeapByteBuffer extends ByteBuffer {
	...
	public ByteBuffer put(int i, byte x) {
        hb[ix(checkIndex(i))] = x;
        return this;
    }
	public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }
	...

  注意,这里的put/get方法没有再使用nextPut/GetIndex,而是使用的CheckIndex方法,此方法由基类Buffer提供:

public abstract class Buffer {
	...
	final int checkIndex(int i) {
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
	...
}

  当向缓冲区目标位置读写数据时,如果参数位置小于零或者大于限制会抛出越界异常。方法内部没有对position参数进行值操作,因此使用目标位置读写模式,position不会自增,示例如下:

public class ByteBufferTest {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        System.out.println("capacity:" + byteBuffer.capacity() + " limit:" + byteBuffer.limit() + " position:" + byteBuffer.position());
        byteBuffer.put(1, (byte)1);
        byteBuffer.put(2, (byte)2);
        System.out.println("position:" + byteBuffer.position());
        System.out.println(byteBuffer.get(1));
        System.out.println("position:" + byteBuffer.position());
    }
}

输出结果如下:
capacity:3 limit:3 position:0
position:0
1
position:0

三 值类型数据读写

  在介绍创建缓冲区一文中提到过ByteBuffer特有的asXXBuffer方法,同样的在缓冲数据读写方面,ByteBuffer也提供了支持不同值类型的数据的读写方法,同前文中介绍的实现模式一样,针对值类型数据的读写也分为自增读写和目标位置读写:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
	...
	// 不带index参数的方法会自增position,增加的position值为值类型位数,如Char类型自增2
	public abstract ByteBuffer putChar(char value);
	public abstract char getChar();
	// 带index参数的方法不会自增position
	public abstract ByteBuffer putChar(int index, char value);
	public abstract char getChar(int index);
	// 其他的Doubule、Int等等类型的put/get方法不再赘述
	...
}

四 结语

  如果想关注更多硬技能的分享,可以参考积少成多系列传送门,未来每一篇关于硬技能的分享都会在传送门中更新链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬睡客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值