java.nio.Buffer类图文解析

Posted wkw1125

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java.nio.Buffer类图文解析相关的知识,希望对你有一定的参考价值。

首先导读:ByteBuffer常用方法详解,我是从该文章开始了解,作图加深理解。

Buffer

ByteBuffer(以及其他类型缓冲区,如CharBuffer、IntBuffer等)继承自抽象类Buffer,用于在堆内存中预留一部分空间对IO数据做临时存储,以提高读写效率。各子类不同之处在于get/put数据的不同,而关于缓冲区读写区域的管理全部由父类Buffer负责。继承关系盗图如下:

4个属性

Buffer的3个关键成员变量:

final int capacity;
int limit;
int position = 0;

以上3个成员变量都有同名方法获取,以及1个关键的衍生值:

public final int remaining()  return limit - position; 

Buffer本质是一个数组,通过以上4个值管理这个工作数组的可读/写区域(我将其称之为“活动区域”),正确理解这些值是用好所有Buffer子类的关键。

结合示意图来理解这4个值:

【图1 Buffer示意图】

(假设,该Buffer处于读模式,即用户正在从该buffer读取数据)

  • capacity:该buffer能容纳的数据总长度,即工作数组的长度,这里是16,数组元素下标为0~15。
  • position:读取位置起点3,get()操作返回该位置的值”h”,并将该值+1。
  • limit:可读取有效数据的界限,类似于数组的length。当前值为13,即get()操作最多读取到position=13-1=12的位置。
  • remaining=limit - position:剩余可读取数据的数量,这里还有10个数据可以读取。

数值关系:position <= limit <= capacity
注意,当position == limit 时表示无数据可读取(或无位置可写入)。

为了理解这些属性,需要先理解一下Buffer的工作方式:

Buffer工作方式

对用户来说,对同一个buffer的使用分为两种操作:往buffer输入数据,之后从buffer读取数据。缓冲区的意义在于,读和写的节奏可能不一样,有可能写入10个数据却只读取了3个,这时新来数据需要写入,也应该可以接着之前的数据末尾继续写入,而不能把未读取的7个数据丢了。

Buffer的工作数组长度capacity在初始化时就确定并且不可改变,position与limit用于划分出数组的“活动区域”,remaining()计算得到的则是该区域的长度。对Buffer的读写操作实际上都是针对这块区域的操作:该“活动区域”在写模式下表示Buffer可以写入数据的位置范围;在读模式下表示Buffer中可以读取的有效数据的范围。
这里之所以强调有效,是由于Buffer始终使用同一个数组处理读和写,一个位置上的数据一旦被读取了,它就失效了,下次写入时这个位置就允许被写入新数据。Buffer处理失效数据,不是将数据移除,而是将position标志位进行变化。对于已读取的数据,只需要将其排除在“活动区域”之外,就不会被读取。如Buffer.clear()仅仅是将标志位position=0,limit=capacity,表示整个数组都可以被写入(覆盖)而已。

若对【图1】中的Buffer调用get方法,即buffer处于读模式,那么可读取下标为3~12之间的数据“helloworld”,其他区间的数据可能存在,但一定是无效的(如这里”hi!”意味着已被读取)。若对其调用put方法,即buffer处于写模式,那么将从位置3开始写入新数据,(这往往意味着”hi!”是已写入的数据)。

因此,要正确处理Buffer的读写操作,就要处理好各个标志位在读写模式切换时的变化。这个变化由两个方法负责:flip()、compact().

2个方法

flip()翻转

flip():(向前)翻转“活动区域”,准备开始读取

//Flips this buffer
//该方法由抽象类Buffer定义
public final Buffer flip() 
    limit = position;
    position = 0;
    return this;

为何叫“翻转”?看图就理解了:

【图2:flip()示意图】

上图中绿色的“活动区域”经过flip()后,像翻书一样地向前(向左)翻转到了下图中绿色部分。将活动区域翻到开头后,就可以使用get()从头开始读取数据。因此,flip()一般用于准备读取buffer。

compact()压实

compact():(向前)压实“活动区域”,准备开始写入。

/**
 * Compacts this byte buffer.
 * The remaining bytes will be moved to the head of the
 * buffer, starting from position zero. Then the position is set to remaining(); the limit is set to capacity; ...
 */
//该方法由Buffer的子类定义和实现
public abstract ByteBuffer compact();

该方法由Buffer的子类定义和实现,从注释可知:将Buffer中剩余的数据区域移动到头部,再将“活动区域”设置为除数据区域外的末尾部分。
看图:

【图3:compact()示意图】

上图中绿色的“活动区域”经过compact()后,往前(往左)压实,挤掉了无效的0~2位置的三个数据;并重新将“活动区域”指向了尾部。压实后,就可以使用put()从绿色部分末尾,开始写入新数据。因此,compact()一般用于准备写入buffer。

ByteBuffer API总结

掌握4个值、2个方法后,Buffer类的使用参照api文档就可以了。
下面以ByteBuffer为例,总结下常用的几个方法,详细见开篇导读文章。

方法解释
wrap(byte[])静态方法,以初始数据创建Buffer(处于读模式
capacity()获取容量
position()获取当前位置
limit()获取当前读写界限
remaining()获取当前可读写数量,值为limit()-position()
flip()翻转活动区域,准备开始从头部读取
compact()压实活动区域,准备开始从尾部写入
mark()记录当前位置position,以备恢复
reset()将当前位置position恢复到mark位置,其他不变
rewind()将当前位置position恢复到0位置,mark设为-1,其他不变
get()读取当前位置数据,并且移动position
get(int)读取指定位置数据,但移动position
put(byte)末尾位置写入,并且移动position
put(int,byte)指定位置写入,但移动position
clear()将整个buffer置为写模式的活动区域,可以从头写到尾
order(ByteOrder)字节顺序,对同一ByteBuffer的读写应保持一致,详见ByteOrder

注意:各种get/put方法是否影响当前位置,需根据api说明使用。

注意点

  1. ByteBuffer.warp(byte[])以一个数组创建buffer后,该buffer默认处于读模式,即position=0,limit=capacity。此时若使用put进行写入,将从0位置开始覆盖掉初始化数组的数据。因此,若初始化的数据是有用数据,在写入开始前应使用compact()压实后,从尾部开始写入。
  2. 一定要注意get/put重载的方法是否影响当前位置,在读取、写入前都要用remaining()进行可读/写判断
  3. remaining()是动态计算得到,谨记在get/put后,该值可能发生改变。
  4. position==limit时,get/put越界。即position=limit-1是最后一个数据

测试结果

测试了几个api证实本文理解正确

get
ByteBuffer after wrap:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12,remaining=12
ByteBuffer after get2:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=2,limit=12;remaining=10
ByteBuffer after flip:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=2;remaining=2
ByteBuffer after comp:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=2,limit=12;remaining=10

flip
ByteBuffer after wrap:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12,remaining=12
ByteBuffer after flip:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=0;remaining=0
ByteBuffer after comp:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12;remaining=12

put
ByteBuffer after wrap:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12,remaining=12
ByteBuffer after put1:[ 72,  73, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=2,limit=12;remaining=10  
ByteBuffer after flip:[ 72,  73, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=2;remaining=2
ByteBuffer after comp:[ 72,  73, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=2,limit=12;remaining=10

compact after wrap
ByteBuffer after wrap:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12,remaining=12
ByteBuffer after comp:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=12,limit=12;remaining=0
ByteBuffer after flip:[104, 101, 108, 108, 111, 44, 119, 111, 114, 108, 100, 33], position=0,limit=12;remaining=12

以上是关于java.nio.Buffer类图文解析的主要内容,如果未能解决你的问题,请参考以下文章

解读Java NIO Buffer

java nio buffer

JAVA NIO buffer (知识三)

Java NIO Buffer详解

java.nio.Buffer 未在运行时加载 clear() 方法

Java NIO Buffer