1.概述 上一章提到Buffer作为抽象类,仅仅定义了数据的容量管理,以及对读写位置的部分规则约定,并且提供了除boolean类型外,剩余七种基本数据类型的读写实现类,这七种实现类提供的API,除了读写的单位不同,其他地方几乎一样。
Buffer的所有子类中,其中最特别的就是ByteBuffer,因为其他基本数据类型都可以转化为byte,因此ByteBuffer除了提供字节类型的读写操作外,还提供了其他六种基本数据类型的重载方法,所以学习缓冲区,只要掌握ByteBuffer以及子类即可。
2.Buffer抽象父类 2.1 成员变量与构造器 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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 public abstract class Buffer { private int mark = -1 ; private int position = 0 ; private int limit; private int capacity; long address; Buffer(int mark, int pos, int lim, int cap) { if (cap < 0 ) throw new IllegalArgumentException("Negative capacity: " + cap); this .capacity = cap; limit(lim); position(pos); if (mark >= 0 ) { if (mark > pos) throw new IllegalArgumentException("mark > position: (" + mark + " > " + pos + ")" ); this .mark = mark; } } public final int capacity () { return capacity; } public final int position () { return position; } public final Buffer position (int newPosition) { if ((newPosition > limit) || (newPosition < 0 )) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1 ; return this ; } public final int limit () { return limit; } public final Buffer limit (int newLimit) { if ((newLimit > capacity) || (newLimit < 0 )) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1 ; return this ; } public final Buffer mark () { mark = position; return this ; } public final Buffer reset () { int m = mark; if (m < 0 ) throw new InvalidMarkException(); position = m; return this ; } final void truncate () { mark = -1 ; position = 0 ; limit = 0 ; capacity = 0 ; } final void discardMark () { mark = -1 ; } }
capacity(容量) ,缓冲区能够容纳的数据元素的最大数量。这个变量在创建一个Buffer实现类时指定,并且在创建完成后永远不能被修改。当缓冲区被填满后,需要手动调用API进行清理,后续才能继续往缓冲区写数据。
limit(边界) ,缓冲区最多可读或写的元素数量,由于元素坐标是从0开始,limit也可以解释为第一个不能被读或写的位置。例如创建缓冲区时指定capacity=1024,那么limit默认值也是1024,可写入的数组坐标是0~1023,当limit值被设置为512时,可写入的数组坐标变为0~511,之后的数组坐标都无法进行读写,就是说读写的坐标必须小于limit值,否则报错。
position(位置) ,下一个要读取或写入的元素坐标值。每次调用put()或子类的get()方法,都会自动的计算更新position值,因为position是从0开始的,因此最大值为capacity-1。
mark(标记) ,标记数组中的某个坐标位置。具体作用和InputStream实现类一样,使用mark()方法配合reset()方法覆盖position值,实现对系统资源的重复读取。
address(堆外地址) ,如果Buffer的最底层实现类为堆外内存,此属性为Buffer实现类在堆外申请的内存空间地址,所有的读写等操作都是针对这个地址对应的内存数据。
2.2 抽象方法 1 2 3 4 5 6 7 8 9 10 public abstract boolean isReadOnly () ;public abstract boolean hasArray () ;public abstract Object array () ;public abstract int arrayOffset () ;public abstract boolean isDirect () ;
isReadOnly()抽象方法,缓冲区是否为只读状态。 常用的HeapByteBuffer、DirectByteBuffer类都是读写缓冲区,JDK也为我们提供了对应的只读类,比如HeapByteBuffer对应的HeapByteBufferR,结尾的R就是只读的意思,HeapByteBufferR类重写此方法固定返回false,并且调用put相关方法都会抛出异常。
hasArray()抽象方法,是否由可访问数组支持。 如果子类是HeapByteBuffer这种基于数组维护缓冲数据的,并且不是只读状态,重写时返回true。像DirectByteBuffer等类是将缓冲数据放在堆外,内存地址通过代码进行管理,没有数组这个概念,则重写时需要返回false,代表不支持数组相关操作。
array()抽象方法,返回缓冲区的数组对象。 如果子类在重写hasArray()方法时,将返回值定义为true,重写此方法一定会返回一个数组对象,如果重写的hasArray()方法返回false,则需要重写此方法时抛出异常。因此通过多态形式操作缓冲区对象,调用此方法前一定要调用hasArray()进行安全校验。
arrayOffset()抽象方法,返回缓冲区数组内第一个元素的下坐标。 这个方法和array()方法类似,都是获取缓冲数组的相关信息,每次读取缓冲区数据时,读取的坐标为position + arrayOffset()。
2.3 读写信息相关方法 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 public final Buffer flip () { limit = position; position = 0 ; mark = -1 ; return this ; } public final Buffer rewind () { position = 0 ; mark = -1 ; return this ; } public final Buffer clear () { position = 0 ; limit = capacity; mark = -1 ; return this ; } public final int remaining () { return limit - position; } public final boolean hasRemaining () { return position < limit; }
flip()方法 ,将Buffer从写模式切换到读模式。首先limit被设置为当前position的值,然后将position设置为0,这就意味着接下来的读取坐标范围,不会超出调用此方法前写入的最大坐标。因此每次切换读模式,一定要手动将读完的数据清除,否则下次position重置为0后,数据会重复读取。既然读完会被清理掉,那么将mark标记作废,也就理所当然了。
rewind()方法 ,也是将Buffer从写模式切换到读模式。比flip()方法少了一个步骤,就是没有重置limit的值,这意味着接下来能读取的坐标范围,取决于之前设置的limit值,有可能接下里的读取操作无法读完之前写入的数据,也有可能会读到没有写入数据的位置(返回基础数据类型的默认值)。
clear()方法 ,也是将Buffer从写模式切换到读模式。与前俩个方法相比,此方法直接将可读的坐标范围开到最大,即使某些坐标没有写入元素也会被读取,返回对应实现类的默认值,例如ByteBuffer子类未写入的坐标默认值是0。
remaining()方法 ,返回目前还有多少个位置可以提供读写,由于可读写的位置受limit参数限制,所以还有多少个位置能提供读写,需要用limit减去已读写的数量。
hasRemaining()方法 ,是否还有可提供读写的位置,由于可读写的位置受limit参数限制,所以是否能继续读写,是根据位置是否到达limit决定。
2.4 读写操作相关方法 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 44 45 46 47 48 49 50 51 52 final int nextGetIndex () { if (position >= limit) throw new BufferUnderflowException(); return position++; } final int nextGetIndex (int nb) { if (limit - position < nb) throw new BufferUnderflowException(); int p = position; position += nb; return p; } final int nextPutIndex () { if (position >= limit) throw new BufferOverflowException(); return position++; } final int nextPutIndex (int nb) { if (limit - position < nb) throw new BufferOverflowException(); int p = position; position += nb; return p; } final int checkIndex (int i) { if ((i < 0 ) || (i >= limit)) throw new IndexOutOfBoundsException(); return i; } final int checkIndex (int i, int nb) { if ((i < 0 ) || (nb > limit - i)) throw new IndexOutOfBoundsException(); return i; } static void checkBounds (int off, int len, int size) { if ((off | len | (off + len) | (size - (off + len))) < 0 ) throw new IndexOutOfBoundsException(); }
Buffer作为一个抽象类,无法确定读写的数据类型、堆内还是堆外存储数据,因此读写相关的方法都是辅助性质的,仅支持读写操作时位置的维护与校验工作。
2.5 总结 Buffer抽象类作为缓冲区的顶层抽象类,没有提供任何关于读写操作的方法,也没有定义任何存储缓冲数据的数组,仅仅定义了堆外缓冲区实现类会用到的内存地址,这主要是因为读写数据的类型有很多,不同的数据类型对应的方法参数不同,Buffer无法进行抽象。
Buffer在NIO设计中的职责,是将数据类型以外的操作规范进行定义,并让所有子类继承并遵循这套规范,比如定义缓冲区的大小并强制不能修改、每次读写前对位置的校验与新位置计算、读取的边界限制、数据元素重读等。
3.ByteBuffer抽象类 3.1 成员变量与创建方式 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 public abstract class ByteBuffer extends Buffer implements Comparable <ByteBuffer > { final byte [] hb; final int offset; boolean isReadOnly; ByteBuffer(int mark, int pos, int lim, int cap, byte [] hb, int offset){ super (mark, pos, lim, cap); this .hb = hb; this .offset = offset; } ByteBuffer(int mark, int pos, int lim, int cap) { this (mark, pos, lim, cap, null , 0 ); } public static ByteBuffer allocateDirect (int capacity) { return new DirectByteBuffer(capacity); } public static ByteBuffer allocate (int capacity) { if (capacity < 0 ) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); } public static ByteBuffer wrap (byte [] array, int offset, int length) { try { return new HeapByteBuffer(array, offset, length); } catch (IllegalArgumentException x) { throw new IndexOutOfBoundsException(); } } public static ByteBuffer wrap (byte [] array) { return wrap(array, 0 , array.length); } }
成员变量hb,缓冲区数组对象。 只有HeapByteBuffer才会用到,堆外缓冲区的数据不在JVM内存中,而是Buffer类的address变量指向的堆外内存地址。作为HeapByteBuffer 最核心的属性,所有的读写或其他操作,都是围绕此数组进行实现。
成员变量offset,数组第一个读写元素的下坐标。 ByteBuffer的所有子类每次读写数据时,真正读写的位置由position + offset决定,offset只有在使用slice()新建缓冲区时才有可能变成非0值。
成员变量isReadOnly,缓冲区是否为只读状态。 此属性并没有被final修饰,但没有对外提供任何修改的方法,因此也是在构造器的初始化中决定的。
allocateDirect()静态方法,创建一个DirectByteBuffer缓冲区。 创建的堆外缓冲区除了容量,其他的成员变量均使用默认值。
allocate()静态方法,创建一个HeapByteBuffer缓冲区。 创建的堆外缓冲区除了容量,其他的成员变量均使用默认值。
wrap()静态方法,创建一个HeapByteBuffer缓冲区。 将数组从offset位置开始,往后的length个元素作为HeapByteBuffer的缓冲区数组,由于参数有可能造成下坐标越界,因此方法内部会捕获异常。
3.2 抽象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public abstract ByteBuffer slice () ;public abstract ByteBuffer duplicate () ;public abstract ByteBuffer asReadOnlyBuffer () ;public abstract byte get () ;public abstract ByteBuffer put (byte b) ;public abstract byte get (int index) ;public abstract ByteBuffer put (int index, byte b) ;public abstract ByteBuffer compact () ;public abstract boolean isDirect () ;
slice()抽象方法,基于当前缓冲区新建一个缓冲区。 新缓冲区的缓冲数据为此缓冲区的数据内容,也就是说俩个缓冲区指向一个内存地址读写数据,一个缓冲区写入数据,在另一个缓冲区中立刻生效,但俩个缓冲区的position、limit、mark相互独立,并且会重置这几个属性的值,由于堆内堆外对内存地址的维护有差异,需要子类去重写。
duplicate()抽象方法,基于当前缓冲区复制一个缓冲区。 复制出来的缓冲区仍然和原缓冲区共享数据,position、limit、mark、capacity属性完全复制,由于堆内堆外对内存地址的维护有差异,需要子类去重写。
asReadOnlyBuffer()抽象方法,基于当前缓冲区复制一个只读缓冲区。 HeapByteBuffer、DirectByteBuffer等子类都会衍生出一个名称R结尾的子类(例如HeapByteBufferR),R结尾的子类就是父类的只读类,将父类关于写入的方法统统重写抛出异常,因此重写asReadOnlyBuffer()方法,就是创建当前缓冲区对应的R结尾名称类,各种属性全部复制之前的。
get()与put()相关抽象方法,读写缓冲区数据。 ByteBuffer只是确定了读写操作以字节为单位进行,但是具体写到堆内还是堆外无法确定,因此设计为抽象类供子类重写。
compact()抽象方法,清理已读写的缓冲数据。 如果已读写的数据后续不会用到,可以使用此方法可以将position往前位置的数据清理掉,清理完成后position值将重置为0。仍然是因为无法确定清理的是堆内还是堆外数据,因此设计为抽象类供子类重写。
isDirect()抽象方法,是否为直接缓冲区。 true则表示直接缓冲区,数据存储在堆外,false则表示非直接缓冲区,数据存储在JVM堆中,需要子类重写。
3.3 读写相关方法 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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public ByteBuffer get (byte [] dst, int offset, int length) { checkBounds(offset, length, dst.length); if (length > remaining()) throw new BufferUnderflowException(); int end = offset + length; for (int i = offset; i < end; i++) dst[i] = get(); return this ; } public ByteBuffer get (byte [] dst) { return get(dst, 0 , dst.length); } public ByteBuffer put (ByteBuffer src) { if (src == this ) throw new IllegalArgumentException(); if (isReadOnly()) throw new ReadOnlyBufferException(); int n = src.remaining(); if (n > remaining()) throw new BufferOverflowException(); for (int i = 0 ; i < n; i++) put(src.get()); return this ; } public ByteBuffer put (byte [] src, int offset, int length) { checkBounds(offset, length, src.length); if (length > remaining()) throw new BufferOverflowException(); int end = offset + length; for (int i = offset; i < end; i++) this .put(src[i]); return this ; } public final ByteBuffer put (byte [] src) { return put(src, 0 , src.length); }
每种类型的缓冲区,都有批量读写的场景,所有的批量读写都是基于单个元素读写实现,因此ByteBuffer对单个读写方法进行封装,实现对批量读写的支持,具体如何单个读写的,让子类自己重写实现,减少代码的冗余,有点类似于设计模式中的模板模式。
3.4 array相关方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final boolean hasArray () { return (hb != null ) && !isReadOnly; } public final byte [] array() { if (hb == null ) throw new UnsupportedOperationException(); if (isReadOnly) throw new ReadOnlyBufferException(); return hb; } public final int arrayOffset () { if (hb == null ) throw new UnsupportedOperationException(); if (isReadOnly) throw new ReadOnlyBufferException(); return offset; }
如果成员变量hb不等于null,说明缓冲区是基于堆内数组建立的,但仅有这个条件,还不能对外提供数组信息的访问,因为在代码中拿到数组的引用,就意味着可以随意修改内部坐标值,所以还需要保证缓冲区不是只读状态(isReadOnly=false)。
3.5 其他方法 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 44 45 46 public String toString () { StringBuffer sb = new StringBuffer(); sb.append(getClass().getName()); sb.append("[pos=" ); sb.append(position()); sb.append(" lim=" ); sb.append(limit()); sb.append(" cap=" ); sb.append(capacity()); sb.append("]" ); return sb.toString(); } public int hashCode () { int h = 1 ; int p = position(); for (int i = limit() - 1 ; i >= p; i--) h = 31 * h + (int )get(i); return h; } public boolean equals (Object ob) { if (this == ob) return true ; if (!(ob instanceof ByteBuffer)) return false ; ByteBuffer that = (ByteBuffer)ob; if (this .remaining() != that.remaining()) return false ; int p = this .position(); for (int i = this .limit() - 1 , j = that.limit() - 1 ; i >= p; i--, j--) if (!equals(this .get(i), that.get(j))) return false ; return true ; } public int compareTo (ByteBuffer that) { int n = this .position() + Math.min(this .remaining(), that.remaining()); for (int i = this .position(), j = that.position(); i < n; i++, j++) { int cmp = compare(this .get(i), that.get(j)); if (cmp != 0 ) return cmp; } return this .remaining() - that.remaining(); }
3.5 总结 ByteBuffer定义了toString()、hashCode()、equals()、compareTo()等常规方法的具体实现外,最重要的是定义了一系列的读写抽象方法,这主要是因为缓冲区的数据可能在堆内,也可以在堆外,ByteBuffer无法同时实现,只能对其抽象让子类实现。
ByteBuffer在NIO设计中的职责,是将byte相关的读写方法进行抽象,如果是在堆内存储数据,则所有的读写方法都围绕成员变量hb操作,如果是在堆外存储数据,则所有的读写方法都围绕成员变量address对应的内存地址操作。