前言
上一篇文章中Java NIO概括性的介绍了Java Nio以及各个核心组件。这篇继续Java Nio的话题,着重了解下Nio中Buffer的原理、Buffer的行为、Buffer种类。
- Buffer原理
- Buffer行为
- Buffer分类
Buffer原理
上篇中简书了Buffer是数据容器,可以重复读写。那么Buffer是如何存储数据的呢?Buffer的读写操作是如何标记读写的位置呢?带着这些问题,我们一起先来看下Buffer的存储,以及属性标记。
线性数组是一门最基础的数据结构,对很多数据结构提供的基本的存储支持。Buffer中使用了线性数组作为其存储实现,即一块连续的存储空间。
Buffer中有四个非常重要的属性位:
- capacity: 表示Buffer的容量;
- limit: Buffer的读写限制,表示最大能读写到limit的位置;
- position: 记录下一个读写的位置;
- mark::记录打标记的位置;
还是一如既往,先直观地图解Buffer
如上图,是一个容量为n的Buffer的初始化状态。创建Buffer时:
- 首先分配容量为n的数组,capacity为n,此时limit设置为n,表示最大的可写限制为n;
- position初始为0,表示即将写入的位置是槽位0
- mark初始化为-1,初始化的Buffer无标记位
Buffer中有几个非常重要的操作会导致Buffer的属性位发生变化
- flip: 翻转Buffer
- clear:清理Buffer
- mark:标记Buffer
属性位的变化表示Buffer的状态切换,每种状态的Buffer都对应Buffer可操作的相应行为
上图展示了Buffer随着flip、clear、mark操作的属性变更和状态转移:初始化状态 -> 一次put后 -> flip后 -> 一个get后。
两次put后,position移动两位,即position=2,表示下一个写入的位置,limit等于capacity表示Buffer最大支持的写入限制。
flip翻转主要是在Buffer的读写之间进行切换,flip后,将limit设置为position的大小,position归零,从0位开始读,读到limit为止。
一次get后,则将position移动一位,表示下一个即将读的位置。
从上可以看出,相应的操作导致属性位变化,Buffer的可读可写状态之间切换。Buffer基于此实现读写操作。
Buffer的mark用来给Buffer当前的postion位置打标记,即mark记录当前postion位置,当Buffer发生reset重置时,将postion设置为之前的mark位置,主要用来实现重复读写,持续读写。(复位代表着可持续操作!!)
Buffer行为
上一节中详细说明了Buffer的存储,读写原理。这一节中继续了解Buffer的常用行为。这里以字节缓冲区ByteBuffer(存储字节数据)的作为例子进行说明。
ByteBuffer的常用行为无非创建、读写,但是ByteBuffer对于读写需要遵循上述的flip,clear等操作。
1)ByteBuffer创建主要通过静态allocate方法实现(工厂模式)
ByteBuffer buffer = ByteBffer.allocate(16);
这里创建大小为16个字节容量的Buffer。
2)读操作主要通过get和get的变体方法实现,字节get方法主要有四种形式:
- get():获取当前position槽位的值
- get(byte[] b):从当前position获取内容至数组b中,直到填满数组。如果buffer数据不足,会抛出BufferUnderflowException
- get(byte b[], int offset, int length) 从当前position获取内容至数组b中,从数组的偏移offset位开始填充,共获取length个
- get(int index) 获取指定槽位的byte,position不发生偏移
3)写操作:主要通过put和put的变体方法实现,字节put方法主要有五种形式:
- put(byte b):向当前position槽位写入一个字节
- put(byte[] bs):从当前position槽位开始写入字节数组,如果buffer容量不足,则抛出:BufferUnderflowException
- put(byte[] bs, int offset, int length) 从当前position槽位写入字节数组,从数组的offset偏移位置取,共写入length个
- put(int index, byte b) 向指定index槽位写入数据
- put(ByteBuffer buffer) 转换参数ByteBuffer
ByteBuffer除了基本的字节读写操作,还提供了更便利的get变体操作,完成以不同数据类型读写ByteBuffer,如:
- getChar 以字符形式读取ByteBuffer
- getInt 以int形式读取ByteBuffer
- putChar 以字符形式写入Buffer
- putInt 以int形式写入Buffer
除了以上的变体操作,还有很多其他的数据类型的get/put变体操作,可以查看ByteBuffer详细的api。
ByteBuffer除了基本的读写操作,还有几个继承自Buffer父类的行为,Buffer的行为在其派生出的子类中都具有:
- isDirect:是否为直接Buffer(后续文章中会说明直接的含义)
- isReadOnly:是否为只读
- hasRemaining:是否存在剩余容量
- remaining:剩余容量大小
Buffer分类
在以ByteBuffer为例介绍完,Buffer的常用行为后,下面在简单的了解下Nio中提供的Buffer类型,以及与ByteBuffer的区别。
Nio中的Buffer可以从多维角度进行划分:1.数据类型;2.堆Buffer和直接内存Buffer;3.抽想Buffer和其对应的具体实现
1) 数据类型
按照存储的数据类型,可以将Buffer划分为:
- ByteBuffer: 存储字节
- ShortBuffer:存储短整型
- IntBuffer:存储整形
- LongBuffer:存储长整型
- CharBuffer:存储字符类型
- FloatBuffer:存储浮点类型
- DoubleBuffer:存储双精度
ByteBuffer提供get/put的变体,以方便的api实现其他数据类型的读写,其他类型的Buffer均只有基本数据类型的get/put读写。
2) 堆/直接内存
按照数据的存储位置,可以将Buffer分为堆Buffer和直接内存Buffer。
-
堆Buffer:Buffer内存分配在Java内存的堆区域,Heap Buffer实现:
HeapByteBuffer/HeapCharBuffer等 -
直接内存Buffer:Buffer使用堆外的直接内存,如:Direct Buffer实现:
DirectByteBuffer/DirectCharBuffer
3) 抽象Buffer和其对应实现
Nio中Buffer的设计模式非常强,工厂模式、。。。我回头再温故下设计模式,嘿嘿。
Nio中利用面向对象的多态性,使用多重继承将Buffer模块设计的鬼斧神工。
对每种数据类型的Buffer都定义其抽象的数据类型Buffer,如:
ByteBuffer/ShortBuffer/IntBuffer等抽象类,然后针对堆Buffer和直接内存Buffer两种形态又设计各自的实现:HeapByteBuffer/DirectByteBuffer、HeapShortBuffer/DirectShortBuffer、HeapIntBuffer/DirectIntBuffer。
总结
至此,Buffer的类容介绍完毕。这里只是浅谈了Buffer的原理、行为和分类,并没有对源码进行解读,感兴趣的朋友可以自行阅读。
这里再做下简单的总结,各种类型的Buffer主要是利用线性数组存储各种类型的数据,维护多个属性位,伴随将属性位变化达到读写状态的变化。读写操作的多态形式使得Buffer的api机器丰富,可用性极强。Buffer的种类可谓纷繁,每种Buffer都有其各自的应用场景,解决不同的需求。