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 , 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()
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