Java NIO基础之ByteBuffer学习


Java NIO使用的数据结构不是普通的byte[],而是类似ByteBuffer的类。ByteBuffer使用起来与byte[]有很大不同,下面主要是自己学习到的一些使用方法。

ByteBuffer同时可以读写,一开始理解有点困难,建议按照推荐的操作步骤来使用ByteBuffer。即

  1. Channel#read(ByteBuffer)或者ByteBuffer#put方法,往ByteBuffer里面写入数据
  2. ByteBuffer#flip
  3. Channel#write(ByteBuffer)或者ByteBuffer#get方法,从ByteBuffer里面读取数据
  4. ByteBuffer#clear, ByteBuffer#compact

读写模式通过不同的API来切换,一般情况下,按照上面这个顺序使用ByteBuffer就可以。不过要正确理解和使用ByteBuffer,需要对ByteBuffer的内部有所了解。

ByteBuffer主要有三个位置指针(不是C/C++的pointer)

  • position,当前位置
  • limit,当前模式下的位置限制
  • capacity,分配时确定的空间大小

capacity因为一开始就被确定了,最容易理解。剩下两个位置指针需要配合ByteBuffer的“读写模式”来理解。

  1. ByteBuffer被分配之后,默认是“写模式”,因为刚分配的空间你读取是没有意义的。这时position为0,limit为capacity的值。假设代码中执行了一些写入操作,每次写入都会增加position。
  2. flip切换读写模式,position为0,即你写入的第一个byte的位置,limit变成你写入数据的最后一个byte后面的位置。
  3. 接下来你可以从ByteBuffer中读取数据了,每次读取增加position。
  4. 读取完毕之后,可以复用这个ByteBuffer,调用clear的话,position重置为0,limit设置为capacity,即回到写模式的初始状态。调用compact的话,你没有读取的数据会向前移动(System.arrayCopy),position设置为这些数据末尾之后的位置,limit设置为capacity。

用代码来测试一下

@Test
public void testByteBufferNormal() {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    assertEquals(0, buffer.position());
    assertEquals(32, buffer.limit());
    assertEquals(32, buffer.capacity());

    // 1. write data into buffer
    buffer.put((byte) 1);
    buffer.put((byte) 2);
    buffer.put((byte) 3);
    assertEquals(3, buffer.position());
    assertEquals(true, buffer.hasRemaining());

    // 2. flip
    buffer.flip();
    assertEquals(0, buffer.position());
    assertEquals(3, buffer.limit());
    assertEquals(32, buffer.capacity());
    assertEquals(true, buffer.hasRemaining());

    // 3. read data from buffer
    assertEquals((byte) 1, buffer.get());
    assertEquals((byte) 2, buffer.get());
    assertEquals((byte) 3, buffer.get());

    assertEquals(3, buffer.position());
    assertEquals(false, buffer.hasRemaining());

    // 4. reset pointers
    buffer.clear();
    assertEquals(0, buffer.position());
    assertEquals(32, buffer.limit());
    assertEquals(32, buffer.capacity());

    // 1. write data into buffer
    buffer.put((byte) 10);
    assertEquals((byte) 10, buffer.array()[0]);
}

如果是普通开发的话,理解到这里就差不多了。不过ByteBuffer只通过位置指针来操作,多少会碰到误用的可能。以下就对于几种可能的误用测试一下。

@Test
public void testByteBufferZeroLength() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(0);
    assertEquals(false, buffer.hasRemaining());

    try {
        buffer.put((byte) 10);
        Assert.fail();
    } catch (BufferOverflowException ignored) {
    }
}

@Test
public void testByteBufferReadBeforeFlip() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    assertEquals(0, buffer.position());
    assertEquals(32, buffer.limit());
    assertEquals(32, buffer.capacity());

    assertEquals((byte) 0, buffer.get());
    assertEquals(1, buffer.position());
}

@Test
public void testByteBufferWriteAfterFlip() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    buffer.put(new byte[]{1, 2, 3});
    assertEquals(3, buffer.position());
    assertEquals(32, buffer.limit());

    buffer.flip();
    assertEquals(0, buffer.position());
    assertEquals(3, buffer.limit());

    assertEquals((byte) 1, buffer.array()[0]);
    buffer.put((byte) 4);
    assertEquals(1, buffer.position());
    assertEquals((byte) 4, buffer.array()[0]);
}

@Test
public void testByteBufferNoMoreToRead() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    buffer.put(new byte[]{1, 2});
    buffer.flip();

    buffer.get();
    buffer.get();
    assertEquals(2, buffer.position());
    assertEquals(2, buffer.limit());
    assertEquals(false, buffer.hasRemaining());

    try {
        buffer.get();
        Assert.fail();
    } catch (BufferUnderflowException ignored) {
    }
}

第一个,ByteBuffer长度设置为0的情况。API中要求长度不能为负数,所以0还是可以的。不过你没有用这个ByteBuffer读写就是了。另外注意下,不能继续写的时候抛出的异常。

第二个,当你不知道flip是否被调用,不小心在“写模式”下调用了读操作,结果只是position向前移动。这说明ByteBuffer对于读写模式确实要求并不严格,而且只用读写指针是无法判断读写模式的。

第三个,同样,在flip之后调用写操作,本应该被读取的数据被你覆盖了。

第四个,没有继续可读的数据的时候跑出BufferUnderflowException异常。

另外在“读模式”下,与常规的输入流一样,你可以用rewind,mark/reset。

@Test
public void testByteBufferRewindUsage() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    buffer.put(new byte[]{1, 2, 3});
    buffer.flip();

    assertEquals((byte) 1, buffer.get());
    assertEquals((byte) 2, buffer.get());
    assertEquals((byte) 3, buffer.get());
    assertEquals(3, buffer.position());

    buffer.rewind();
    assertEquals(0, buffer.position());
    assertEquals((byte) 1, buffer.get());
}

@Test
public void testByteBufferMarkAndReset() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    buffer.put(new byte[]{1, 2, 3});
    buffer.flip();

    buffer.get();
    assertEquals(1, buffer.position());

    buffer.mark();
    assertEquals(1, buffer.position());

    buffer.get();
    assertEquals(2, buffer.position());

    buffer.reset();
    assertEquals(1, buffer.position());
}

最后,涉及到数据传输的ByteOrder,当你往ByteBuffer里面写入一个字节以上的单个数据的话,比如整型,ByteBufer的position会一下子向前移动4,具体代码如下:

@Test
public void testByteBufferPutInt() throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(32);
    assertEquals(ByteOrder.BIG_ENDIAN, buffer.order());

    buffer.putInt(10);
    assertEquals(4, buffer.position());
    assertEquals(32, buffer.limit());

    buffer.flip();
    assertEquals(4, buffer.limit());

    assertEquals((byte) 0, buffer.get());
    assertEquals((byte) 0, buffer.get());
    assertEquals((byte) 0, buffer.get());
    assertEquals((byte) 10, buffer.get());
}

到此为止,ByteBuffer的基本使用就结束了,希望对各位有用。

参考: