一、Buffer 简介
- Java NIO 中的 Buffer 用于和 NIO 通道进行交互。
- 数据是从通道读入缓冲区,从缓冲区写入到通道中的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/6df5a836eb7e4ddcbbe5d634340af906.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6aqR5aOr5qKm,size_20,color_FFFFFF,t_70,g_se,x_16)
- 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。
- 这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
- 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。
- 在读取数据时,它是直接读到缓冲区中的。
- 在写入数据时,它也是写入到缓冲区中的。
- 任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
- 而在面向流 I/O 系统中,所有数据都是直接写入 或者 直接将数据读取到 Stream 对象中。
- 在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer。
- 最常用的就是 ByteBuffer。
- 对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应。
- 它们之间的继承关系如下图所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/fb35953224b446859a896fb01033a05e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6aqR5aOr5qKm,size_20,color_FFFFFF,t_70,g_se,x_16)
二、Buffer 用法
1. 使用 Buffer 读写数据
- 写入数据到 Buffer。
- 调用 flip() 方法,反转读写模式。
- 从 Buffer 中读取数据。
- 调用 clear() 或者 compact() 方法,清除缓冲区中数据。
- 当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。
- 一旦要读取数据时,需要通过
flip()
方法,将 Buffer 从 写模式 切换到 读模式。 - 在读模式下,可以读取之前写入到 Buffer 的所有数据。
- 一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
- 有两种方式能清空缓冲区:调用
clear()
或 compact()
方法。
- clear() 方法会清空整个缓冲区。
- compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
2. ByteBuffer 示例
public class ByteBuffer1 {
public static void main(String[] args) throws IOException {
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\ByteBuffer1.txt", "rw");
FileChannel channel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
channel.close();
accessFile.close();
}
}
3. IntBuffer 示例
public class IntBuffer1 {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(8);
int capacity = buffer.capacity();
System.out.printf("容量: %s", capacity).println();
for (int i = 0; i < capacity; i++) {
int j = 2 * (i + 1);
buffer.put(j);
}
buffer.flip();
while (buffer.hasRemaining()) {
int value = buffer.get();
System.out.printf("%s、", value);
}
}
}
三、Buffer 三个核心属性
capacity
。position
。limit
。
- position 和 limit 的含义,要取决于 Buffer 处在读模式还是写模式。
- 不管 Buffer 处在什么模式,capacity 的含义总是一样的。
- capacity、position 和 limit 在读写模式中的说明:
![在这里插入图片描述](https://img-blog.csdnimg.cn/d45892cc9fbb4ed1a8fad20c52473049.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6aqR5aOr5qKm,size_20,color_FFFFFF,t_70,g_se,x_16)
1. capacity
属性
- capacity 容量,作为一个内存块,Buffer 有一个固定的大小值。
- 最多只能往缓冲区里写 capacity 个 byte、long、char 等类型。
- 一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续往里写数据。
2. position
属性
- position 表示写入数据的当前位置,position 的初始值为 0。
- 当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。
- position 最大值为
capacity - 1
(因为 position 的初始值为 0)。
- position 表示读入数据的当前位置,如:
position = 2
表示已读入了 3个byte,或从第 3个byte 开始读取。 - 通过 flip() 方法切换到读模式时,position 会被重置为 0,当 Buffer 从 position 开始读入数据后,position 位置会移到下一个可读入的数据 Buffer 单元。
3. limit
属性
- limit 表示对 Buffer 最多写入多少个数据。
- 写模式下,limit 等于 Buffer 的 capacity。
- limit 表示 Buffer 里有多少可读数据(not null 的数据)。
- 因此能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)。
四、Buffer 类型
- ByteBuffer 字节。
- MappedByteBuffer 映射字节。
- CharBuffer 字符
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
五、向 Buffer 写入数据
1. 分配 Buffer
- 获得一个 Buffer 对象,首先要进行分配容量。
- 每一个 Buffer 类,都有一个 allocate() 方法。
ByteBuffer byteBuffer = ByteBuffer.allocate(48);
CharBuffer charBuffer = CharBuffer.allocate(1024);
2. 向 Buffer 写入数据
- 从 Channel 写入 Buffer。
- 通过 Buffer 的 put() 方法写入 Buffer。
2.1 从 Channel
写入 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
2.2 put()
方法写入 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put("qs".getBytes());
- put() 方法有很多重载,允许不同的方式把数据写入到 Buffer 中。
- 如:写到一个指定的位置。
- 或者把一个字节数组写入 Buffer。
3. flip()
方法
- flip() 方法将 Buffer 从写模式 切换到 读模式。
- 调用 flip() 方法会将 position 设回 0,并将 limit 设置成之前 position 的值。
- 换句话说,position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。
六、从 Buffer 读取数据
- 从 Buffer 读取数据到 Channel。
- get() 方法从 Buffer 读取数据。
1. 从 Buffer 读取数据到 Channel
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.write(buffer);
2. get()
方法从 Buffer 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte b = buffer.get();
- get() 方法有很多重载,允许不同的方式从 Buffer 中读取数据。
- 如:从指定 position 读取。
- 或者从 Buffer 中读取数据到字节数组。
七、Buffer 常用方法
1. rewind()
方法
- Buffer.rewind() 将 position 设回 0,可以重读 Buffer 中的所有数据。
- limit 不变,仍表示能从 Buffer 中读取多少个元素(byte、char 等)。
2. clear()
与 compact()
方法
- 读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。
- 通过 clear() 或 compact() 方法来完成。
- clear() 方法:position 将被设回 0,limit 被设置成 capacity 的值。
- 换句话说,Buffer 被清空了。Buffer 中的数据并未清除,这些标记告诉可以从哪里开始往 Buffer 中写数据。
- compact() 方法:将所有未读的数据拷贝到 Buffer 起始处。
- 然后将 position 设到最后一个未读元素正后面。
- limit 属性依然像 clear() 方法一样,设置成 capacity。
- 现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
- 如果 Buffer 中有一些未读的数据,调用 clear() 方法,数据将被遗忘。
- 意味不再有标记会告诉你哪些数据被读过,哪些还没有。
- 如果 Buffer 中仍有未读的数据,且后续还需要这些数据。
- 此时想要先写些数据,那么使用 compact() 方法。
3. mark()
与 reset()
方法
- Buffer.mark() 方法,可以标记 Buffer 中的一个特定 position。
- Buffer.reset() 方法,可以恢复到这个特定 position。
buffer.mark();
buffer.get();
buffer.reset();
八、Buffer 操作
1. 缓冲区分片
- 在 NIO 中,除了可以分配或者包装一个缓冲区对象。
- 还可以根据现有的缓冲区对象,来创建一个子缓冲区。
- 即在现有缓冲区上切出一片来作为一个新的缓冲区。
- 但现有的缓冲区与创建的子缓冲区,在底层数组层面上是数据共享的。
- 也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。
- 调用
slice()
方法可以创建一个子缓冲区。
public class ByteBuffer2 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
for (int i = 0; i < slice.capacity(); i++) {
byte b = slice.get(i);
b *= 10;
slice.put(i, b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.remaining() > 0) {
System.out.printf("%s、", buffer.get());
}
}
}
2. 只读缓冲区
- 通过调用
asReadOnlyBuffer()
方法,将任何常规缓冲区转换为只读缓冲区。 - 这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。
- 如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。
public class ByteBuffer3 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.get(i);
b *= 10;
buffer.put(i, b);
}
readOnlyBuffer.position(0);
readOnlyBuffer.limit(buffer.capacity());
while (readOnlyBuffer.remaining() > 0) {
System.out.printf("%s、", readOnlyBuffer.get());
}
}
}
- 如果修改只读缓冲区的内容,会报
ReadOnlyBufferException
异常。
- 只读缓冲区对保护数据很有用。
- 将缓冲区传递给某个方法时,无法知道这个方法是否会修改缓冲区中的数据。
- 创建一个只读的缓冲区,可以保证该缓冲区不会被修改。
- 只能将常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。
3. 直接缓冲区
- 直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区。
- JDK 的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。
- 也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中,或者从一个中间缓冲区中拷贝数据。
- 要分配直接缓冲区,需要调用
allocateDirect()
方法,而不是 allocate()
方法,使用方式与普通缓冲区并无区别。
public class ByteBuffer4 {
public static void main(String[] args) throws IOException {
String inFile = "E:\\TEMP\\nio\\ByteBuffer41.txt";
String outFile = "E:\\TEMP\\nio\\ByteBuffer42.txt";
FileInputStream fileInputStream = new FileInputStream(inFile);
FileOutputStream fileOutputStream = new FileOutputStream(outFile);
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int read = inChannel.read(buffer);
if (read == -1) {
break;
}
buffer.flip();
outChannel.write(buffer);
}
outChannel.close();
inChannel.close();
fileOutputStream.close();
fileInputStream.close();
}
}
4. 内存映射文件 I/O
- 内存映射文件 I/O,是一种读和写文件数据的方法。
- 可以比常规的基于流,或者基于通道的 I/O 快的多。
- 内存映射文件 I/O,是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。
- 这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射)到内存中。
- 内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。
- Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
- 尽管创建内存映射文件相当简单,但是向它写入可能是危险的。
- 仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
public class ByteBuffer5 {
private static final int start = 0;
private static final int size = 1024;
public static void main(String[] args) throws IOException {
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\ByteBuffer5.txt", "rw");
FileChannel channel = accessFile.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, start, size);
buffer.put(0, (byte) 97);
buffer.put(1023, (byte) 122);
channel.close();
accessFile.close();
}
}