Java NIO 系列文章

  1. 高并发IO的底层原理及4种主要IO模型
  2. Buffer的4个属性及重要方法

Buffer类及其属性

Buffer类

Buffer类是一个抽象类,对应于Java的主要数据类型,在NIO中有7种缓冲区类




JAVA 对象属性统一清空格 java清空对象属性的方法_JAVA 对象属性统一清空格


实际上,使用最多的还是ByteBuffer二进制字节缓冲区类型

Buffer类的重要属性

Buffer类在其内部,有一个相应类型的数组内存块,作为内存缓冲区。为了记录读写的状态和位置,Buffer类提供了一些重要的属性。其中,有三个重要的成员属性:capacity(容量)、position(读写位置)、limit(读写的限制)。

除此之外,还有一个标记属性:mark(标记),可以将当前的position临时存入mark中;需要的时候,可以再从mark标记恢复到position位置。

capacity属性

Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity容量,缓冲区就满了,不能再写入了。

Buffer类的capacity属性一旦初始化,就不能再改变。因为Buffer类的对象在初始化时,会按照capacity分配内部的内存,内存分配好之后,它的大小当然不能再改变了。

前面讲到,Buffer类是一个抽象类,Java不能直接用来新建对象。使用的时候,必须使用Buffer的某个子类,例如使用DoubleBuffer,则写入的数据是double类型,如果其capacity是100,那么我们最多可以写入100个double数据

limit属性

Buffer类的limit属性,表示读写的最大上限。limit属性与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的。 + 写模式: limit表示可以写入的最大上限。在刚进入写模式时,limit会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满 + 读模式: limit的含义为最多能从缓冲区中读取到多少数据。

position属性

Buffer类的position属性,表示当前的位置。position属性与缓冲区的读写模式有关。在不同的模式下,position属性的值时不同的,当缓冲区进行读写模式切换时,position会进行调整。 + 写模式:position变化规则如下 1. 刚进入写模式时,position值为0,表示当前的写入位置为从头开始 2. 每当一个数据写到缓冲区后,position会向后移动到下一个可写位置,position+1 3. 初始position为0,最大可写值position为limit - 1 4. 当position值达到limit时,缓冲区就无空间可写了 + 读模式: position变化规则如下 1. 刚进入读模式时,position被重置为0,表示从头开始读 2. 读取数据后,position移动到下一个可读位置,position+1 3. position最大的值为最大可读上限limit,当position到达limit时,表明缓冲区已无数据可读

4个属性小结

除了前面的3个属性,第4个属性mark(标记)比较简单。就是相当一个暂存属性,暂时保存position的值,方便后面的重复使用position值。


JAVA 对象属性统一清空格 java清空对象属性的方法_System_02


Buffer类的重要方法

主要介绍Buffer类的几个重要方法,包含Buffer实例的获取、对Buffer实例的写入、读取、重复读、标记和重置等。

首先定义一个公共的打印类


private static void logger(IntBuffer intBuffer) {
      System.out.println("capacity: " + intBuffer.capacity());
      System.out.println("limit: " + intBuffer.limit());
      System.out.println("position: " + intBuffer.position());
  }


allocate()创建缓冲区


public static void allocateTest() {
        IntBuffer intBuffer = IntBuffer.allocate(20);
        logger(intBuffer);
    }


例子中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(20),创建了一个Intbuffer实例对象,并且分配了20*4个字节的内存空间。


position:0
limit20
capacity20


从上面的运行结果,可以看出:

一个缓冲区在新建后,处于写入的模式,position写入位置为0,最大可写上限limit为的初始化值(这里是20),而缓冲区的容量capacity也是初始化值。

put()写入到缓冲区


public static void putTest() {
      IntBuffer intBuffer = IntBuffer.allocate(20);
      // 写4个
      IntStream.range(1, 5).forEach(intBuffer::put);
      logger(intBuffer);
  }


输出结果如下:


capacity: 20
limit: 20
position: 4


从结果可以看到,position变成了4,指向了第5个可以写入的元素位置。而limit最大写入元素的上限、capacity最大容量的值,并没有发生变化。

flip()翻转

向缓冲区写入数据之后,是否可以直接从缓冲区中读取数据呢?呵呵,不能。

这时缓冲区还处于写模式,如果需要读取数据,还需要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读取模式。


public static void flipTest() {
      IntBuffer intBuffer = IntBuffer.allocate(20);
      // 写4个
      IntStream.range(1, 5).forEach(intBuffer::put);
      logger(intBuffer);
      // 翻转缓冲区,从写模式翻转成读模式
      intBuffer.flip();
      logger(intBuffer);
  }


输出结果如下:


capacity: 20
limit: 20
position: 4

capacity: 20
limit: 4
position: 0


调用flip方法后,之前写入模式下的position值4,变成了可读上限limit值4;而新的读取模式下的position值,简单粗暴地变成了0,表示从头开始读取。

对flip()方法的从写入到读取转换的规则,详细的介绍如下: 1. 首先,设置可读的长度上限limit。将写模式下的缓冲区中内容的最后写入位置position值,作为读模式下的limit上限值。 2. 其次,把读的起始位置position的值设为0,表示从头开始读。 3. 最后,清除之前的mark标记,因为mark保存的是写模式下的临时位置。在读模式下,如果继续使用旧的mark标记,会造成位置混乱

有关上面的三步,其实可以查看flip方法的源代码,Buffer.flip()方法的源代码如下:


public final Buffer flip() {
      limit = position;
      position = 0;
      mark = -1;
      return this;
  }


新的问题来了,在读取完成后,如何再一次将缓冲区切换成写入模式呢?

可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为写模式。

Buffer的模式转换,大致如下图所示:


JAVA 对象属性统一清空格 java清空对象属性的方法_ci_03


get()从缓冲区读取

调用flip方法,将缓冲区切换成读取模式。这时,可以开始从缓冲区中进行数据读取了。读数据很简单,调用get方法,每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。


public static void getTest() {
      IntBuffer intBuffer = IntBuffer.allocate(20);
      System.out.println("---> getTest start");
      // 写 5 个
      IntStream.range(1, 6).forEach(intBuffer::put);
      logger(intBuffer);
      // 翻转 写 --> 读
      intBuffer.flip();
      // 读 2 个
      IntStream.range(1, 3).forEach(i -> System.out.println(intBuffer.get()));
      logger(intBuffer);
      // 读 3 个
      IntStream.range(1, 4).forEach(i -> System.out.println(intBuffer.get()));
      logger(intBuffer);

      // 此时已经读完所有5个数据,能否立即进入 写模式呢?
      // 答案是: 不能! 需要调用 Buffer.clear() 或 Buffer.compact() 即清空或压缩缓冲区 才能进入 写 模式
      intBuffer.compact();
      IntStream.range(1, 5).forEach(intBuffer::put);
      logger(intBuffer);
  }


输出结果如下:


// 写5个数据
capacity: 20
limit: 20
position: 5

// flip() 进入读模式

// 读 2个数据
1
2
capacity: 20
limit: 5
position: 2

// 读3个数据
3
4
5
capacity: 20
limit: 5
position: 5


从程序的输出结果,我们可以看到,读取操作会改变可读位置position的值,而limit值不会改变。如果position值和limit的值相等,表示所有数据读取完成,position指向了一个没有数据的元素位置,已经不能再读了。此时再读,会抛出BufferUnderflowException异常。

这里强调一下,在读完之后,是否可以立即进行写入模式呢?不能。现在还处于读取模式,我们必须调用Buffer.clear()Buffer.compact(),即清空或者压缩缓冲区,才能变成写入模式,让其重新可写。

rewind()倒带

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。


public static void rewindTest() {
      IntBuffer intBuffer = IntBuffer.allocate(20);
      // 写5个数据
      IntStream.range(1, 6).forEach(intBuffer::put);
      // 切换到读模式
      intBuffer.flip();
      // 第一次读
      IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));
      logger(intBuffer);

      // IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));

      // 倒带 position -> 0
      intBuffer.rewind();
      logger(intBuffer);

      // 第二次读
      IntStream.range(1, 6).forEach(i -> System.out.println(intBuffer.get()));
      logger(intBuffer);
  }


输出结果如下:


// 第一次读
1
2
3
4
5
capacity: 20
limit: 5
position: 5

// rewind() 倒带
capacity: 20
limit: 5
position: 0

// 第二次读
1
2
3
4
5
capacity: 20
limit: 5
position: 5


rewind()方法,主要是调整了缓冲区的position属性,具体的调整规则如下: 1. position重置为0,所以可以重读缓冲区中的所有数据。 2. limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取多少个元素。 3. mark标记被清理,表示之前的临时位置不能再用了。 Buffer.rewind()方法的源代码如下:


public final Buffer rewind() {
      position = 0;
      mark = -1;
      return this;
  }


mark()和reset()

Buffer.mark()方法的作用是将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将mark的值恢复到position中


public static void markAndResetTest() {
      IntBuffer intBuffer = IntBuffer.allocate(20);
      // 写5个
      IntStream.range(1, 6).forEach(intBuffer::put);
      // 切换到读
      intBuffer.flip();

      // 读3个,并在position为2时,做标记
      IntStream.range(1, 4).forEach(i -> {
          // 标记position
          if (i == 2) {
              intBuffer.mark();
          }
          System.out.println(intBuffer.get());
      });
      logger(intBuffer);

      // 将position 重新设置成 mark
      intBuffer.reset();
      logger(intBuffer);

      //重置后读
      IntStream.range(1, 3).forEach(i -> {
          System.out.println(intBuffer.get());
      });
  }


输出结果如下:


// 读3个
1
// mark做标记
2 
3
capacity: 20
limit: 5
position: 3

// reset 重置
capacity: 20
limit: 5
position: 1

// 重置后读
2
3


clear( )清空缓冲区

在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法会将position清零,limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。


public final Buffer clear() {
      position = 0;
      limit = capacity;
      mark = -1;
      return this;
  }


使用Buffer类的基本步骤

总体来说,使用Java NIO Buffer类的基本步骤如下: 1. 使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。 2. 调用put方法,将数据写入到缓冲区中。 3. 写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。 4. 调用get方法,从缓冲区中读取数据。 5. 读取完成后,调用Buffer.clear()或Buffer.compact()方法,将缓冲区转换为写入模式。