【源码解析】Java NIO 包中的 HeapByteBuffer
文章目录
- 1. 前言
- 2. HeapByteBuffer
- 3. HeapByteBuffer 的创建
- 4. 创建视图
- 5. get 获取元素
- 6. put 设置元素
- 7. compact 切换写模式
- 8. 大端模式和小端模式
- 9. HeapByteBufferR
- 10. 小结
1. 前言
上一篇文章我们介绍了 ByteBuffer 里面的一些抽象方法和概念,这篇文章开始就要介绍 ByteBuffer 的实现类了,本篇文章先从 HeapByteBuffer 开始。
- 【源码解析】Java NIO 包中的 Buffer
- 【源码解析】【源码解析】Java NIO 包中的 ByteBuffer
2. HeapByteBuffer
HeapByteBuffer 是 ByteBuffer 的子实现类,受 JVM 管理,内部使用一个 byte 数组存储数据。
下面废话不多说,来看下里面的属性和方法。
3. HeapByteBuffer 的创建
首先先来看下 HeapByteBuffer 的构造器。
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
HeapByteBuffer(byte[] buf, int off, int len) { // package-private
super(-1, off, off + len, buf.length, buf, 0);
/*
hb = buf;
offset = 0;
*/
}
protected HeapByteBuffer(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{
super(mark, pos, lim, cap, buf, off);
/*
hb = buf;
offset = off;
*/
}
这些方法调用的底层 ByteBuffer 构造器如下:
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
构造器的调用逻辑其实不难,我们主要看下第二个 super(-1, off, off + len, buf.length, buf, 0)
,这个构造器的意思是传入 buf 数组,并且以 off 为 数组起点,len 为数组元素个数来映射一个 ByteBuffer,其实就是通过数组来创建一个 ByteBuffer。
这里是 HeapByteBuffer 的构造器,但是我们知道不同包下如果需要调用构造器是需要 public
修饰的,这些构造器的权限修饰是 default
、protected
,所以这里并不是创建 HeapByteBuffer 的地方,底层的 wrap
和 allocate
才是创建 HeapByteBuffer 的方法,这两个方法是顶层 ByteBuffer
提供的。
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 allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
4. 创建视图
在 ByteBuffer 的文章中我们已经介绍过了,创建视图有两种方法:slice
、duplicate
,前者是创建一个视图,这个视图里面的数据是原生 ByteBuffer 的当前位置 position 开始一直到 limit 之间的数据。
而 duplicate
就是完完全全复刻原生 ByteBuffer,它们的 offset,mark,position,limit,capacity 变量的值全部是一样的。
public ByteBuffer slice() {
int pos = this.position();
int lim = this.limit();
int rem = (pos <= lim ? lim - pos : 0);
// 这里面的 pos + offset,是因为创建出来的 ByteBuffer
// 视图其实操作的还是原来的 ByteBuffer,由于创建出来的 ByteBuffer
// position 从 0 开始,所以需要加上偏移量
// 这个偏移量就等于原生视图的 position + offset
return new HeapByteBuffer(hb,
-1,
0,
rem,
rem,
pos + offset);
}
public ByteBuffer duplicate() {
return new HeapByteBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
不过关于 slice
还是得多说一句,因为创建出来的 ByteBuffer
是从原生视图的 position -> limit
这段的数据,并且创建出来的 ByteBuffer
的 position 从 0 开始了,所以如果要访问到 子 ByteBuffer
的数据就必须得加上 offset,这个 offset 就是原生 ByteBuffer 的 position。
如果我们从子 ByteBuffer 视角看,position = 0 表示第一个元素,但是从原生 ByteBuffer 视角看,子 ByteBuffer 的 position + offsete
才是指向第一个元素,也就是下标 4 的位置。
当然了,我们知道 ByteBuffer 也有只读的,那么创建出来的视图也可以是只读的,不过这时候创建出来的就是 HeapByteBufferR
了,这个类是 HeapByteBuffer
的子类。
public ByteBuffer asReadOnlyBuffer() {
return new HeapByteBufferR(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
5. get 获取元素
get 方法就是从 position 位置来获取元素。
// 从 position 获取一个字节,并且将 position + 1
public byte get() {
return hb[ix(nextGetIndex())];
}
// 指定下标获取字节,并不会设置 position + 1
public byte get(int i) {
return hb[ix(checkIndex(i))];
}
/**
* 将 HeapByteBuffer 中的字节转移到指定的字节数组中
* @param dst 目标字节数组
* @param offset 拷贝到目标字节数组的哪个位置
* @param length 拷贝的长度
* @return
*/
public ByteBuffer get(byte[] dst, int offset, int length) {
// 检查长度
checkBounds(offset, length, dst.length);
if (length > remaining())
// 当前 ByteBuffer 是否有 length 个字节的数据
throw new BufferUnderflowException();
// 从 hb 中指定位置开始,拷贝 length 个字节到 dst 的 offset 下标中
System.arraycopy(hb, ix(position()), dst, offset, length);
// 重新设置 position
position(position() + length);
return this;
}
上面三个方法,我们先看前两个,首先是 get()
,这个方法会获取 position 位置下标,然后从数组中获取字节,这里面的 nextGetIndex
就是获取 position
,并且将 position + 1
,idx
这个方法是 offset + position
。
final int nextGetIndex() {
int p = position;
if (p >= limit)
throw new BufferUnderflowException();
position = p + 1;
return p;
}
/**
* 确定要访问的 index,为了兼容视图的操作,就需要加上 offset,原生 Buffer 中的 offset = 0
* @param i
* @return
*/
protected int ix(int i) {
return i + offset;
}
加上 offset 是因为这里的 ByteBuffer 有可能是一个视图 Buffer,所以需要加上 offset
来获取 position
的位置。
第二个方法 get(int i)
里面通过 checkIndex
来检查下标 i 是否在符合的范围内,如果不在就抛出异常,注意这个方法没有对 position 操作。
final int checkIndex(int i) {
if ((i < 0) || (i >= limit))
throw new IndexOutOfBoundsException();
return i;
}
再来看最后一个 get 方法 get(byte[] dst, int offset, int length)
,这个方法就是传入一个 dst 数组,然后从 ByteBuffer 的 offset 开始将 length 个字节加入 dst 数组中。在这个方法里面会先检查长度,如果 ByteBuffer 剩下的字节数不够 length 个字节了,就抛出异常。否则就使用 System.arraycopy
将数组中的数据拷贝到数组中。
System.arraycopy(hb, ix(position()), dst, offset, length)
这个方法就是将 hb 中 从 offset + position 开始的 length 个字节拷贝到 dst 数组的 offset 下标(开始)。
之所以要用 System.arraycopy
,是因为这个方法在数据量大的时候,性能是要比直接使用 for 循环遍历加入要高的。
最后拷贝之后重新设置下 position 的位置为 position + length
。
6. put 设置元素
既然有 get 获取元素,同理也有 put 设置字节。
/**
* 向 position 的位置写入一个字节
* @param x
* @return
*/
public ByteBuffer put(byte x) {
// 往 position 写入一个字节,然后把 position 向后移动一个位置
hb[ix(nextPutIndex())] = x;
return this;
}
final int nextPutIndex() {
int p = position;
if (p >= limit)
throw new BufferOverflowException();
position = p + 1;
return p;
}
这个方法就是从 position 开始设置 x,同时让 position + 1。接下来的 put 方法就是设置字节 x 到下标 i 的位置。
/**
* 向下标 i 的位置写入一个 x
* @param i
* @param x
* @return
*/
public ByteBuffer put(int i, byte x) {
// 向 index 写入字节 x,注意写入之后 position 不会移动
hb[ix(checkIndex(i))] = x;
return this;
}
当然了,下面的 put 方法还可以传入一个 src,然后从 offset 开始将 length 个字节的数据拷贝到 ByteBuffer 中。
/**
* 将 src 中 offset 开始长度为 length 的字节拷贝到 Buffer 中
* @param src
* @param offset
* @param length
* @return
*/
public ByteBuffer put(byte[] src, int offset, int length) {
// 边界检查
checkBounds(offset, length, src.length);
// 长度检查
if (length > remaining())
throw new BufferOverflowException();
// 开始拷贝
System.arraycopy(src, offset, hb, ix(position()), length);
// 更新 position
position(position() + length);
return this;
}
这个方法的逻辑和上面的 get 方法的类似,所以不多说了,最后 put 方法还可以传入一个 ByteBuffer,将 ByteBuffer 中的数据拷贝到当前 ByteBuffer 中。
public ByteBuffer put(ByteBuffer src) {
// 如果是 HeapByteBuffer
if (src instanceof HeapByteBuffer) {
// 不能自己拷贝自己
if (src == this)
throw new IllegalArgumentException();
HeapByteBuffer sb = (HeapByteBuffer)src;
// 要拷贝的 src 的 position
int spos = sb.position();
// 当前 ByteBuffer 的 position
int pos = position();
// 要拷贝的 src 还剩下多少字节可以拷贝
int n = sb.remaining();
// 如果要拷贝的 src 还剩下的字节数比当前 ByteBuffer 剩余位置要大
// 说明当前 ByteBuffer 没有那么多地方接收 src 的数据
if (n > remaining())
throw new BufferOverflowException();
// 这里就是正常拷贝了
System.arraycopy(sb.hb, sb.ix(spos),
hb, ix(pos), n);
// 设置 src 的 position 和当前 ByteBuffer 的 position
sb.position(spos + n);
position(pos + n);
} else if (src.isDirect()) {
// 直接内存 ByteBuffer
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
// 调用 DirectByteBuffer 的 get 方法将 pisition 开始的字节设置到当前 ByteBuffer 的字节数组中
src.get(hb, ix(position()), n);
// 调整 position
position(position() + n);
} else {
// 不是 HeapByteBuffer 也不是直接 ByteBuffer,这时候调用父类通用方法去添加了
super.put(src);
}
return this;
}
这里面的逻辑其实不难,主要是对两个类型的 ByteBuffer 进行判断
- HeapByteBuffer:因为这个 HeapByteBuffer 是 JVM 管理的,背后有 hb 数组作为底层支撑,所以可以直接拷贝。
- DirectByteBuffer:这个方法底层是操作直接内存,也就是直接通过 offset 来获取的,不受 JVM 管理,所以需要调用 DirectByteBuffer.get 方法来获取。
7. compact 切换写模式
这个方法上一篇文章中已经介绍过 compact 了,这里就不多说,直接一句话总结就是:将没有处理的数据挪到 ByteBuffer 前面,接着继续往后写入
。
/**
* 切换写模式,介绍看这里
* {@link Buffer#clear()}
* @return
*/
public ByteBuffer compact() {
// remaining:limit - position
// 从原来数组的 position 开始,把 remaining 长度的数据拷贝到下标 0 的位置
// 也就是把 [position, limit) 未读的数据拷贝到前面
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
// 设置 position = remaining
position(remaining());
// 设置 limit = capacity
limit(capacity());
// 重置 mark
discardMark();
return this;
}
8. 大端模式和小端模式
上一篇文章 ByteBuffer 的解析中已经说过这两个模式了,那么在 HeapByteBuffer 中可以通过 Bits.getInt 来获取一个 int 元素,因为我们知道 ByteBuffer 里面存储的最小单位是 Byte,4 个 Byte 构成一个 int 数字,所以我们就以 getInt 这个方法来看下如何处理的,当然除了 getInt 之外,还有 getLong … 这些方法,所以看 getInt 的逻辑。
public int getInt() {
return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
}
public int getInt(int i) {
return Bits.getInt(this, ix(checkIndex(i, 4)), bigEndian);
}
上面两个方法就是 getInt 方法,在再继续看里面的核心逻辑:
static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
}
如果是大端序,那么会走 getIntB
方法,如果是小端序,那么会走 getIntL
方法。
static int getIntB(ByteBuffer bb, int bi) {
return makeInt(bb._get(bi ),
bb._get(bi + 1),
bb._get(bi + 2),
bb._get(bi + 3));
}
上面的方法中,bb 是 ByteBuffer,而 bi 是起始下标,这个 _get
方法就是在 ByteBuffer 里面通过数组下标直接索引,那么最终的逻辑需要看 makeInt。
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
return (((b3 ) << 24) |
((b2 & 0xff) << 16) |
((b1 & 0xff) << 8) |
((b0 & 0xff) ));
}
上面方法中调用 makeInt 传入的就是从低地址到高地址的 4 个 bit,传入到 makeInt 中,所以这里 makeInt 就是低地址在高位,高地址在低位。
比如 1234
,二进制为:00000000 00000000 00000100 11010010
。大端序的 ByteBuffer 存储就是上面左边的,小端序的 ByteBuffer 存储就是右边的。
那么大端序已经看完了,下面再来看下小端序的。
static int getIntL(ByteBuffer bb, int bi) {
return makeInt(bb._get(bi + 3),
bb._get(bi + 2),
bb._get(bi + 1),
bb._get(bi ));
}
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
return (((b3 ) << 24) |
((b2 & 0xff) << 16) |
((b1 & 0xff) << 8) |
((b0 & 0xff) ));
}
这里面的代码逻辑就是和大端序反过来了,上面小端序和大端序就介绍到这了,其实里面的逻辑不难,主要就是搞懂字节在 ByteBuffer 中的存储就行了。
9. HeapByteBufferR
上面的方法我们就介绍到这了,剩下的方法很多都是重复的,比如看了 getInt 的逻辑之后,就可以大概推出 getChar 这些的逻辑,put 也差不多。
所以最后来介绍下 HeapByteBufferR,这个 Buffer 是 HeapByteBuffer 的子类,是一个只读的 HeapByteBuffer,也就是不可写入。
class HeapByteBufferR
extends HeapByteBuffer
{
...
}
这个只读类里面的方法和 HeapByteBuffer 是差不多的,既然这个类是只读类,那么最终里面的一些方法比如切换写模式,这时候就会抛出异常。
public ByteBuffer compact() {
throw new ReadOnlyBufferException();
}
void _put(int i, byte b) {
throw new ReadOnlyBufferException();
}
...
这里就是简单介绍下这个类的情况,不需要详细解析,因为上面也说过里面的方法和 HeapByteBuffer 是差不多的。
10. 小结
好了,到这里 HeapByteBuffer 就解析完成了,下一篇文章就到 DirectByteBuffer 了。
如有错误,欢迎指出!!!!