Buffer是入门Java NIO的基础,本文希望通过一些形象的比喻来解释一下缓冲区的概念,帮助读者快速理解和记忆。
本文灵感来自于Bilibili博主v若水若水分享的尚硅谷Java视频_NIO视频教程,有需要看视频学习的朋友可以在Bilibili上搜索Java NIO找到相关视频学习。
本文中的英文摘自java.nio中相关类的注释。
Buffer是什么?
A container for data of a specific primitive type.
Buffer是特定基本类型数据的容器。
- 我愿意把Buffer比作 火车。
Buffer能装什么?
- Buffer能装int,float,char,double,short,long,byte
- Buffer不能装boolean
整数 | 浮点数 | 字符 | 字节数 |
---|---|---|---|
byte(8位) | 1字节 | ||
short(16位) | char(16位) | 2字节 | |
int(32位) | float(32位) | 3字节 | |
long(64位) | double(64位) | 4字节 |
Buffer的重要属性及对应的获取方法
the essential properties of a buffer are its capacity, limit, and position
- capacity,容量。火车有 n 节车厢
- position,位置。这里需要再假想一辆小车,行驶在火车旁边的站台上。position表示当前小车停留在第 n 节车厢前。
- limit,限制。火车的前 n 节车厢。
属性 | 属性获取方法 |
---|---|
capacity | public final int capacity() |
position | public final int position() |
limit | public final int limit() |
接下来的讨论都会紧紧围绕这三个属性
安排一列火车-allocate
以最常用的ByteBuffer为例:
/**
* 分配一个新的字节缓冲区。
* 新缓冲区的位置position将为零,其限制limit等于
* 容量capacity,其标记mark将是不确定的,并且其每个元素将是
* 初始化为零。 它将有一个{@link #array backing array},
*,其{@link #arrayOffset array offset}将为零。
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
- 可以看到allocate方法会使用HeapXXXBuffer类来实例化。
- allocate将分配一个指定容量的缓冲区。存储哪类基本数据类型,这取决于你使用哪类Buffer。
测试代码:
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}
控制台输出:
1024
1024
0
通俗解释:
安排一辆1024节车厢的火车。容量为1024节车厢,允许装货1024节车厢,现在小车停在第0节车厢上。
学过数组的都知道,计算机中,一个长度为1024的数组下标是从0到1023
装货-put
装单个"货"
测试用例1
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put((byte) 'a');
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}
控制台输出
1024
1024
1
通俗解释
把 a 装到火车的第 0 号车厢,成功装上之后,小车向前移了一个节车厢,停在了第 1 号车厢。
这里实际上装进ByteBuffer的是由 a 转化得到的 8 位二进制数(01100001)。
8 位二进制正好就是1个字节。
a 是怎么得到二进制数的?这个问题可以查看ASCII码表
测试用例2
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(11, (byte) 'a');
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}
控制台输出
1024
1024
0
结论
put(byte)
会使得position发生改变,但是put(index, byte)
不会使position发生改变
装一组"货"
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}
控制台输出
1024
1024
4
通俗解释
把[a,b,c,d]依次装到火车的第 0, 1, 2, 3 号车厢,现在车厢旁边的小车停在 4 号车厢前。
这里实际上装进ByteBuffer的是由 a,b,c,d 转化得到的二进制数。
结论
无论是put一个字节(byte)还是一个字节数组(byte[]),put成功后position都会向后移动。
装货时超载-BufferOverflowException
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.limit(2);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
buffer.put("abc".getBytes());
}
控制台输出
1024
2
0
Exception in thread "main" java.nio.BufferOverflowException
at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:189)
at java.nio.ByteBuffer.put(ByteBuffer.java:859)
at nio.Main.main(Main.java:14)
通俗解释
虽然这列火车有1024节车厢,但是规定 只能装在前2节车厢内 。因此当你同时派来了 3 辆小车,分别装着 a,b,c,从 0 号车厢排列到 2 号车厢,当你想同时装货到火车上的时候,你被列车管理员拒绝了,理由是你超载了!因此,现在*** a,b,c全都没装上火车 ***。
代码
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);
}
结论:全装or全部不给装
这里是先校验,后写入数据。要么全都正常写入,要么一个也不会写入。
可重复读-get
读单个
测试用例1
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put((byte) 'a');
System.out.println(buffer.get(0));
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.position());
}
控制台输出
97
1
97
1
结论
get(index)
不会改变position的位置,且可以重复读,读取结果总是一样的。
测试用例2
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position());
byte[] bytes = new byte[2];
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position());
}
控制台输出
4
[0, 0]
6
通俗解释
在装货 [a, b, c, d] 之后,小车的位置已经移动到4号车厢前(第5节车厢),此时是不能从当前车厢读取到有效的货物信息的。另外,小车因为成功读取了2个车厢的货物信息(虽然结果为0),小车的位置已经发生了改变,目前小车移动到了6号车厢前(第7节车厢)。
如何让小车回到起始位置呢?
flip
/**
* 在执行一系列 put i>操作之后,调用此方法以准备一系列相对
* 的 get i>操作。
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
rewind
/**
* 假设已正确设置了限制,请在执行一系列 get i>操作之前调用此方法。
*/
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear
/**
* 在使用一系列 put i>操作填充此缓冲区之前,请调用此方法。
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
结论
flip()
和rewind()
相同点,都是在准备调用get
之前调用。
flip()
和rewind()
不同点,rewind
需要用户事先确保limit正确。
clear()
用于在准备重新调用put
之前调用。
再探get
测试用例
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position());
byte[] bytes = new byte[2];
buffer.flip();
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position());
buffer.rewind();
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position());
}
控制台输出
4
[97, 98]
2
[97, 98]
2
结论
在调用get(byte[])
之前事先调用flip()
则此次可以正常读取数据到数组中。调用rewind()
则可以“倒带”(我愿意理解成把小车开回原点),又可以再次读取。
小车越界-BufferUnderflowException
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position());
byte[] bytes = new byte[6];
buffer.flip();
buffer.get(bytes);
}
控制台输出
Exception in thread "main" java.nio.BufferUnderflowException
at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:151)
at java.nio.ByteBuffer.get(ByteBuffer.java:715)
at nio.Main.main(Main.java:15)
通俗解释
在调用flip()
之后,列车管理员规定小车最多只能移到到4号车厢前面,因为只有0号到3号车厢装有货物可以读到货物信息,即只有4节车厢中是含有货物信息的,而小车却想获取6节车厢中货物信息,结果被列车管理员告知你越界了!
小车越界可不同于数组越界-IndexOutOfBoundsException
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
byte[] bytes = new byte[6];
buffer.flip();
buffer.get(bytes, 0, 7);
}
控制台输出
Exception in thread "main" java.lang.IndexOutOfBoundsException
at java.nio.Buffer.checkBounds(Buffer.java:567)
at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:149)
at nio.Main.main(Main.java:14)
结论
数组越界是表示对于数组的操作就有问题,与Buffer中的属性关系不大。比如数组长度只有6,却想读取7个字节到数组中去,直接导致了数组越界。
标记和重置-mark&reset
Marking and resetting
A buffer's mark is the index to which its position will be reset when the {@link #reset reset} method is invoked. The mark is not always defined, but when it is defined it is never negative and is never greater than the position. If the mark is defined then it is discarded when the position or the limit is adjusted to a value smaller than the mark. If the mark is not defined then invoking the {@link #reset reset} method causes an {@link InvalidMarkException} to be thrown.
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.mark();
buffer.put("ab".getBytes());
System.out.println(buffer.position());
buffer.reset();
System.out.println(buffer.position());
byte[] bytes = new byte[2];
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
}
控制台输出
2
0
[97, 98]
通俗解释
在一开始,小车就在0号车厢做了个标记,然后在完成对0号车厢和1号车厢的装货后,一个重置,小车位置由2号车厢回到了事先标记好的0号车厢,然后就可以开始读了。!!注意:这不是一种安全的读方式,这里仅仅是示例作用。
拓展知识1:Buffer支持链式调用
测试代码
public static void main(String[] args) {
byte[] bytes = new byte[4];
byte result = ((ByteBuffer)ByteBuffer.allocate(1024)
.put("abcd".getBytes())
.flip())
.get();
System.out.println(result);
}
控制台输出
97
拓展知识2:clear不会清除buffer中原来存储的内容
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position());
buffer.clear();
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.get(1));
System.out.println(buffer.get(2));
System.out.println(buffer.get(3));
}
控制台输出
4
0
97
98
99
100
拓展知识3:putChar('a')和put((byte)'a')有别
测试代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putChar('a');
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.get(1));
buffer.put((byte)'a');
System.out.println(buffer.position());
System.out.println(buffer.get(2));
System.out.println(buffer.get(3));
}
控制台输出
2
0
97
3
97
0
结论
putChar('a')
会放入2个字节到缓冲区,如果实际编码只有一个字节,那就会用0来表示前一个字节。而put((byte)'a')
只会放入1个字节到缓冲区。你可以通过buffer.position()
观察这一点。