2.1.2 Buffer API
现在让我们看看怎么使用一个Buffer。下面列举的是Buffer类的方法。
package java.nio;
public abstract class Buffer {
public final int capacity();
public final int position();
public final Buffer position(int pos);
public final int limit();
public final Buffer limit(int pos);
public final Buffer mark();
public final Buffer reset();
public final Buffer clear();
public final Buffer flip();
public final Buffer rewind();
public final int remaining();
public final boolean hasRemaining();
public abstract boolean isReadOnly();
}
首先需要注意的是那些通常需要返回void的方法,比如clear(),在这里返回的是一个Buffer引用。这个方法返回的是被调用对象的引用(即this)。这样的做法是一种类设计技术,可以允许程序员进行级联调用。如下面的代码:
buffer.mark();
buffer.position(5);
buffer.reset();
就可以被简写为:
buffer.mark().position(5).reset();
java.nio包中的类被设计成级联调用的形式。你可能在StringBuffer类中看到同样的用法。
注意: 级联调用如果使用的好,可以产生简单、优雅、易读的代码。如果滥用也将会导致一堆不知所云的代码。一般使用级联调用是为了让你的代码更加具有可读性,让你的想法更清晰。如果级联调用降低了代码的清晰度,那么请不要使用它们。请时刻保证你代码的可读性。 |
对于上面的API需要注意还要isReadOnly()方法。所有的Buffer都是可读的,但不是所有的都是可写的。每个具体的Buffer子类都继承了isReadOnly()方法来标示这个Buffer是否能被修改。有一些类型的Buffer类可能并没有存储数据的数组。比如MappedByteBuffer的数据可能实际保存在只读的文件中。你也可以显示的创建一个只读的视图Buffer,来防止对数据的意外修改。试图对只读的Buffer类的修改将会导致ReadOnlyBufferException的抛出。但是我们要超越自己。
2.1.3 访问
让我们从基础开始。缓冲区管理着固定大小数目的元素。但是在特定的时间,我们可能只关心缓冲区中的一部分数据。也就是说,可能在移除数据之前,我们只对缓冲区填充了一部分数据。这时我们需要知道向缓冲区中添加了多少数据,下一个数据将放在哪里等等信息。位置(Position)属性就为我们提供了这样的功能。位置属性将决定当我们调用put()方法时下一个数据元素被插入的位置和调用get()方法时下一个元素从哪里取出。机敏的读者可能发现上面的Buffer的API中并没有get()和put()方法。这是因为每一个Buffer子类都提供了这两个方法,只是因为每个Buffer子类处理的数据类型都不一样,因此每个子类的put()和get()方法都有不同的参数和不同类型的返回值,因而他们不能被放入抽象的父类中。他们的定义必须遵从特定数据类型的子类。对于以下的讨论,我们将假定使用ByteBuffer类,这个类的方法被列在下面(它包含了get()方法和put()方法,对这两个方法的讨论将在2.1.10小节中讨论):
public abstract class ByteBuffer
extends Buffer implements Comparable {
public abstract byte get();
public abstract byte get(int index);
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(int index, byte b);
}
Get和put访问数据的方法可以是相对的也可以是绝对的。就像前面列出的,相对版本的方法没有参数。当相对版本的方法被调用时,位置属性加上一。如果位置数据前进的太多,那么方法就会抛出异常。比如put方法,如果位置属性大于限制属性,那么方法就会抛出BufferOverflowException异常。而对于get方法,如果位置属性不小于限制属性,那么也将抛出BufferUnderflowException异常。绝对版本的方法不会改变位置属性,但是如果参数index不在有效的范围之内(比如是负数或者不小于限制属性),那么也将抛出IndexOutOfBoundsException异常。
2.1.4 填充
先举个例子。我们将代表“Hello”字符串的ascii码序列载入一个名为buffer的ByteBuffer对象。当下面的代码在图2.2中创建的buffer对象执行之后,buffer的状态将如图2.3所示的一样。
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
图2.3 五次调用put之后的buffer
注意这个例子中所有的字符都被强制转换为字节类型。我们不能不经过转换,就像:
buffer.put('H');
因为我们存放的是字节而不是字符。记住在java中所有的字符内部表示都是unicode编码的,每个字符占两个字节。这一小节的例子使用的字节是跟ascii字符集数值值一样大小的量。通过将字符转换为字节,我们丢掉了字符的高8位,使用低8位来填充位置。这样做只适合Latin字符而不适合所有的unicode字符。为了让事情简化,我们暂时将避免讨论字符串映射问题。第六章中将包括字符集的详细内容。
既然我们的数据已经存放在buffer中了,如果我们想改变里面的数据但是又不想改变当前buffer的位置属性,该怎么办呢?绝对路径版本的put方法就可以达到这样的目的。假设我们想将buffer中ascii字符的数据从"Hello"改为"Mellow",我们可以像下面那样做:
buffer.put(0,(byte)'M').put((byte)'w');
我们使用绝对版本的put方法将位置为0的数据使用十六进制为0x4d代替,然后将0x77放入当前位置属性标示的位置,最后一个put方法将位置属性增加一。最后的结果如图2.4所示。
图2.4 改变后的buffer