Buffer
整个java.nio由Buffer、Channels、Selector、字符集和正则表达式组成,本节我们对Buffer进行展开。
一个Buffer对象是固定数量的数据的容器。对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。
缓冲区的工作与通道紧密联系。通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。这种在协同对象(通常是所写的对象以及一到多个Channel对象)之间进行的缓冲区数据传递是高效数据处理的关键。
一、Buffer基础
1.1 属性
如图1.1,对 于每个非布尔原始数据类型都有一个缓冲区类,我们先剖析Buffer,再对某些子类进行展开。
图1.1 Buffer类的展开
本质上,缓冲区是包在一个对象内的基本数据元素数组,只不过Buffer类对其进行了有效包装,定义了用于处理数据缓冲区的 API。
1 public abstract class Buffer { 2 // Invariants: mark <= position <= limit <= capacity 3 private int mark = -1; 4 private int position = 0; 5 private int limit; 6 private int capacity; 7 }
所有的缓冲区都通过四个属性来提供关于其所包含的数据元素的信息:
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
位置(Position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
标记(Mark):一个备忘位置。调用mark( )来设定mark = postion。调用reset( )设定position = mark。标记在设定前是未定义的(undefined)。
这四个属性之间总是遵循:0 <= mark <= position <= limit <= capacity。
图1.2 新建的ByteBuffer
如图1.2,其位置被设为 0,而且容量和上界被设为 10,刚好经过缓冲区能够容纳的最后一个字节。标记最初未定义。容量是固定的,但另外的三个属性可以在使用缓冲区时改变。
1.2 API
public abstract class Buffer { public final Buffer clear( ) public final Buffer flip( ) public final Buffer rewind( ) public final int remaining( ) public final boolean hasRemaining( ) public abstract boolean isReadOnly( ); }
Buffer中的API有很多,这里我们主要对一些需要注意的API进行一些说明。
1.2.1 isReadOnly( )
所有的缓冲区都是可读的,但并非所有都可写,例如MappedByteBuffer的内容可能实际是一个只读文件。每个具体的缓冲区类都通过执行isReadOnly( )来标示其是否允许该缓存区的内容被修改。对只读的缓冲区的修改尝试将会导致 ReadOnlyBufferException 抛出。
1.2.2 put( )和get( )
Buffer的API并没有get( )或put( )函数,因为它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以不能在顶层 Buffer 类中被抽象地声明,而是在子类进行定义。
1 public abstract class ByteBuffer 2 extends Buffer implements Comparable{ 3 public abstract byte get( ); 4 public abstract byte get (int index); 5 public abstract ByteBuffer put (byte b); 6 public abstract ByteBuffer put (int index, byte b); 7 } 8
put和get可以是相对的或者绝对的。相对函数依赖于position属性(在调用put( )时指出了下一个数据元素应该被插入的位置,或者当get( )被调用时指出下一个元素应从何处检索),当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛出异常(nio包中自定义的异常)。
绝对函数依赖于数组索引(下标),当绝对函数被调用时,不会影响position属性,抛出的异常是IndexOutOfBoundsException。
提供两种存取方式的原因,例如,我们将‘hello’载入CharBuffer中,但是现在我们需要将其‘Mellow’,当然我们可以clear( )后,重新调用put( )。
1 buffer.put('H').put('e').put('l').put('l').put('o');
但更好的方式是:
1 buffer.put(0,'M').put('w');
这里第一次使用绝对调用,将‘H’替换为‘M’,绝对调用不改变position,因此再调用相对调用,将其位置属性加一。
另外需要注意的是,使用put和get进行批量操作时,批量传输的大小总是固定的。如果使用参数只有数组的函数,操作时会使用到数组中全部数据。若是get操作,小缓冲区到大数组时会抛出BufferUnderflowException异常;若是put操作,大缓冲区到小数组时也会抛出异常。合理的操作是:
1 char [] smallArray = new char [10]; 2 while (buffer.hasRemaining( )) { 3 int length = Math.min (buffer.remaining( ), smallArray.length); 4 buffer.get (smallArray, 0, length); 5 processData (smallArray, length); 6 }
1.2.3 flip( )和remind( )
假设缓冲区已经写入了数据(不一定写满了capacity),因为执行完put( ),position被定位到刚插入的有效数据的下一位,现在如果有通道在缓冲区上执行get( ),那么它将从我们刚刚插入的有用数据之外取出未定义数据,如图1.3。
图1.3 翻转后的Buffer
只有我们将位置值重新设为0,通道才会从正确位置开始获取,但是怎么知道何时到达我们所插入数据末端?这就是limit属性被引入的目的。limit属性指明了缓冲区有效内容的末端。我们需要将limit属性设置为当前位置,然后将位置重置为0。
1 buffer.limit(buffer.position()).position(0);
当然可以这么实现,不过flip( )函数更便利,它将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态,如图1.4。
图1.4 翻转前的Buffer
rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。可以使用rewind( )后退,重读已经被翻转的缓冲区中的数据。
如果将缓冲区翻转两次,那么它实际上会大小变为0,此时put和get操作就会抛出异常,需要重新设置属性。
1.2.4 clear( )
clear( )函数将缓冲区重置为空状态,但实际上它并不改变缓冲区中的任何数据元素,而是仅仅将limit设为capacity的值,把position设回0,并丢弃标记。
注:缓冲区并不是多线程安全的。如果想以多线程同时存取特定的缓冲区,需要在存取缓冲区之前进行同步。
1.2.5 compact( )
有时可能会从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,就需要从数组的0下标开始,将还未读的数据元素复制。
图1.5 需要部分释放的缓冲区
图1.6 压缩后的缓冲区
如图1.5,调用compact( ),其执行方式是将要复制的数据直接从数组0位置开始覆盖掉之前的数据,但4-5位置并没有受影响。limit被设为被复制的数据元素的数目,即缓冲区现在被定位在缓冲区中最后一个“存活”元素后插入数据的位置;上界属性被设置为容量的值。
1.2.6 mark( )
标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在mark( )函数被调用之前是未定义的,调 用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调 用 reset( )将导致 InvalidMarkException 异常。一些缓冲区函数会抛弃已经设定的标记 (rewind( ),clear( ),以及 flip( )总是抛弃标记)。如果调用 limit( )或 position( )带有索引参数的版本设置新的值,这个值比当前的标记小,则会抛弃标记。
1.2.7 equals( )和compareTo( )
使用equals( )函数,两个缓冲区被认为相等的充要条件是:
- 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
- 两个对象都剩余相同的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素(从position到limit)必须相同。
使用compareTo( )函数以词典顺序进行比较。compareTo( )也不允许不同对象间进行比较,但 compareTo( )更为严格:如果传递一个类型错误的对象,它会抛出 ClassCastException 异常,但equals( )只会返回 false。
二、创建缓冲区
以上提到的所有类没有一种能够直接实例化,它们都是抽象类,都是用包含静态工厂方法创建相应类的新实例。以CharBuffer为例,新的缓冲区是由分配或包装操作创建的。
1 public abstract class CharBuffer 2 extends Buffer implements CharSequence, Comparable{ 3 // This is a partial API listing 4 public static CharBuffer allocate (int capacity) 5 public static CharBuffer wrap (char [] array) 6 public static CharBuffer wrap (char [] array, int offset, int length) 7 public final boolean hasArray( ) 8 public final char [] array( ) 9 public final int arrayOffset( ) 10 }
(1)分配操作创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。
1 CharBuffer charBuffer = CharBuffer.allocate (100);
这段代码隐含地从堆空间中分配了一个char型数组作为备份存储器来储存 100 个 char变量。
(2)包装操作创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用您所提供的数组作为存储空间来储存缓冲区中的数据元素。它调用 wrap()函数:
1 char [] myArray = new char [100]; 2 CharBuffer charbuffer = CharBuffer.wrap (myArray);
这段代码构造了一个新的缓冲区对象,但数据元素会存在于数组中。这意味着通过调用put()函数造成的对缓冲区的改动会直接影响这个数组,而且对这个数组的任何改动也会对这个缓冲区对象可见。
(3)使用带有offset和length作为参数的 wrap()函数,则会构造一个按该offset和length参数值初始化位置和上界的缓冲区:
1 CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42);
实际上这个缓冲区并不是只占用了一个数组子集的缓冲区,这个缓冲区可以存取这个数组的全部范围,offset和length参数只是设置了初始的状态。
1 char[] chars = new char[]{'a','b','c','d','e','f'}; 2 CharBuffer buffer = CharBuffer.wrap(chars,0,5); 3 4 while (buffer.hasRemaining()) 5 System.out.print(buffer.get()+""); 6 buffer.clear(); 7 8 System.out.println(); 9 10 while (buffer.hasRemaining()) 11 System.out.print(buffer.get()+""); 12 13 result://a b c d e 14 //a b c d e f 15
slice( )函数可以提供一个只占用备份数组一部分的缓冲区,下文讨论。
(3)通过allocate( )或者wrap( )函数创建的缓冲区通常都是间接的。使用hasArray( )函数,若返回true,则该缓冲区有一个可存取的备份数组,可用array()函数返回这个缓冲区对象所使用的数组存储空间的引用;若返回 false,调用array( )函数或者arrayOffset( )函数会抛UnsupportedOperationException 异常。
(4)如果一个缓冲区是只读的,它的备份数组将会是超出上界的,即使一个数组对象被提供给 wrap( )函数。调用array( )或arrayOffset ()会抛出ReadOnlyBufferException异常,来阻止对得到存取权来修改只读缓冲区的内容。如果通过其它的方式获得了对备份数组的存取权限,对这个数组的修改也会直接影响到这个只读缓冲区。
(5)arrayOffset( )返回缓冲区数据在数组中存储的开始位置的偏移量。如果使用了带有三个参数的版本wrap( )函数来创建一个缓冲区,arrayOffset( )会一直返回0。
三、复制缓冲区
我们以CharBuffer为例,,它们都是通过调用已存在的缓冲区实例中的函数来创建。
1 public abstract class CharBuffer 2 extends Buffer implements CharSequence, Comparable{ 3 // This is a partial API listing 4 public abstract CharBuffer duplicate( ); 5 public abstract CharBuffer asReadOnlyBuffer( ); 6 public abstract CharBuffer slice( ); 7 }
(1)duplicate( )函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。该副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。
注:复制一个缓冲区会创建一个新的Buffer对象,但并不复制数据。原始缓冲区和副本都会操作同样的数据元素。
(2)asReadOnlyBuffer( )函数来生成一个只读的缓冲区视图。新的缓冲区不允许使用put( ),并且isReadOnly( )函数将会返回 true。
注:如果一个只读的缓冲区与一个可写的缓冲区共享数据,或者有包装好的备份数组,那么对这个可写的缓冲区或直接对这个数组的改变将反映在所有关联的缓冲区上,包括只读缓冲区。
(3)slice( )创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数(limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。
四、直接缓冲区
字节缓冲区跟其他缓冲区类型明显的不同是,它们可以成为通道所执行的I/O的源头或目标(通道只接收ByteBuffer作为参数)。
操作系统的在内存区域中进行I/O操作,这些内存区域就操作系统方面而言,是相连的字节序列(I/O简述)。因此只有字节缓冲区有资格参与I/O操作,而I/O操作的目标内存区域必须是连续的字节序列。在JVM中,字节数组可能不会在内存中连续存储,或者无用存储单元收集可能随时对其进行移动。
在Java中,数组是对象,而数据存储在对象中的方式在不同的JVM实现中都各有不同。出于这一原因,引入了直接缓冲区的概念。直接缓冲区被用于与通道和固有I/O例程交互。它们通过使用固有代码来告知操作系统直接释放或填充内存区域,对用于通道直接或原始存取的内存区域中的字节元素的存储尽了最大的努力。
非直接字节缓冲区可以被传递给通道,但性能损耗相对更大。通常非直接缓冲不可能成为一个本地 I/O 操作的目标。如果您向一个通道中传递一个非直接ByteBuffer对象用于写入,通道可能会在每次调用中隐含地进行下面的操作:
1)创建一个临时的直接ByteBuffer对象。
2)将非直接缓冲区的内容复制到临时缓冲中。
3)使用临时缓冲区执行低层次I/O操作。
4)临时缓冲区对象离开作用域,并最终成为被回收的无用数据。
这可能导致缓冲区在每个I/O上复制并产生大量对象。相比之下,直接缓冲区虽然在创建非直接缓冲区要花费更高的成本(直接缓冲区使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈。建立和
销毁直接缓冲区会明显比具有堆栈的缓冲区更加费力)。
但直接缓冲区的内存区域不受无用存储单元收集支配,因为它们位于标准JVM堆栈之外。直接ByteBuffer是通过调用具有所需容量的ByteBuffer.allocateDirect( )函数产生。注:wrap( )函数所创建的被包装的缓冲区总是非直接的。
1 public abstract class ByteBuffer 2 extends Buffer implements Comparable{ 3 // This is a partial API listing 4 public static ByteBuffer allocate (int capacity) 5 public static ByteBuffer allocateDirect (int capacity) 6 public abstract boolean isDirect( ); 7 }
五、视图缓冲区
这里我们主要说ByteBuffer,视图缓冲区通过已存在的缓冲区对象实例的工厂方法来创建,这种视图对象维护它自己的属性,容量,位置,上界和标记,但是和原来的缓冲区共享数据元素。实际上和上面“复制缓冲区”一样。
1 public abstract class ByteBuffer 2 extends Buffer implements Comparable{ 3 // This is a partial API listing 4 public abstract CharBuffer asCharBuffer( ); 5 public abstract ShortBuffer asShortBuffer( ); 6 public abstract IntBuffer asIntBuffer( ); 7 public abstract LongBuffer asLongBuffer( ); 8 public abstract FloatBuffer asFloatBuffer( ); 9 public abstract DoubleBuffer asDoubleBuffer( ); 10 }
上面的每一个工厂方法都在原有的ByteBuffer对象上创建一个视图缓冲区,调用其中的任何一个方法都会创建对应的缓冲区类型,这个缓冲区是基础缓冲区的一个切分,由基础缓冲区的位置和上界决定。新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型中组成一个数据类型的字节数。在切分中任一个超过上界的元素对于这个视图缓冲区都是不可见的。视图缓冲区的第一个元素从创建它的 ByteBuffer 对象的位置开始(positon( )函数的返回值)。
1 public abstract class ByteBuffer 2 extends Buffer implements Comparable{ 3 public abstract char getChar( ); 4 public abstract char getChar (int index); 5 }
这里一起说说ByteBuffer提供的转换方法,这些函数从当前位置开始存取ByteBuffer的字节数据,根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成需要的原始数据类型。例如,如果 getInt()函数被调用,从当前的位置开始的四个字节会被包装成一个int类型的变量然后作为函数的返回值返回。显然这种方式不是那么精确。
六、 内存映射缓冲区
映射缓冲区是带有存储在文件,通过内存映射来存取数据元素的字节缓冲区。映射缓冲区通常是直接存取内存的,只能通过 FileChannel 类创建。映射缓冲区的用法和直接缓冲区类似,但是 MappedByteBuffer 对象可以处理独立于文件存取形式的的许多特定字符。