Java Buffer编程基础

目录 [−]

  1. Buffer抽象类的成员
    1. 属性properties
    2. 操作方法
  2. Buffer的子类
    1. ByteBuffer: 最通用的子类, 处理字节数据类型。
    2. CharBuffer
    3. DoubleBuffer
    4. FloatBuffer
    5. IntBuffer
    6. LongBuffer
    7. ShortBuffer
    8. MappedByteBuffer
  3. Buffer的创建
    1. 直接缓冲区和间接缓冲区
  4. 参考

Java 1.4中在java.nio包中增加了Buffer类以及一些处理基本数据类型的子类(除了boolean型) ,用来提供为基本数据类型(primitive) 的数据提供一个容器。
何谓Buffer? Buffer 是一个线性的有限长度的特定基本数据的序列。 除了基础数据外,它还包括一些基础操作和属性, 比如capacity, limitposition

实际使用中使用特定的子类来处理数据。每个子类都定义了两套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() : 返回positionlimit之差, 也就是未读/写的数据的数量.

操作方法

  • reset 重置。 将position的值重置为mark的值。 这个方法不会更改mark的值,也不会将mark的值丢掉。
  • clear 清空。 清空缓冲区。 position的值设为0limit的值设为capacitymark的值被丢弃。 在填充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是否有底层的数组实现。
  • hasRemainingpositionlimit之前是否还有元素。
  • 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 Typesize (byte)
byte1
short2
int4
long8
float4
double8
char2

所以可以将这些基本数据类型的数据写入到字节缓冲区中或者从中读出。
ByteBuffer针对基本数据类型定义了便利的方法, 如getChar(),getInt(),putFloat,putShort ...等方法。
注意putXXX可能会抛出BufferOverflowExceptionReadOnlyBufferException异常, getXXX可能会抛出BufferUnderflowException异常。

同时ByteBuffer还提供了创建视图view的方法。 可以基于ByteBuffer创建其它基本类型的buffer,它们的底层数据指向都一个对象,但是相应的position, limit, mark都是独立的。例如asIntBuffer()返回一个IntBuffer对象, 返回的IntBuffer对象的第一个元素对应于此ByteBufferposition的位置的元素。 IntBufferposition的值为0,capacitylimit是此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。 将positionlimit的之间的数据复制到缓冲区的开始部分。 比如另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()
1
2
CharBuffer cb = CharBuffer.allocate(1024);
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024);
  • 包装一个数组
1
2
int[] bytes = new int[1024];
IntBuffer ib = IntBuffer.wrap(bytes);
  • 内存映射,即调用FileChannel的map()方法
1
2
FileChannel fc = new RandomAccessFile("test.data", "rw").getChannel();
MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, length);

直接缓冲区和间接缓冲区

byte buffer既可以是直接缓冲区可以是非直接缓冲区。 对于直接缓冲区, Java虚拟机极可能的直接执行native I/O操作,避免在操作系统的native I/O操作时还要复制内容到一个中间缓冲区。
它使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。

可以通过allocateDirect工厂方法直接创建直接缓冲区, 内部会创建DirectByteBuffer对象, 通过unsafe.allocateMemory分配内存。 相对而言, 这个方法返回的缓冲区要比非直接缓冲区多少有点更高的分配/销毁的花费 (时间和空间)。 直接缓冲区在垃圾回收堆的外部, 所以建议主要用于大的长时间活动的缓冲区,确实能提高性能的环境中。

也可以通过内存镜像文件的方式使用直接缓冲区 FileChannel.map。 Java平台可选择使用JNI来创建直接缓冲区。 如果Buffer指向一个不能访问的内存区域时, 缓冲区的内容不会被更改, 访问操作可能会导致一个不确定的异常。

可以通过isDirect方法判断一个缓冲区是否是直接缓冲区。

虽然直接缓冲区是堆外内存,但是由于DirectByteBuffer引用了它,当DirectByteBuffer被垃圾回收时,此堆外内存会被释放掉,不会出现内存泄漏的问题。

参考

  1. JavaDOC
  2. Java NIO Buffer
  3. How would you code an efficient Circular Buffer in Java or C#
  4. Ostermiller Java Utilities