ByteBuffer和CharBuffer等其它Buffer的直接子类一样,顾名思义,就是存取字节的Buffer。很多数据最终在和底层交互上都是使用了字节,而更大的数据是由字节组合而成。谈到字节的组合,就不得不谈到字节大小的定义和字节的顺序。关于字节是8位构成的这个结论,似乎现在的计算机教材都理所当然地描述出来,我们也默认接受了这样的一个事实。但实际上字节由8个二进制位构成也是有渊源和优点的,这与IBM的360主机有关,详细的可以参考这个。下面说说组成数据的字节顺序。
对于多字节的数据在系统中的存储,通常按数据的高位和低位在系统内存中的高地址和低地址存放分为大端(big endian)和小端(little endian)两种方式。
在Java API中,有ByteOrder这样一个public类,在其中定义了大端和小端两个常量。通过这个java.nio.ByteOrder类的nativeOrder()方法,也可以确定当前系统平台的字节顺序。
在不同的平台上可能有不同的字节顺序标准。但在ByteBuffer类中,默认是使用了ByteOrder.BIG_ENDIAN字节序。但可以通过ByteBuffer的重载方法获取和设置字节序:
1. ByteBuffer的实现
提到ByteBuffer的实现,我们先来看下Win下JDK实现的类层次结构图。
而在Oracle的Java SE API中,实际上只提到了MappedByteBuffer。所以堆实现和具体的直接实现(DirectByteBuffer)我们只简单了解就行了,因为这个不在API中,和平台实现相关。
HeapByteBuffer 是虚拟机的堆中实现,DirectByteBuffer是系统级别实现(使用unsafe的 unsafe.allocateMemory(size)),使用时后者比前者节省了拷贝过程,但后者的构建和析构成本更高,总体性能需要具体问题综合分析。而且DirectByteBuffer会受到平台方面的约束,使用时需要小心注意。
而API中出现了的java.nio.MappedByteBuffer则是针对文件映射工作的,也是一种Direct的ByteBuffer。除了继承ByteBuffer类的方法外,API还提供了下面3个方法:
关于文件映射相关的具体内容,下面会详细说。
2. ByteBuffer和Buffer的其它直接子类之间的关系
前面的一篇Buffer的文章提到了它的几个直接子类,分别是ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer。除了ByteBuffer,还有另外6种,而这些也都和Java的基本数据类型有一定的对应关系,下面我们对使用上的情况梳理下。
ByteBuffer继承于Buffer。和CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer一样,都是抽象类。
在Java NIO中,除了Buffer还有Channel等类。Buffer也只有和Channel配合才能充分地把Java NIO使用起来。而Channel中的很多方法都是使用ByteBuffer作为参数和返回结果进行传递的,好处就是Byte是字节、是基础,而方法也简单了很多。
而这样的设计有一个要求,就是使用Byte以外的类型也能很好的利用到ByteBuffer,这其中有两种方式:
下面是两段代码例子。
1
2
3
4
|
ByteBuffer byteBuffer = ByteBuffer.allocate (
7
).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );
int
value = buffer.getInt( );
|
当然,在实际使用过程中也会有需要注意的问题,比如字符数据,就需要考虑字符集编码的问题。下面是《Thinking in Java》中的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public
class
BufferToText {
private
static
final
int
BSIZE =
1024
;
public
static
void
main(String[] args)
throws
Exception {
FileChannel fc =
new
FileOutputStream(
"data2.txt"
).getChannel();
fc.write(ByteBuffer.wrap(
"Some text"
.getBytes()));
fc.close();
fc =
new
FileInputStream(
"data2.txt"
).getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
// Doesn’t work:
System.out.println(buff.asCharBuffer());
// Decode using this system’s default Charset:
buff.rewind();
String encoding = System.getProperty(
"file.encoding"
);
System.out.println(
"Decoded using "
+ encoding +
": "
+ Charset.forName(encoding)
.decode(buff));
// Or, we could encode with something that will print:
fc =
new
FileOutputStream(
"data2.txt"
).getChannel();
fc.write(ByteBuffer.wrap(
"Some text"
.getBytes(
"UTF-16BE"
)));
fc.close();
// Now try reading again:
fc =
new
FileInputStream(
"data2.txt"
).getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
// Use a CharBuffer to write through:
fc =
new
FileOutputStream(
"data2.txt"
).getChannel();
buff = ByteBuffer.allocate(
24
);
// More than needed
buff.asCharBuffer().put(
"Some text"
);
fc.write(buff);
fc.close();
// Read and display:
fc =
new
FileInputStream(
"data2.txt"
).getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());
}
}
|
3. 内存映射和ByteBuffer的使用
这段内容将简单说明下文件内存映射的概念和ByteBuffer的其它点。
ByteBuffer的最基本使用,和上一篇NIO中讲CharBuffer等Buffer的直接子类一样,就是put()和get()。为提高效率,除了单个字节读写,有整块的操作方法,即对get()和put()的重载方法。
而内存映射这个概念最初一直困惑了我很久才搞明白,但实际上原理并不复杂,只需要了解操作系统工作的最基本原理。我们通常的直接通过API做IO,会用到一系列的系统调用(system call),之后通过驱动程序和外部设备交互来完成输入输出操作,磁盘上的文件读写也是一样。而文件内存映射之所以得到很好的使用是因为,使用了文件的内存映射可以大大提高效率。提高效率的点就在于,不必每个IO操作都经过系统调用来完成,这个效率是相对较低的,而是巧妙灵活地使用内存管理系统。我们都知道当程序需要使用大量内存而实际物理内存较小的时候,我们的内存管理系统会进行页的换入换出操作,使部分当前使用不到的内存页放到磁盘上去,而缺页中断又会相应的做换入操作 —— 这就是内存映射的基础。
在Java中,在Java NIO的FileChannel类中,提供了一个map()方法,这个方法返回的结果就是一个MappedByteBuffer类的对象,也就是一个ByteBuffer对象。这使得我们对磁盘上文件内容的读写,完全可以像对其他Buffer一样,进行put()和get()。
4. 其它一些实现细节
这是一些未深入整理的实现细节点,在Win下的Oracle/Sun JDK: