【源码解析】Java NIO 包中的 ByteBuffer
文章目录
- 1. 前言
- 2. ByteBuffer 概述
- 3. 属性
- 4. 构造器
- 5. 方法
- 5.1 allocate 分配 Buffer
- 5.2 wrap 映射数组
- 5.3 slice 获取子 ByteBuffer
- 5.4 duplicate 复刻 ByteBuffer
- 5.5 asReadOnlyBuffer 创建只读的 ByteBuffer
- 5.6 get 方法获取字节
- 5.7 put 方法往 ByteBuffer 里面加入字节
- 5.8 array 和 arrayOffset
- 5.9 compact 切换写模式
- 5.10 其他方法
- 6. 大端序和小端序
- 7. 小结
1. 前言
上一篇文章我们介绍了最底层的 Buffer,那么这篇文章就要介绍下 Buffer 的
比较核心的一个实现类 ByteBuffer,上一篇文章的地址如下:
- 【源码解析】Java NIO 包中的 Buffer
2. ByteBuffer 概述
上面就是 Buffer 的继承结构,当然 Buffer 的子类肯定不会只有这么点,比如下面的图:
只不过上面图中就给了几个基本 Buffer 的实现类,可以看到几个重要的实现类 MappedByteBuffer
、HeapByteBuffer
、DirectByteBuffer
都是 ByteBuffer
的子类,这几个实现类也是我们要介绍的重点,只不过这篇文章我们先介绍 ByteBuffer。
ByteBuffer 是字节缓存,也是最常见的 Buffer,无论是缓存映射还是文件映射都有 ByteBuffer 的身影。上一篇文章中我们也说过,没有 ByteBuffer 之前,对于字节流一个一个处理都是比较繁琐的,有了 ByteBuffer 之后就可以一次处理一大批的数据,性能更加高效。
下面我们就来看下 ByteBuffer 这个类的庐山真面目。
3. 属性
final byte[] hb;
hb 是 ByteBuffer 中存储字节数据的数组,专门用于 HeapByteBuffer 中数据的存放,如果是直接内存 Buffer,那这个数组就不会存储数据。
final int offset;
offset 是 ByteBuffer 中第一个元素的起始位置,也可以说是存储元素的数组的第一个起始下标,一般都是从 0 开始。
boolean isReadOnly;
这个属性就是表示是否是只读的,如果一个 Buffer 是只读的,那么就不能修改,只能读取。
ByteBuffer 的属性比较简单,是因为指针都封装到底层 Buffer 了,所以到 ByteBuffer 这一层属性就没那么多了。
4. 构造器
ByteBuffer(int mark, int pos, int lim, int cap,byte[] hb, int offset){super(mark, pos, lim, cap);this.hb = hb;this.offset = offset;}ByteBuffer(int mark, int pos, int lim, int cap) {this(mark, pos, lim, cap, null, 0);}
无论是哪个构造器,都绕不过 mark
、pos
、limit
、cap
这几个指标,就是 Buffer 里面的这四个参数。
那么这两个构造器不同的是参数上第一个构造器需要设置数组 hb
和 offset
。这其实就很明显了,调用第一个方法的其实就是创建 HeapByteBuffer
,第二个方法则是 DirectByteBuffer
和 MappedByteBuffer
会调用。
5. 方法
因为 ByteBuffer 是抽象类,所以里面的所有方法几乎都留给了子类去实现,所以我这里就简单介绍下这个 ByteBuffer 里面的一些抽象方法以及这些方法的具体用途。
5.1 allocate 分配 Buffer
这个方法用于分配 HeapByteBuffer 和 DirectByteBuffer,其实就是直接 new 出来。
// 分配 HeapByteBuffer
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);
}// 分配 DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}
5.2 wrap 映射数组
这个方法用于将传入数组中的一部分数据或者是数组的全部数据映射成一个 HeapByteBuffer(转换),为什么不是 DirectByteBuffer 和 MappedByteBuffer 呢?当然是另外两个是直接内存了不受 JVM 管理了,所以传入的数组肯定不能映射成堆外的 Buffer。
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 wrap(byte[] array) {return wrap(array, 0, array.length);
}
这里其实就是简单的创建一个 HeapByteBuffer。
5.3 slice 获取子 ByteBuffer
public abstract ByteBuffer slice();
slice() 方法用于创建一个新的 ByteBuffer 对象,其内容是当前 ByteBuffer 对象内容的一个共享子序列,啥意思呢?新创建的 ByteBuffer 对象和原始 ByteBuffer 对象之间的内容是共享的,但它们的位置(position)、限制(limit)和标记(mark)值是独立的。换句话说这两个 ByteBuffer 对象的底层数组是一样的,只是 Buffer 的几个标记不一样。
既然新建的 ByteBuffer 和原来的 ByteBuffer 共享一个内存空间,那也就意味新的 ByteBuffer 由下面的性质。
-
共享内存
- 对当前 ByteBuffer 对象内容的任何修改将反映在新创建的 ByteBuffer 对象中,反之亦然
-
状态独立
- 新创建的 ByteBuffer 对象的 position 将被设置为 0
- 新创建的 ByteBuffer 对象的 capacity 和 limit将等于当前 ByteBuffer 对象剩余的字节数
- 新创建的 ByteBuffer 对象的 mark 会被重置为 -1
-
属性继承
- 当前 ByteBuffer 是什么类型(HeapByteBuffer 和 DirectByteBuffer),创建出来的 ByteBuffer 就是什么类型
- 如果当前 ByteBuffer 对象是只读的(read-only),则新创建的 ByteBuffer 对象也将是只读的
可以看到上面图中,slice 获取的 ByteBuffer 视图中 position 重新指向了 0 的位置,而 limit = 6,那么问题来了,既然 slice 之后获取的 ByteBuffer 重新设置了这几个指标,那么如何进行访问呢?
不知道大家还记得 ByteBuffer 中的 offset
吗?这个 offset 上面我们说过了就是 position 的偏移量,所以 slice 创建出来的子 ByteBuffer 可以通过 offset + position
来算出,比如在上面例子中 offset = 4
。
那下面我们还可以看个例子:
public static void byteBufferTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);ByteBuffer slice = byteBuffer.slice();System.out.println(slice.arrayOffset()); // 4
}
在上面这个例子中,创建出来的 slice.offset = 4
,那么下面我们接着往里面写入数据。
slice.put((byte) 5);slice.put((byte) 6);slice.put((byte) 7);slice.put((byte) 8);System.out.println(Arrays.toString(byteBuffer.array()));
最后来看下输出的结果:
可以看到最后的输出结果就表明了对创建出来的 slice 添加数据也会影响到原来的 ByteBuffer,同时 slice 是在原来 ByteBuffer 的 position 后面继续操作,也能看到上面输出的 offset 就是调用 slice 方法时候的 position 值。
5.4 duplicate 复刻 ByteBuffer
public abstract ByteBuffer duplicate();
如果说上面的 slice 是从原来的 ByteBuffer 截取一段(共享地址)下来,这个方法就是完整复刻整个 ByteBuffer。也就是说它们的 offset,mark,position,limit,capacity
变量的值全部是一样的。
下面来看个例子,其实主要是看里面的 offset 是多少,可以看到 duplicate
就是完全复制一个 ByteBuffer,在里面可以
public static void byteBufferTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);ByteBuffer duplicate = byteBuffer.duplicate();System.out.println(duplicate.arrayOffset()); // 0duplicate.put((byte) 5);duplicate.put((byte) 6);duplicate.put((byte) 7);duplicate.put((byte) 8);System.out.println(Arrays.toString(byteBuffer.array()));
}
5.5 asReadOnlyBuffer 创建只读的 ByteBuffer
public abstract ByteBuffer asReadOnlyBuffer();
这个方法就是创建一个只读的 ByteBuffer,而如果写入数据的话会抛出 ReadOnlyBufferException 异常。
5.6 get 方法获取字节
get 方法就是 ByteBuffer 里面获取字节的方法,可以通过这个方法来获取 ByteBuffer 里面 position 位置的值,当然这个方法也有很多重载方法,其中我们也可以传入一个 byte[] 数组,然后把 ByteBuffer 里面的值传到数组里面。
public abstract byte get();// 获取指定下标下面的值
public abstract byte get(int index)// 从 offset 开始获取 length 个字节放到数组 dst 中
public ByteBuffer get(byte[] dst, int offset, int length) {// 检查指定 index 的边界,确保不能越界checkBounds(offset, length, dst.length);// 检查 ByteBuffer 是否有足够的转移字节if (length > remaining())throw new BufferUnderflowException();int end = offset + length;// 从 offset 开始获取 length 个字节转移到数组 dst 中for (int i = offset; i < end; i++)dst[i] = get();return this;
}// 将 ByteBuffer 全部放到 dst 中
public ByteBuffer get(byte[] dst) {return get(dst, 0, dst.length);
}
5.7 put 方法往 ByteBuffer 里面加入字节
// 往 position 位置设置字节 b,同时设置 position = position + 1
public abstract ByteBuffer put(byte b);// 往 index 设置字节 b,设置之后 position 不会改变
public abstract ByteBuffer put(int index, byte b);// 把 src 的所有字节放到当前的 ByteBuffer 里面
public ByteBuffer put(ByteBuffer src) {if (src == this)throw new IllegalArgumentException();if (isReadOnly())throw new ReadOnlyBufferException();int n = src.remaining();if (n > remaining())throw new BufferOverflowException();for (int i = 0; i < n; i++)put(src.get());return this;
}// 从 offset 开始,将 length 个字节设置到当前 ByteBuffer 中
public ByteBuffer put(byte[] src, int offset, int length) {// 检查指定 index 的边界,确保不能越界checkBounds(offset, length, src.length);// 检查 ByteBuffer 是否能够容纳得下if (length > remaining())throw new BufferOverflowException();int end = offset + length;// 从字节数组得 offset 处,转移 length 个字节到 ByteBuffer 中for (int i = offset; i < end; i++)this.put(src[i]);return this;
}// 传入一个字节数组,设置到当前 ByteBuffer 中
public final ByteBuffer put(byte[] src) {return put(src, 0, src.length);
}
上面几个方法都是 put 方法,就是往当前 ByteBuffer 里面设置数据的,不过要注意下,当调用 put(int index, byte b)
来设置字节,position 不会被修改。
5.8 array 和 arrayOffset
public final byte[] array() {if (hb == null)throw new UnsupportedOperationException();if (isReadOnly)throw new ReadOnlyBufferException();return hb;
}public final int arrayOffset() {if (hb == null)throw new UnsupportedOperationException();if (isReadOnly)throw new ReadOnlyBufferException();return offset;
}
这两个方法就是获取 Buffer 底层的数组和数组的第一个元素的偏移量,这个偏移量其实就是 Buffer 第一个元素的下标。
但是如果 Buffer 是只读的,那么就没办法获取,会抛出异常 ReadOnlyBufferException
。
5.9 compact 切换写模式
ByteBuffer 切换写模式之前已经介绍过一个方法了,就是 clear()
,但是这里面有个问题,就是 clear 这个方法是直接把 position 设置为 0,也就是从头开始写入,如果在调用 clear
之前已经把数据读完了那当然没问题,但是如果还遗留一些数据,这样新写入的数据会把原来剩下那些没读取完的覆盖掉,比如看下面的例子:
public static void clearTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);// 切换读模式byteBuffer.flip();System.out.println(byteBuffer.get()); // 1System.out.println(byteBuffer.get()); // 2System.out.println(byteBuffer.get()); // 3// 切换写模式byteBuffer.clear();byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 5);System.out.println(Arrays.toString(byteBuffer.array()));// [1, 2, 3, 5, 0, 0, 0, 0, 0, 0]
}
在上面例子中,首先我们往 ByteBuffer 里面设置了 1,2,3,4
,然后切换到读模式,接着读取前三个数据,也就是 1,2,3
,接着我们调用 clear()
方法切换到写模式,然后往里面写入 1,2,3,5
,这时候我们就发现原来里面的 4 被 5 覆盖了。但是如果换成 compact 方法就不一样了。
public static void compactTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);byteBuffer.flip();System.out.println(byteBuffer.get()); // 1System.out.println(byteBuffer.get()); // 2System.out.println(byteBuffer.get()); // 3byteBuffer.compact();byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 5);System.out.println(Arrays.toString(byteBuffer.array()));// [1, 2, 3, 5, 0, 0, 0, 0, 0, 0]
}
调用 duplicate
之后,会把没有读取到的 4 放到 ByteBuffer 的前面,然后继续往后写入,所以 compact 这个方法切换写模式并不会覆盖没有读取完的数据。
当切换写模式之后,会先把 [position, limit)
挪到下标 0
开始的位置,然后设置 position = limit - position
,就是设置 position = 4 - 3 = 1
,后面会从 1
开始继续写入。
5.10 其他方法
上面就是比较常用的方法,下面剩下那些就是不常用的,可以看下下面的截图。
6. 大端序和小端序
ByteBuffer 里面还有一个重要的概念就是大端序和小端序,下面就来介绍下这个概念,我们先来随便看一个数字的二进制,比如数字 1234
,二进制为:00000000 00000000 00000100 11010010
。
数字存储到计算机中有两种方式,一种是内存的低地址向高地址存储,一种是高地址向低地址存储,也就是下面的两种方式。
- 大端序(Big-Endian):数据的高位字节存储在内存的低地址,低位字节存储在内存的高地址。
- 小端序(Little-Endian):数据的低位字节存储在内存的低地址,高位字节存储在内存的高地址。
比如下面图中的存储:
在大端序中,数字 1234
的存储从 0 开始,高位存储到 0 的位置,依次类推,小端序则反过来。
在 JVM 中,堆的地址从下往上是从低到高的,对于大端序,读取数据的时候就是从高位开始读取,对于小端序则是从低位开始读取。
在 ByteBuffer 中则是通过一个变量 bigEndian
来表示这个 ByteBuffer 存储数据是大端序还是小端序。
boolean bigEndian = true;
同时也给了一个方法返回时大端序还是小端序。
public final ByteOrder order() {return bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
}
当然除了上面两个方法,ByteBuffer 也提供了方法去设置大端序还是小端序。
public final ByteBuffer order(ByteOrder bo) {bigEndian = (bo == ByteOrder.BIG_ENDIAN);nativeByteOrder =(bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));return this;
}
当然这里 ByteBuffer 只是指定大端序还是小端序,对于不同的字节序,从里面读取数据的时候的操作就不同,因为这里只是 ByteBuffer,如果是 IntBuffer 这种一次性读取四个字节的,就需要根据不同的字节序来判断要如何组成一个 int 了,我举个例子,还是下面这张图。
- 如果是大端序,这时候从下标 0 - 4 存储的就是 int 高到底的字节,那么组合的方法就是:
(arr[0] << 24) | (arr[1] << 16) || (arr[2] << 8) || arr[3]
- 如果是小端序,这时候从下标 0 - 4 存储的就是 int 低到高的字节,那么组合的方法就是:
(arr[0]) | (arr[1] << 8) || (arr[2] << 16) || (arr[3] << 24)
那么这里就简单介绍下这两个概念,因为具体的实现是在子类中去完成的,这篇文章就先不介绍了。
7. 小结
这篇文章就先介绍到这了,这个 ByteBuffer 是比较重要的一个类,为什么要介绍这个类呢?因为后面我将会逐步开始学习并写一些 RocketMQ 的文章,但我们都知道像这种 RocketMQ 的中间件的内存存储都离不开文件映射,其中就离不开 MappedByteBuffer,所以要慢慢从最底层的 Buffer 开始学习,这样才知道当往文件里面写入数据的时候,到底是怎么写入的。
如有错误,欢迎指出!!!