【源码解析】Java NIO 包中的 HeapByteBuffer

server/2025/1/13 12:20:57/

文章目录


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 修饰的,这些构造器的权限修饰是 defaultprotected,所以这里并不是创建 HeapByteBuffer 的地方,底层的 wrapallocate 才是创建 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 的文章中我们已经介绍过了,创建视图有两种方法:sliceduplicate,前者是创建一个视图,这个视图里面的数据是原生 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 + 1idx 这个方法是 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 进行判断

  1. HeapByteBuffer因为这个 HeapByteBuffer 是 JVM 管理的,背后有 hb 数组作为底层支撑,所以可以直接拷贝。
  2. 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 了。





如有错误,欢迎指出!!!!


http://www.ppmy.cn/server/158001.html

相关文章

springCloud特色知识记录(基于黑马教程2024年)

目录 Nacos 简介 Nacos 的特点 Nacos 的使用步骤可以查看黑马教程文档&#xff1a;‍‌​‌​&#xfeff;⁠​⁠​​​​​&#xfeff;‬​​​​‍‌&#xfeff;‬⁠​&#xfeff;​‬​​​​‍​&#xfeff;⁠​&#xfeff;​​⁠​​‬​⁠&#xfeff;​​day03-微…

计算机网络(三)——局域网和广域网

一、局域网 特点&#xff1a;覆盖较小的地理范围&#xff1b;具有较低的时延和误码率&#xff1b;使用双绞线、同轴电缆、光纤传输&#xff0c;传输效率高&#xff1b;局域网内各节点之间采用以帧为单位的数据传输&#xff1b;支持单播、广播和多播&#xff08;单播指点对点通信…

基于 Python 和 OpenCV 的人脸识别上课考勤管理系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

Kafka 主题管理

主题作为消息的归类&#xff0c;分区则是对消息的二次归类。分区可以有一至多个副本&#xff0c;每个副本对应一个日志文件。 分区的划分不仅为Kafka提供了可伸缩性、水平扩展的功能&#xff0c;还通过多副本机制来为Kafka提供数据冗余以提高可靠性。 图 主题、分区、副本和日…

在 Java 中使用 GET 和 POST 请求

在 Java 中&#xff0c;我们通常使用 HttpURLConnection 或第三方库&#xff08;如 Apache HttpClient 或 OkHttp&#xff09;来发送 GET 和 POST 请求。本文将通过示例讲解如何实现这两种 HTTP 请求。 1. 使用 HttpURLConnection 实现 GET 和 POST 请求 HttpURLConnection 是…

26个开源Agent开发框架调研总结(2)

根据Markets & Markets的预测&#xff0c;到2030年&#xff0c;AI Agent的市场规模将从2024年的50亿美元激增至470亿美元&#xff0c;年均复合增长率为44.8%。 Gartner预计到2028年&#xff0c;至少15%的日常工作决策将由AI Agent自主完成&#xff0c;AI Agent在企业应用中…

02-51单片机数码管与矩阵键盘

一、数码管模块 1.数码管介绍 如图所示为一个数码管的结构图&#xff1a; 说明&#xff1a; 数码管上下各有五个引脚&#xff0c;其中上下中间的两个引脚是联通的&#xff0c;一般为数码管的公共端&#xff0c;分为共阴极或共阳极&#xff1b;其它八个引脚分别对应八个二极管…

【数据结构-堆】2233. K 次增加后的最大乘积

给你一个非负整数数组 nums 和一个整数 k 。每次操作&#xff0c;你可以选择 nums 中 任一 元素并将它 增加 1 。 请你返回 至多 k 次操作后&#xff0c;能得到的 nums的 最大乘积 。由于答案可能很大&#xff0c;请你将答案对 109 7 取余后返回。 示例 1&#xff1a; 输入&…