Java NIO 教程 (三) 缓冲区

NIO缓冲区用于跟Channels进行互动.正如上篇文章所说到的,数据从缓冲区读出到通道,从通道写入至缓冲区

缓冲区实际上是一块可以读写的内存区.内存区被缓存区对象封装起来,并且提供了一些可以让我们更加方便操作这块内存区的方法

缓冲区基本用法

用缓冲区来读写数据基本上分为以下4步:

  1. 向缓冲区中写入数据
  2. 调用flip()方法
  3. 从缓冲区中读出数据
  4. 调用clear()或者compact()方法

当你向缓冲区中写入数据后,缓冲区对象会记录你向其中写入了多少数据.当你需要读取数据时,你需要把缓冲区从写模式切换到读模式(通过调用flip()方法).在读模式中,缓冲区可以令你读取所有写在缓冲区中的数据.(译者注:切换模式在实现上其实只是向两个状态变量赋值,所以切换模式的开销很小,速度快)

当你读取了所有的数据后,你需要清理缓冲区,使我们能够再次向其写入数据.你可以用两种方式达到同样的效果:调用clear()方法,或者调用compact()方法.clear()方法清除整个缓冲区(将整个缓冲区中的数据置为初始值,时间为O(n)),或者调用compact()方法,它只会清除你已经读取的数据,所有未读取的数据将会移至缓冲区的开头,数据会在未读取的数据之后开始向缓冲区写入

以下是一个简单的缓冲区使用的例子,在其中我们用到了write,flip,read和clear方法

public class BasicFileChannelTest {
   public static void main(String[] args) throws Exception{
      RandomAccessFile aFile = new RandomAccessFile("data\\nio-data.txt", "rw");
      FileChannel inChannel = aFile.getChannel();

      ByteBuffer buf = ByteBuffer.allocate(48);

      int bytesRead = inChannel.read(buf);
      while (bytesRead != -1) {

         System.out.println("Read " + bytesRead);
         buf.flip();
         while(buf.hasRemaining()){
            System.out.print((char) buf.get());
         }

         buf.clear();
         bytesRead = inChannel.read(buf);
      }
      aFile.close();
   }
}

缓冲区的Capacity,Position与Limit状态变量

正如前文所言,缓冲区实际上是一块能够写入数据,并且在稍后能够读取数据的区域.这块区域被NIO所实现的缓冲区对象包裹着,实现了封装,并且缓冲区对象还提供了一系列方法来更方便地操作内存块

为了更好的理解缓冲区是如何工作的,缓冲区有三个属性是需要熟悉的

  • capacity
  • position
  • limit

position,limit的意义跟缓冲区在读模式还是在写模式有关,而capacity则永远保持不变,与模式无关

以下这幅图是缓冲区分别在写模式和读模式下,这三个状态变量的位置

[外链图片转存失败(img-CHSMIpPa-1564142637104)(https://cdn.sinaimg.cn.52ecy.cn/large/005BYqpgly1g3owmmzefzj30ej09cwem.jpg)]

Capacity

作为一个内存块,缓冲区是有其确定且固定的大小的,这个大小就被称之为capacity(容量).当缓冲区满了以后,在你想要继续将数据写入缓冲区之前,你需要清空它(读取数据,或者直接清空).

Position

当你把数据写入缓冲区时,这个操作会在某个position(位置)进行.初始化时,position为0,当数据被写入到缓冲区后,position会增加,并指向下一个插入数据的位置.position的最大值为capacity-1(译者注:数组从零开始计数,所以一开始position是指向第一个数据被插入的位置,也就是0,当缓冲区满了之后,position就会指向数组的末端)

当你从缓冲区读取数据时,一样会从某个position开始.当你对缓冲区对象执行了flip()方法后,position会被重置为0.当你读取数据时,position也会随之增加.

Limit

在写模式中,缓冲区的limit属性是你能够向缓冲区写入多少数据,一般limit的值与capacity相同

当执行了flip()方法后,缓冲区进入读模式,此时limit意味着你能从缓冲区中读出多少数据.因此,当你切换到读模式时,limit会被设置成在写模式时position的位置.换句话说,你写入多少,就可以读多少.

例子

我们来看一个实际的例子,以便更好地理解缓冲区三个状态变量的概念

ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("程序被初始化,此时三个变量的值分别为:");
        System.out.println("position: " + buf.position());
        System.out.println("limit: " + buf.limit());
        System.out.println("capacity: " + buf.capacity());
        buf.put(new Byte("1"));
        buf.put(new Byte("2"));
        buf.put(new Byte("3"));
        System.out.println("读入了三个字节,此时三个变量的值分别为:");
        System.out.println("position: " + buf.position());
        System.out.println("limit: " + buf.limit());
        System.out.println("capacity: " + buf.capacity());
        buf.flip();
        System.out.println("执行了flip()方法,此时三个变量的值分别为:");
        System.out.println("position: " + buf.position());
        System.out.println("limit: " + buf.limit());
        System.out.println("capacity: " + buf.capacity());
        while (buf.hasRemaining()) {
            buf.get();
        }
        buf.clear();
        System.out.println("执行了clear()方法,此时三个变量的值分别为:");
        System.out.println("position: " + buf.position());
        System.out.println("limit: " + buf.limit());
        System.out.println("capacity: " + buf.capacity());

执行结果如下:

程序被初始化,此时三个变量的值分别为:
position: 0
limit: 1024
capacity: 1024
读入了三个字节,此时三个变量的值分别为:
position: 3
limit: 1024
capacity: 1024
执行了flip()方法,此时三个变量的值分别为:
position: 0
limit: 3
capacity: 1024
执行了clear()方法,此时三个变量的值分别为:
position: 0
limit: 1024
capacity: 1024

缓冲区类型

NIO定义了如下几个缓冲区类型

  • ByteBUffer
  • MapperByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

正如你所见,这些缓冲区类型对应不同的基本数据类型,换句话说,你可以将这些基本类型当做缓冲区中的字节进行使用,

不过MapperByteBuffer有一些特殊,会在本系列其他文章中介绍

创建一个缓冲区并为其分配内存

要想获得一个缓冲区对象,你首先需要动态分配它.每个缓冲区类都有allocate()方法来做这个工作.

下面这个例子动态分配了一个容量为48byte的ByteBuffer

ByteBuffer buf = ByteBuffer.allocate(48);

下面这个例子动态分配了一个容量为1024byte的CharBuffer

CharBuffer buf = CharBuffer.allocate(1024);

向缓冲区中写入数据

我们有两种方式向缓冲区中写入数据

  1. 从通道向缓冲区写入数据
  2. 执行缓冲区对象的put方法

以下是一个从通道向缓冲区写入数据的例子

int bytesRead = inChannel.read(buf); //read into buffer.

以下是一个执行缓冲区对象的put方法的例子

buf.put(127);

关于put()方法,还有许多重载版本,让我们能够以多种方式向缓冲区中写入数据.

具体的方法可以观看官方的文档(此处用的是JDK11)

flip()方法

flip()方法将一个缓冲区对象从写模式切换到读模式.调用flip()方法会使position设置为0(你没有忘记上面所说的三个变量对吧?),并将limit设置为position之前所在的位置.

换句话说,position现在标记读数据开始的位置,limit标记有多少数据写入了缓冲区中(有多少数据能够被读取)

从缓冲区中读出数据

我们有两种方式从缓冲区中读取数据

  1. 使通道从缓冲区读取数据
  2. 执行缓冲区对象的get()方法

以下是一个通道从缓冲区读取数据的例子

int bytesWritten = inChannel.write(buf);

以下是一个执行缓冲区对象的get()方法的例子

byte aByte = buf.get();

关于get()方法,还有许多重载版本,让我们能够以多种方式从缓冲区中读取数据.

具体的方法可以观看官方的文档(此处用的是JDK11)

rewind()

rewind方法将position设置为0,让我们可以从重新读取所有缓冲区中的数据.limit则保持不变,因此仍然标记着有多少数据能够从缓冲区中读取

clear()和compact()

当你完全地从缓冲区中读取数据后,你需要让缓冲区能够重新写入数据.clear()和compact()方法都可以完成这个操作

如果你调用了clear()方法,position会被设置成0,limit被设置为capacity.换句话说,缓冲区被重置了,不过缓冲区中的数据没有被清除,只是三个状态变量变为初始化的状态

如果你在完全读取数据之前调用了clear()方法,数据就会被"忘记",也就是说这些数据已经不可被读取(但仍然存在)

如果有上面所说到的情况,那么你需要先写入一些数据(当然不能太多以致未读取的数据被覆盖),然后调用compact()方法

compact()方法将所有未读取的数据复制到缓冲区的头部,然后将position设置为未读取数据的后面,limit仍然设置为capacity.现在缓冲区就可以写入了,并且不会覆盖没有读取的数据

mark()和reset()

你可以为缓冲区标记现在的位置,只需要调用mark()方法.当你读取了一些数据,或者写入了一些数据后,你可以调用reset()方法来把position设置为标记的地方

以下是一个例子

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.

equals()和compareTo()

可以通过这两个方法来比较两个缓冲区

equals()

当满足以下全部条件时,equals方法返回true

  • 类型相同(如:同为ByteBuffer)
  • 缓冲区中有相同数量的数据
  • 这些数据全部相等

如你所见,equals()方法只会比较buffer中的部分属性而非全部属性.事实上,该方法只会比较缓冲区中的数据

compareTo()

compareTo()方法与equals相同,比较的是缓冲区中的数据.当满足下面条件时,缓冲区A会认为比缓冲区B小

  • 缓冲区A中数据比缓冲区B中对应位置数据要小(根据字典顺序)