3.2 缓冲区

从前面对Java I/O中流的概念和实现的介绍可以看出,Java中流的实现采用了一种简单而朴素的做法,即以字节流为基础,在字节流上再通过过滤流来满足不同的需要。对于开发人员来说,流加上字节数组的使用方式的抽象层次较低,使用起来比较繁琐。这其中比较麻烦的一点在于字节数组的长度是不可变的。一旦创建了某个长度的字节数组,当数据过多以至于超出数组长度的限制时,需要开发人员自己来进行管理,比如重新创建一个新的长度更大的数组,然后再把之前的数据复制进去。一种可行的做法是利用ByteArrayOutputStream类,不断地向ByteArrayOutputStream类的对象中写入数据,写入完成之后,用它的toByteArray方法可以得到一个字节数组。但这种做法缺乏足够的灵活性,性能也比较差。更好的做法是使用Java NIO中新的缓冲区实现。

3.2.1 基本用法

Java NIO中的缓冲区在某些特性上类似于Java中的基本类型的数组(如字节数组或整型数组等),比如缓冲区中的数据排列是线性的,缓冲区的空间也是有限的。不过两者的差别也是显著的,最主要的区别在于缓冲区所提供的功能远比数组丰富得多,而且也支持存储类型异构的数据。要理解缓冲区的使用,就需要理解缓冲区的3个状态变量,分别是容量(capacity)、读写限制(limit)和读写位置(position)。使用缓冲区时产生的错误,绝大多数都源自错误地理解了这3个变量的含义。

首先最容易理解的状态变量是缓冲区的容量。容量指的是缓冲区的额定大小。容量是在创建缓冲区时指定的,无法在创建后更改。在任何时候缓冲区中的数据总数都不可能超过容量。第二个变量是读写限制,表示的是在缓冲区中进行读写操作时的最大允许位置。比如对于一个容量为32的缓冲区来说,如果设置其读写限制的值是16,那么就只有前半个缓冲区在读写时是可用的。如果希望后半个缓冲区也能进行读写操作,就必须把读写限制设置为32。最后一个变量是读写位置,表示的是当前进行读写操作时的位置。当在缓冲区中进行相对读写操作时,在这个位置上进行。对于这3个变量,除了只能获取容量外,读写限制和读写位置都有相应的获取和设置的方法,分别是limit和position,其中不带参数的重载形式用来获取值,而带参数的形式用来设置值。对于一个缓冲区来说,它的当前可以使用的范围是在读取位置和读取限制之间的这一段区域。通过remaining方法可以获取到这段可用范围的长度。

缓冲区同样也支持标记和重置的特性,类似于前面提到的流的标记和重置。当调用mark方法时,会在当前的读写位置上设置一个标记。在调用reset方法之后,会使得读写位置回到上一次mark方法设置的位置上。进行标记时的位置不能超过当前的读写位置。如果通过position方法重新设置了读写位置,而使之设置的标记的位置超出了新的读写位置的范围,那么该标记就会失效。在任何时候,缓冲区中的各个状态变量之间满足关系“0<=标记位置<=读写位置<=读写限制<=容量”。

Java NIO中的java.nio.Buffer类是所有不同数据类型的缓冲区的父类。一般来说,通过调用缓冲区对象的limit和position方法就可以满足大部分的需求。不过对于某些常见的场景,Buffer类也提供了快捷的方法。当复用一个已有的缓冲区时,如果希望向缓冲区中写入数据,可以调用clear方法,该方法会把读写限制设为缓冲区的容量,同时把读写位置设为0;当需要读取一个缓冲区中的数据时,可以调用flip方法,该方法会把读写限制设为当前的读写位置,再把读写位置设为0,这样可以保证缓冲区中的全部数据都可以被读取;当希望重新读取缓冲区中的数据的时候,可以调用rewind方法,该方法不会改变读写限制,但是会把读写位置设为0。在后面的示例代码中,可以看到这几个方法在缓冲区读写操作时的使用模式。熟悉这些模式,就会避免出现一些常见的错误。

缓冲区进行的读写操作分成两类:一类是根据当前读写位置进行的相对读写操作,另外一类是根据在缓冲区中的绝对位置进行的读写操作。两者的差别在于相对读写会改变当前读写位置,而绝对读写则不会。