当前位置: 首页 > article >正文

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

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





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


http://www.kler.cn/a/502003.html

相关文章:

  • CF 371A.K-Periodic Array(Java实现)
  • 基于Qt的OFD阅读器开发原理与实践
  • OpenStack 网络服务的插件架构
  • uniapp中rpx和upx的区别
  • 高级java每日一道面试题-2025年01月08日-微服务篇-负载平衡的意义什么 ?
  • SQL美化器优化
  • C++—9、如何在Microsoft Visual Studio中调试C++
  • 性能测试工具Jmeter负载模拟
  • TPS61022 PFM的机制以及TPS61xxx转换器的PFM与PWM之间的负载阈值
  • 使用jquery的$.ajax提交带有FormData的post请求报错TypeError: Illegal invocation
  • 详解C#调用系统文件窗口:打开文件、保存文件和选择文件夹
  • 【Javaweb02】初探CSS:网页样式设计的入门指南
  • 《机器学习》——TF-IDF(关键词提取)
  • 如何用python实现语音输出
  • 积鼎科技:国产自主核反应堆流体仿真解决方案
  • 深度解析Linux中的调试器gdb/cgdb的使用
  • MySQL 16 章——变量、流程控制和游标
  • 青少年编程与数学 02-006 前端开发框架VUE 23课题、UI框架
  • huggingface/bert/transformer的模型默认下载路径以及自定义路径
  • 《使用 YOLOV8 和 KerasCV 进行高效目标检测》
  • 【机器学习案列】学生抑郁可视化及预测分析
  • 迁移WSL中的ext4.vhdx文件
  • MATLAB与Python中的快速傅里叶变换频谱分析
  • R9000P发热严重
  • 聚铭网络当选中关村华安关键信息基础设施安全保护联盟理事单位
  • 2Hive表类型