目录 [−]
Java 1.4中在java.nio包中增加了Buffer类以及一些处理基本数据类型的子类(除了boolean型) ,用来提供为基本数据类型(primitive) 的数据提供一个容器。
何谓Buffer? Buffer
是一个线性的有限长度的特定基本数据的序列。 除了基础数据外,它还包括一些基础操作和属性, 比如capacity
, limit
和 position
。
实际使用中使用特定的子类来处理数据。每个子类都定义了两套get/put的操作。
- 相对位置操作 (Relative )。 从当前位置
position
读写一个或者多个元素, 并position
增加相应的数值。 如果一个get请求的数据超过了limit
的位置,会抛出BufferUnderflowException
异常。 如果一个put操作超过了limit
的限制, 会抛出BufferOverflowException
异常。不管上面哪种情况,没有数据被传输。 - 绝对位置操作 (Absolute )。 显式地提供index, 不会影响
position
的值。 如果索引超过limit
会抛出IndexOutOfBoundsException
异常。
数据也可以通过Channel
的I/O操作如write
,read
写入或者读出。
显然, Buffer只有写入了数据才可能有意义的数据读出。
Buffer类并不是线程安全的, 使用时要特别小心, 避免多线程同时读写同一个Buffer。 万不得已, 需要为读写操作加锁。
cache和buffer的区别
从应用场景上看:Buffer
更多的(场景)是减小写操作的冲击,而Cache
主要用于减小读 I/O 的重复开销。
Buffer抽象类的成员
Buffer提供了一系列的操作缓冲区的方法以及属性。 但是属性(property)不是以字段field
的方式提供,而是以方法method
的方式提供。
属性properties
capacity()
: 上面提到, Buffer里的元素是有限的。 这个值代表Buffer的元素的最大数量。 这个值不会为负数, 也不会被改变。limit()
: 很多情况下, 缓冲区不是填满的。limit
是第一不应该被读/写的数据的索引位置。 显然这个值不能为负数,也不会超过capacity
的值。position()
: 下一个要被读/写的数据的索引。 不能为负值也不会超过limit
的值。mark
: 被标记的索引。 调用reset
方法会将position
的值设为mark
的值。 这样可以重新读/写Buffer的数据。 当position
或者limit
的值小于它的值时,它的值会被丢弃。 它的值不能为负数,也不会超过position
的值, 也可能没有设置, 如果没有设置的话调用reset
方法会抛出InvalidMarkException
异常。 没有直接读取的方法。remaining()
: 返回position
和limit
之差, 也就是未读/写的数据的数量.
操作方法
reset
重置。 将position
的值重置为mark
的值。 这个方法不会更改mark
的值,也不会将mark
的值丢掉。clear
清空。 清空缓冲区。position
的值设为0
,limit
的值设为capacity
,mark
的值被丢弃。 在填充Buffer之前一般会调用此方法:buf.clear(); in.read(buf);
。 这个方法不会擦除以前填充的数据,但是在实际使用中的情况下功能一样。flip
反转。 反转缓冲区会将limit
的值设为position
的值, 然后position
的值设为0
。 如果设置了mark
, 则会被丢弃。 一般在填充完缓冲区后读写数据时调用此方法:buf.put(magic);in.read(buf);buf.flip();out.write(buf);
rewind
回退。position
设为0
,mark
值被丢弃,limit
的值不变。 和上面的flip
类似,但是flip
会改变limit
的值,但是remind
不会。 应用场景:out.write(buf);buf.rewind(); buf.get(array);
mark()
标记当前位置。 用position
的值设置mark
。
clear
,flip
,limit(newLimit)
,mark
,position(newPosition)
, reset()
和rewind
返回本身的Buffer, 这意味着你可以使用流式风格, 如buffer.flip().position(23).limit(42);
请记住以下公式, 下面的不等式在任何时候都成立:
0 <= mark <= position <= limit <= capacity
一个新创建的Buffer的position
总是0
, mark未定义。 初始的limit
可能为0
,或者其它正值, 这依赖于buffer的类型以及它是如何创建的。 初始化的Buffer包含零个元素。
另外, Buffer还提供其它的一些成员:
array
: 返回底层的数组实现。 如果底层不是使用数组实现,或者是只读的, 可能会抛出异常ReadOnlyBufferException
,UnsupportedOperationException
。 一般使用前会调用hasArray
判断是否支持数组。 Buffer内容的修改会影响数组的值,反之亦然。arrayOffset
: Buffer的position
的值p
对应 数组的p + arrayOffset()
, 也就是Buffer的第一个元素在数组中的偏移值。hasArray
: 判断Buffer是否有底层的数组实现。hasRemaining
:position
和limit
之前是否还有元素。isReadOnly
: 是否只读。isDirect
: buffer是否是直接缓冲区。
equals()
当满足下列条件时,表示两个Buffer相等:
- 有相同的类型(byte、char、int等)。
- Buffer中剩余的byte、char等的个数相等。
- Buffer中所有剩余的byte、char等都相同。
注意它只比较剩余的部分。
compareTo()
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
- 第一个不相等的元素小于另一个Buffer中对应的元素 。
- 前面的元素都相等,但第一个Buffer剩余的元素比另一个少。
Buffer的子类
Buffer
是一个抽象类。 实际我们使用的是它的子类, 主要是针对基本数据类型做的优化。
ByteBuffer
: 最通用的子类, 处理字节数据类型。
提供了get/put 单个字节或者字节数组的方法, 还是分相对和绝对操作。
字节是其它基本数据类型的基础。 比如int类型是32位也就是4个字节。
Data Type | size (byte) |
---|---|
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
double | 8 |
char | 2 |
所以可以将这些基本数据类型的数据写入到字节缓冲区中或者从中读出。
ByteBuffer针对基本数据类型定义了便利的方法, 如getChar()
,getInt()
,putFloat
,putShort
...等方法。
注意putXXX
可能会抛出BufferOverflowException
和ReadOnlyBufferException
异常, getXXX
可能会抛出BufferUnderflowException
异常。
同时ByteBuffer还提供了创建视图view
的方法。 可以基于ByteBuffer创建其它基本类型的buffer,它们的底层数据指向都一个对象,但是相应的position
, limit
, mark
都是独立的。例如asIntBuffer()
返回一个IntBuffer
对象, 返回的IntBuffer
对象的第一个元素对应于此ByteBuffer
的position
的位置的元素。 IntBuffer
的position
的值为0
,capacity
和limit
是此ByteBuffer
的剩余的字节的数量/4 (int是四个字节), mark未定义。 当且仅当ByteBuffer
是直接缓冲区时此IntBuffer
才是直接缓冲区, 当且仅当ByteBuffer
是只读的 IntBuffer
才是只读的。
注意ByteBuffer
依然是抽象类, allocate
方法和allocateDirect
方法创建缓冲区时实际是创建HeapByteBuffer
或者DirectByteBuffer
类。
仍然支持流式风格。 bb.putInt(0xCAFEBABE).putShort(3).putShort(45);
asReadOnlyBuffer()
转换成只读缓冲区。新缓冲区的position, limit, 和 mark是独立的。 初始值和原缓冲区相同。duplicate
复制当前的ByteBuffer
,底层的数据是公用的,但是position
,capacity
,limit
,mark
是独立的, 方法返回的ByteBuffer
初始拥有和原ByteBuffer
相同的position
,capacity
,limit
,mark
。slice()
也是一个新的byte buffer,和原bye buffer的数据共享。 但是新的byte buffer将自原byte buffer的position
的位置开始。 这也就是slice的含义。
提供了allocate(int capacity)
和allocateDirect(int capacity)
两种方法。wrap(byte[] array)
, wrap(byte[] array, int offset, int length)
将字节数组包装成ByteBuffer
。
order()
和order(ByteOrder bo)
用来返回和设置字节序: 大端模式(BIG_ENDIAN)和小端模式(LITTLE_ENDIAN)。 默认总是大端模式(BIG_ENDIAN)。
compact
: 压缩ByteBuffer。 将position
和limit
的之间的数据复制到缓冲区的开始部分。 比如另p = position
,则将 p + 1
处的数据复制到index 1
, ...... limit -1
处的数据复制到n = limit -1 -p
。 缓冲区的position
设置为n+1
, limit设置为capacity
, mark丢弃.
以下的子类类似ByteBuffer,但是没有转换成其它Buffer的方法和视图。这是容易理解的,因为Byte才是其它基本数据类型的基础单位。wrap
包装相应基本数据类型的数组。依然有compact
, duplicate
, slice
方法. 流式风格, get/put 单数据操作和批操作, 相对位置操作和绝对位置操作。
CharBuffer
提供了append
方法,等同于put方法。charAt(int index)
返回指定位置的字符。subSequence(int start, int end)
返回指定位置的缓冲区。 与原缓冲区共享数据。 capacity
相同。 新缓冲区的position
为原缓冲区position
+ start, limit
为原缓冲区的position
+ end。 direct
, readonly
和原缓冲区相同。
DoubleBuffer
处理double类型数据。
FloatBuffer
处理float类型数据。
IntBuffer
处理int
类型数据。
LongBuffer
处理long
类型数据。
ShortBuffer
处理short
类型数据。
MappedByteBuffer
继承于ByteBuffer
。 它是以内存镜像文件为基础的直接字节缓冲区。 可以通过FileChannel.map
创建。force()
强制对数据的改变写入到存储设备。isLoaded()
: 缓冲区的数据是否都全部加载到物理内存中。load()
: 加载缓冲区的数据到物理内存中。
内存映射文件是一种允许Java程序直接从内存访问的特殊文件。通过将整个文件或者文件的一部分映射到内存中、操作系统负责获取页面请求和写入文件,应用程序就只需要处理内存数据,这样可以实现非常快速的IO操作。用于内存映射文件的内存在Java的堆空间以外。
Buffer的创建
- allocate()或者allocateDirect()
|
|
- 包装一个数组
|
|
- 内存映射,即调用FileChannel的map()方法
|
|
直接缓冲区和间接缓冲区
byte buffer既可以是直接缓冲区可以是非直接缓冲区。 对于直接缓冲区, Java虚拟机极可能的直接执行native I/O操作,避免在操作系统的native I/O操作时还要复制内容到一个中间缓冲区。
它使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作。
可以通过allocateDirect
工厂方法直接创建直接缓冲区, 内部会创建DirectByteBuffer
对象, 通过unsafe.allocateMemory
分配内存。 相对而言, 这个方法返回的缓冲区要比非直接缓冲区多少有点更高的分配/销毁的花费 (时间和空间)。 直接缓冲区在垃圾回收堆的外部, 所以建议主要用于大的长时间活动的缓冲区,确实能提高性能的环境中。
也可以通过内存镜像文件的方式使用直接缓冲区 FileChannel.map
。 Java平台可选择使用JNI来创建直接缓冲区。 如果Buffer指向一个不能访问的内存区域时, 缓冲区的内容不会被更改, 访问操作可能会导致一个不确定的异常。
可以通过isDirect
方法判断一个缓冲区是否是直接缓冲区。
虽然直接缓冲区是堆外内存,但是由于DirectByteBuffer
引用了它,当DirectByteBuffer
被垃圾回收时,此堆外内存会被释放掉,不会出现内存泄漏的问题。