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;

// 如果下个读写位置已超出limit,强制设置为limit
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

// 获取下次读取的位置值,如果超出边界值则抛出异常,方法返回后position递增1
final int nextGetIndex() {
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}

// 获取下次读取的位置值,跳过nb个元素,如果超出边界值则抛出异常,position递增nb
final int nextGetIndex(int nb) {
if (limit - position < nb)
throw new BufferUnderflowException();
int p = position;
position += nb;
return p;
}

// 获取下次写入的位置值,如果超出边界值则抛出异常,方法返回后position递增1
final int nextPutIndex() {
if (position >= limit)
throw new BufferOverflowException();
return position++;
}

// 获取下次写入的位置值,跳过nb个元素,如果超出边界值则抛出异常,position递增nb
final int nextPutIndex(int nb) {
if (limit - position < nb)
throw new BufferOverflowException();
int p = position;
position += nb;
return p;
}

// 参数i为读写的位置值,如果小于0或者超出边界,则超出正常范围,抛出异常
final int checkIndex(int i) {
if ((i < 0) || (i >= limit))
throw new IndexOutOfBoundsException();
return i;
}

// 参数i为读写的位置值,nb为跳过的元素数量,如果小于0或者超出边界,则超出正常范围,抛出异常
final int checkIndex(int i, int nb) {
if ((i < 0) || (nb > limit - i))
throw new IndexOutOfBoundsException();
return i;
}

// 如果将长度为size的数组,从off坐标开始,往后len个元素写入缓冲区,验证是否符合范围要求
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) {

// 调用Buffer的静态方法校验坐标
checkBounds(offset, length, dst.length);

// 如果剩余未读的元素数量,少于需要读取的元素数量,抛出异常
if (length > remaining())
throw new BufferUnderflowException();

// 从缓冲区的offset坐标开始,读取length个字节到dst数组
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}

public ByteBuffer get(byte[] dst) {
// 从缓冲区的0坐标开始,读取dst.length个字节到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) {

// 调用Buffer的静态方法校验坐标
checkBounds(offset, length, src.length);

// 即将写入的字节数量,不能超出当前缓冲区可以写入的数量
if (length > remaining())
throw new BufferOverflowException();

// 从src数组的offset坐标开始,往后length个元素写入当前缓冲区
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}

public final ByteBuffer put(byte[] src) {
// 将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对应的内存地址操作。

评论