文章目录
- 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
HeapByteBuffer_11">2. HeapByteBuffer
HeapByteBuffer 是 ByteBuffer 的子实现类,受 JVM 管理,内部使用一个 byte 数组存储数据。
下面废话不多说,来看下里面的属性和方法。
HeapByteBuffer__18">3. HeapByteBuffer 的创建
首先先来看下 HeapByteBuffer 的构造器。
java">HeapByteBuffer(int cap, int lim) { // package-privatesuper(-1, 0, lim, cap, new byte[cap], 0);/*hb = new byte[cap];offset = 0;*/
}HeapByteBuffer(byte[] buf, int off, int len) { // package-privatesuper(-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 构造器如下:
java">ByteBuffer(int mark, int pos, int lim, int cap, // package-privatebyte[] 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
提供的。
java">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 变量的值全部是一样的。
java">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 + offsetreturn 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
的子类。
java">public ByteBuffer asReadOnlyBuffer() {return new HeapByteBufferR(hb,this.markValue(),this.position(),this.limit(),this.capacity(),offset);}
5. get 获取元素
get 方法就是从 position 位置来获取元素。
java">// 从 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);// 重新设置 positionposition(position() + length);return this;
}
上面三个方法,我们先看前两个,首先是 get()
,这个方法会获取 position 位置下标,然后从数组中获取字节,这里面的 nextGetIndex
就是获取 position
,并且将 position + 1
,idx
这个方法是 offset + position
。
java">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 操作。
java">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 设置字节。
java">/*** 向 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 的位置。
java">/*** 向下标 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 中。
java">/*** 将 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);// 更新 positionposition(position() + length);return this;
}
这个方法的逻辑和上面的 get 方法的类似,所以不多说了,最后 put 方法还可以传入一个 ByteBuffer,将 ByteBuffer 中的数据拷贝到当前 ByteBuffer 中。
java">public ByteBuffer put(ByteBuffer src) {// 如果是 HeapByteBufferif (src instanceof HeapByteBuffer) {// 不能自己拷贝自己if (src == this)throw new IllegalArgumentException();HeapByteBuffer sb = (HeapByteBuffer)src;// 要拷贝的 src 的 positionint spos = sb.position();// 当前 ByteBuffer 的 positionint 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 的 positionsb.position(spos + n);position(pos + n);} else if (src.isDirect()) {// 直接内存 ByteBufferint n = src.remaining();if (n > remaining())throw new BufferOverflowException();// 调用 DirectByteBuffer 的 get 方法将 pisition 开始的字节设置到当前 ByteBuffer 的字节数组中src.get(hb, ix(position()), n);// 调整 positionposition(position() + n);} else {// 不是 HeapByteBuffer 也不是直接 ByteBuffer,这时候调用父类通用方法去添加了super.put(src);}return this;
}
这里面的逻辑其实不难,主要是对两个类型的 ByteBuffer 进行判断
- HeapByteBuffer:因为这个 HeapByteBuffer 是 JVM 管理的,背后有 hb 数组作为底层支撑,所以可以直接拷贝。
- DirectByteBuffer:这个方法底层是操作直接内存,也就是直接通过 offset 来获取的,不受 JVM 管理,所以需要调用 DirectByteBuffer.get 方法来获取。
7. compact 切换写模式
这个方法上一篇文章中已经介绍过 compact 了,这里就不多说,直接一句话总结就是:将没有处理的数据挪到 ByteBuffer 前面,接着继续往后写入
。
java">/*** 切换写模式,介绍看这里* {@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 = remainingposition(remaining());// 设置 limit = capacitylimit(capacity());// 重置 markdiscardMark();return this;
}
8. 大端模式和小端模式
上一篇文章 ByteBuffer 的解析中已经说过这两个模式了,那么在 HeapByteBuffer 中可以通过 Bits.getInt 来获取一个 int 元素,因为我们知道 ByteBuffer 里面存储的最小单位是 Byte,4 个 Byte 构成一个 int 数字,所以我们就以 getInt 这个方法来看下如何处理的,当然除了 getInt 之外,还有 getLong … 这些方法,所以看 getInt 的逻辑。
java">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 方法,在再继续看里面的核心逻辑:
java">static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
}
如果是大端序,那么会走 getIntB
方法,如果是小端序,那么会走 getIntL
方法。
java">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。
java">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 存储就是右边的。
那么大端序已经看完了,下面再来看下小端序的。
java">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 中的存储就行了。
HeapByteBufferR_405">9. HeapByteBufferR
上面的方法我们就介绍到这了,剩下的方法很多都是重复的,比如看了 getInt 的逻辑之后,就可以大概推出 getChar 这些的逻辑,put 也差不多。
所以最后来介绍下 HeapByteBufferR,这个 Buffer 是 HeapByteBuffer 的子类,是一个只读的 HeapByteBuffer,也就是不可写入。
java">class HeapByteBufferRextends HeapByteBuffer
{...
}
这个只读类里面的方法和 HeapByteBuffer 是差不多的,既然这个类是只读类,那么最终里面的一些方法比如切换写模式,这时候就会抛出异常。
java">public ByteBuffer compact() {throw new ReadOnlyBufferException();
}void _put(int i, byte b) {throw new ReadOnlyBufferException();
}...
这里就是简单介绍下这个类的情况,不需要详细解析,因为上面也说过里面的方法和 HeapByteBuffer 是差不多的。
10. 小结
好了,到这里 HeapByteBuffer 就解析完成了,下一篇文章就到 DirectByteBuffer 了。
如有错误,欢迎指出!!!!