NIO-缓冲区

本文作者:杨龙,叩丁狼高级讲师。原创文章,转载请注明出处。

定义

I/O 操作也就是向操作系统发出请求,让它要么把缓冲区里的数据排干(写),要么用数据把缓冲区填满(读)。那么在 NIO 中使用 Buffer 进行对缓冲区抽象。概念上,缓冲区是包在一个对象内的基本数据元素数组,直白说就是数据的容器。Buffer 类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中。Buffer 类以及它专有的子类定义了一个用于处理数据缓冲区的 API。

继承体系

  • Buffer,注意其是一个抽象类。
    Buffer体系图.png

缓冲区属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:

  • 容量(Capacity),缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变
  • 上界(Limit),缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。缓冲区对象创建时其值等于缓冲区容量值,值可以改变。
  • 位置(Position),下一个要被读或写的元素的索引。位置会自动由相应的 get 和 put 方法更新。
  • 标记(Mark),一个备忘位置。调用 mark() 来设定 mark = postion。调用 reset() 设定 position = mark。标记在设定前是 -1 。(参见源码)

这四个属性之间总是遵循以下关系:mark <= position <= limit <= capacity。

ByteBuffer byteBuffer = ByteBuffer.allocate(10); // 创建一个容量为 10 的字节缓冲区对象
System.out.println(byteBuffer.position());       // 获取缓存区位置值,值为 0
System.out.println(byteBuffer.limit());          // 获取缓存区上界值,值为 10
System.out.println(byteBuffer.capacity())        // 获取缓存区容量值,值为 10

缓冲区属性示意图.png

缓冲区 API

缓冲区API.png

  • 方法返回是 Buffer 可链式调用,产生简洁,优美,易读的代码。
  • 每个具体的缓冲区类都通过执行 isReadOnly() 来标示其是否允许该缓存区的内容被修改,对只读的缓冲区的修改尝试将会导致
    ReadOnlyBufferException 抛出。

存取

存取方法.png

  • 每一个 Buffer 类都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一
    的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。
  • 通过 Buffer 的 API get 方法可从缓冲区中获取数据;通过 Buffer 的 API put 方法可往缓冲区中存放数据。
  • 相对存取(没带位置参数),执行完位置值加一,使用 put ,若位置不小于上界,就会抛出 BufferOverflowException 异常;使用 get,若位置不小于上界,就会抛出 BufferUnderflowException 异常。
  • 绝对存取(带了位置参数),不会影响缓冲区的位置属性,但若所提供的索引超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte)'a');                      // 对当前位置 0 存入一个值,没有带位置参数,导致位置加一
System.out.println(byteBuffer.position());      // 1
System.out.println(byteBuffer.limit());         // 10
System.out.println(byteBuffer.capacity());      // 10
System.out.println(byteBuffer.get(0));          // 取出指定位置为 0 上的值,结果 97

存取效果.png

替换

缓冲区中存放了一些数据,若想在更改位置值的情况下对缓冲区的数据进行修改,使用带有位置参数的 put 方法可以达到这个目的。

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte)'a');
byteBuffer.put((byte)'b');  
byteBuffer.put((byte)'c');  
byteBuffer.put(0, (byte)'d'); // 替换位置 0 的值为 100

替换效果.png

翻转释放

如何获取缓冲区的数据,如获取上面存的三个数据。

for(int i = 0; i < byteBuffer.position(); i++){
    System.out.println(byteBuffer.get(i));
}

翻转

  • 可使用缓冲区的 flip 方法,翻转缓冲区,来设置上限值和重置 position 值。但区分 rewind 方法,这个方法仅仅只是把位置值重置为 0。
// 通过 byteBuffer.flip() 翻转缓冲区, 即把 limit = position,再把 position = 0
byteBuffer.flip(); // 等价于 byteBuffer.limit(byteBuffer.position()).position(0);
System.out.println(byteBuffer.position()); // 0
System.out.println(byteBuffer.limit());    // 3

翻转效果.png

释放

  • remaining 方法将告知您从当前位置到上界还剩余的元素数目。
  • hasRemaining 方法可释放缓冲区时告诉您是否已经达到缓冲区的上界。
int count = byteBuffer.remaining();
for (int i = 0; i < count; i++) {
    System.out.println(byteBuffer.get());
}
while (byteBuffer.hasRemaining()) {
    System.out.println(byteBuffer.get());
}

压缩

  • 使用缓冲区的 compact 方法会导致上界属性被设置为容量的值,因此缓冲区可以被再次填满。调用 compact 的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte)'a');
byteBuffer.put((byte)'b');
byteBuffer.put((byte)'c');
byteBuffer.flip();

System.out.println(byteBuffer.position());  // 0
System.out.println(byteBuffer.limit());     // 3
System.out.println(byteBuffer.capacity());  // 10

System.out.println(byteBuffer.get());       // 97

System.out.println(byteBuffer.position());  // 1
System.out.println(byteBuffer.limit());     // 3
System.out.println(byteBuffer.capacity());  // 10

压缩前.png

byteBuffer.compact(); // 压缩
System.out.println(byteBuffer.position());  // 2
System.out.println(byteBuffer.limit());     // 10
System.out.println(byteBuffer.capacity());  // 10

System.out.println(byteBuffer.get(0));      // 98
System.out.println(byteBuffer.get(1));      // 99
System.out.println(byteBuffer.get(2));      // 99

压缩效果.png

标记

  • 标记,使缓冲区能够记住一个位置并在之后将其返回。
  • 缓冲区的标记在 mark 方法被调用之前是未定义的,调用时 mark = position;而带调用 reset 方法 position = mark,但标记值未定义,调用 reset 将导致 InvalidMarkException 异常。
  • 一些缓冲区方法会抛弃已经设定的标记(rewind,clear,以及 flip 总是抛弃标记)。
  • 若新设定的值比当前的标记小,调用 limit 或 position 带有索引参数的版本也会抛弃标记。
  • 不要混淆 reset 和 clear。clear 方法将清空缓冲区,重置 position = 0,limit = capacity,丢失标记值,但之前存数据依然还在。
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte)'a');
byteBuffer.put((byte)'b');
byteBuffer.put((byte)'c');
byteBuffer.position(2).mark().position(4);          // 此时 mark = 2,position = 4
byteBuffer.reset();                                 // 此时 mark = 2,position = 2
byteBuffer.clear();                                 // 此时 mark = -1,position = 0,limit = 10,capacity = 10

比较

  • 缓冲区比较使用 equals 方法,比较规则如下:
    • 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer 绝不会等于非 buffer 对象。
    • 两个对象都剩余同样数量的元素。buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
    • 在每个缓冲区中应被 get 函数返回的剩余数据元素值必须一致。
    • 即同时满足以上条件才为 true,否则为 false。
ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
byteBuffer1.put((byte)'a');
byteBuffer1.put((byte)'b');
byteBuffer1.put((byte)'c');
byteBuffer1.flip();
ByteBuffer byteBuffer2 = ByteBuffer.allocate(10);
byteBuffer2.put((byte)'a');
byteBuffer2.put((byte)'b');
byteBuffer2.put((byte)'c');
byteBuffer2.flip();
System.out.println(byteBuffer1.equals(byteBuffer2)); // true

批量存取

  • 缓冲区的涉及目的就是为了能够高效传输数据。一次移动一个数据元素那样并不高效。缓冲区 API 提供了向缓冲区内外批量存取数据元素方法:
    批量存取方法.png

  • get 可供从缓冲区到数组进行的数据复制使用。

    • 第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。
    • 第二种形式使用 offset 和 length 参数来指定目标数组的子区间,把缓冲区里的数据释放到数组这区间上。
    • 若将一个小型缓冲区传入一个大型数组,需要明确地指定缓冲区中剩余的数据长度,否则不会有数据被传递,并且抛出 BufferUnderflowException 异常。
  • put 是以相反的方向移动数据,从数组移动到缓冲区。

    • 第一种形式只将一个数组作为参数,将一个数组里的数据存放到缓冲区中。

    • 第二种形式使用 offset 和 length 参数来指定目标数组的子区间,把数组这区间的的数据存放到缓冲区中。

    • 若缓冲区中没有足够的空间,那么不会有数据被传递,同时抛出一个 BufferOverflowException 异常

    • 调用带有一个缓冲区引用作为参数的 put()来在两个缓冲区内进行批量传递。

    • 若源缓冲区的 remaining 大于目标缓冲区的 remaining,那么数据不会被传递,同时抛出 BufferOverflowException 异常。

    • 若将一个缓冲区传递给它自己,就会引发 java.lang.IllegalArgumentException 异常。

ByteBuffer byteBuffer = ByteBuffer.allocate(5);
byte[] byteArray = new byte[10];
System.out.println(byteBuffer.remaining()); // 返回是 limit - position,结果是 5
// byteBuffer.get(byteArray); 缓冲区里的数据不够填充数组,即 5 < 10 抛出异常 java.nio.BufferUnderflowException
// byteBuffer.put(byteArray); 数组里数据过多,缓冲区存不下,即 10 > 5 抛出异常 java.nio.BufferOverflowException
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
byte[] byteArray = new byte[5];
byteBuffer.get(byteArray);  // 等价于:byteBuffer.get(byteArray, 0, byteArray.length);
byteBuffer.put(byteArray);  // 等价于:byteBuffer.put(byteArray, 0, byteArray.length);
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byte [] bigArray = new byte[1000];
// 这里使用的前提是缓冲 remaining = limit - position,其值小于等于数组长度
byteBuffer.get(bigArray, 0, byteBuffer.remaining());

// 当数组小于缓存区的 remaining,分多次释放
byte [] smallArray = new byte[5];
while (byteBuffer.hasRemaining()) {
    int length = Math.min (byteBuffer.remaining(), smallArray.length);
    byteBuffer.get(smallArray, 0, length);
}


ByteBuffer srcBuffer = ByteBuffer.allocate(10);
ByteBuffer dstBuffer = ByteBuffer.allocate(10);
dstBuffer.put(srcBuffer); // 只有 dstBuffer 有足够的空间才这样使用,其等价于下面的代码
while (srcBuffer.hasRemaining()) {
    dstBuffer.put(srcBuffer.get());
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/87365100