3.2.2 字节缓冲区
对于Java中的基本类型,除了布尔类型之外,都有对应的缓冲区实现,用来存储此类型的数据。布尔类型的缓冲区可以很容易地用字节类型来替代。这些缓冲区类型中最重要的实现类是java.nio.ByteBuffer类。在ByteBuffer类中,除了可以对基本的字节进行操作之外,还可以操作其他基本类型的数据。对于字节数据,除了每次操作单个字节之外,还支持批量式处理一个字节数组。代码清单3-3中给出了使用ByteBuffer类的示例。在创建ByteBuffer类的对象时,只能通过其静态工厂方法allocate来分配新空间,或者通过wrap方法来包装一个已有的字节数组。在创建ByteBuffer类的对象时需要指定缓冲区的容量。在创建完成之后,可以通过put方法向缓冲区中添加数据,而get方法则从其中读取数据。进行绝对读写操作的put和get方法的第一个参数都是读写位置的序号。需要注意的是,这个序号是根据字节数来进行计算的。如果在写入数据时使用的是类似putChar或putLong这样的操作基本数据类型的方法,在通过相应的getChar或getLong方法来读取的时候,需要开发人员自己来计算起始字节的位置。如果计算错误,那么得到的就不是当时写入的数据。如果ByteBuffer类的对象中存放的是不同类型的数据,那么这种计算是无法避免的。如果ByteBuffer类的对象中存放的是相同的类型,可以考虑使用对应类型的缓冲区实现类,或是从ByteBuffer类的对象中创建相应的视图。
代码清单3-3 ByteBuffer类的使用示例
public void useByteBuffer(){
ByteBuffer buffer=ByteBuffer.allocate(32);
buffer.put((byte)1);
buffer.put(new byte[3]);
buffer.putChar('A');
buffer.putFloat(0.0f);
buffer.putLong(10,100L);
buffer.getChar(4);//值为'A'
}
由于ByteBuffer类支持对基本数据类型的处理,因此必须要考虑字节顺序。同样的字节序列按照不同的顺序去解释,所得到的结果是不同的。java.nio.ByteOrder类中定义了两种最基本的字节顺序:BIG_ENDIAN对应的大端表示和LITTLE_ENDIAN对应的小端表示。大端表示的含义是字节序列中高位在前,而小端表示则正好相反。ByteOrder类中的静态方法nativeOrder可以获取到底层操作系统平台采用的字节顺序。ByteBuffer类的对象默认使用的是大端表示。代码清单3-4中给出了一个通过修改字节顺序来改变基本类型的值的示例。
代码清单3-4 字节缓冲区的字节顺序
public void byteOrder(){
ByteBuffer buffer=ByteBuffer.allocate(4);
buffer.putInt(1);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.getInt(0);//值为16777216
}
ByteBuffer类支持的另外一个操作是压缩。压缩操作的一个典型的应用场景是把ByteBuffer类的对象作为数据传输时的缓冲区来使用。例如,有一个发送者不断地向ByteBuffer类的对象中填充数据,而另外一个接收者从相同的ByteBuffer类的对象中不断地获取数据。发送者可能用数据填充满了ByteBuffer类的对象中读写限制范围之内的全部可用空间,而接收者却暂时只读取了ByteBuffer类的对象中的一部分数据。接收者在完成读取之后要使用compact方法进行压缩操作。压缩操作就是把ByteBuffer类的对象中当前读写位置到读写限制范围内的数据复制到内部存储空间中的最前面,然后再把读写位置移动到紧接着复制完成的数据的下一个位置,读写限制也被设置成ByteBuffer类的对象的容量。经过压缩之后,发送者的下次写入操作就不会覆盖接收者还没读取的内容,而接收者每次总是可以从ByteBuffer类的对象的起始位置进行读取。代码清单3-5中给出了压缩操作的使用示例。
代码清单3-5 字节缓冲区的压缩操作的示例
public void compact(){
ByteBuffer buffer=ByteBuffer.allocate(32);
buffer.put(new byte[16]);
buffer.flip();
buffer.getInt();//当前读取位置为4
buffer.compact();
int pos=buffer.position();//值为12
}
在代码清单3-5中,经过put、flip和getInt等方法调用之后,ByteBuffer类的对象的当前读取位置是4,而读取限制是16,所以在压缩的时候,没有被读取的12个字节会被复制到ByteBuffer类的对象的内部存储空间的开头位置,同时当前读取位置变为被复制的字节数,即12。
ByteBuffer类的实现分为直接缓冲区和非直接缓冲区两种。在直接缓冲区中进行I/O操作时,会尽可能避免多余的数据复制,而直接使用操作系统底层的I/O操作来完成。这样可以提升读写操作时的性能,不过也带来了额外的创建和销毁时的代价。直接缓冲区一般是常驻内存的,会增加程序的内存开销,所以直接缓冲区一般只用在对性能要求很高的情况中。通过ByteBuffer类的静态方法allocateDirect可以创建新的直接缓冲区,这类似于allocate方法的使用。