三、Java NIO Buffer


一、Buffer 简介

  • Java NIO 中的 Buffer 用于和 NIO 通道进行交互。
  1. 数据是从通道读入缓冲区,从缓冲区写入到通道中的。
    在这里插入图片描述
  • 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存
  1. 这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
  2. 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的
  3. 在读取数据时,它是直接读到缓冲区中的。
  4. 在写入数据时,它也是写入到缓冲区中的。
  5. 任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
  6. 而在面向流 I/O 系统中,所有数据都是直接写入 或者 直接将数据读取到 Stream 对象中。

  • 在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer。
  1. 最常用的就是 ByteBuffer。
  2. 对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应。
  • 它们之间的继承关系如下图所示:
    在这里插入图片描述

二、Buffer 用法


1. 使用 Buffer 读写数据

  • 一般遵循以下四个步骤:
  1. 写入数据到 Buffer。
  2. 调用 flip() 方法,反转读写模式。
  3. 从 Buffer 中读取数据。
  4. 调用 clear() 或者 compact() 方法,清除缓冲区中数据。

  1. 当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。
  2. 一旦要读取数据时,需要通过 flip() 方法,将 Buffer 从 写模式 切换到 读模式
  3. 在读模式下,可以读取之前写入到 Buffer 的所有数据。
  4. 一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
  5. 有两种方式能清空缓冲区:调用 clear()compact() 方法。
  1. clear() 方法会清空整个缓冲区。
  2. compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

2. ByteBuffer 示例

/**
 * @author: wy
 * describe: ByteBuffer 示例
 */
public class ByteBuffer1 {

    public static void main(String[] args) throws IOException {
        // 一、打开 FileChannel
        RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\ByteBuffer1.txt", "rw");
        FileChannel channel = accessFile.getChannel();

        // 1. 创建容量为`1024`个字节的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 2. 读入缓冲区
        int bytesRead = channel.read(buffer);
        while (bytesRead != -1) {
            // 3. 反转读写模式
            buffer.flip();
            while (buffer.hasRemaining()) {
                // 4. 一次读取`1`个字节
                System.out.println((char) buffer.get());
            }
            // 5. 清空缓整个缓冲区,让它可以再次被写入
            buffer.clear();
            // 6. 如果返回 -1,表示读到了文件末尾
            bytesRead = channel.read(buffer);
        }

        // 二、关闭 Channel
        channel.close();
        accessFile.close();
    }
}

3. IntBuffer 示例

/**
 * @author: wy
 * describe: IntBuffer 示例
 */
public class IntBuffer1 {

    public static void main(String[] args) {
        /*
        1. 分配新的 int 类型缓冲区,参数为缓冲区的容量大小。
        2. 新缓冲区的当前位置为零,其界限(限制位置)为其容量大小。
        3. 它将具有一个底层实现数组,其数组偏移量将为零。
         */
        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);
            // 4. 将计算的整数,写入缓冲区的当前位置(当前位置递增)
            buffer.put(j);
        }

        // 5. 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为 0
        buffer.flip();

        // 6. 查看在 当前位置 和 限制位置 之间是否有元素
        while (buffer.hasRemaining()) {
            // 7. 读取缓冲区当前位置的整数,然后当前位置递增
            int value = buffer.get();
            System.out.printf("%s、", value);
        }
        /*
        容量: 8
        2、4、6、8、10、12、14、16、
         */
    }
}

三、Buffer 三个核心属性

  • Buffer 的三个属性:
  1. capacity
  2. position
  3. limit
  • position 和 limit 的含义,要取决于 Buffer 处在读模式还是写模式。
  • 不管 Buffer 处在什么模式,capacity 的含义总是一样的。
  • capacity、position 和 limit 在读写模式中的说明:
    在这里插入图片描述

1. capacity 属性

  • capacity 容量,作为一个内存块,Buffer 有一个固定的大小值。
  1. 最多只能往缓冲区里写 capacity 个 byte、long、char 等类型。
  2. 一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续往里写数据。

2. position 属性

  • 写数据到 Buffer 中
  1. position 表示写入数据的当前位置,position 的初始值为 0。
  2. 当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。
  3. position 最大值为 capacity - 1(因为 position 的初始值为 0)。
  • 从 Buffer 中读取数据
  1. position 表示读入数据的当前位置,如:position = 2 表示已读入了 3个byte,或从第 3个byte 开始读取。
  2. 通过 flip() 方法切换到读模式时,position 会被重置为 0,当 Buffer 从 position 开始读入数据后,position 位置会移到下一个可读入的数据 Buffer 单元。

3. limit 属性

  • 写数据
  1. limit 表示对 Buffer 最多写入多少个数据。
  2. 写模式下,limit 等于 Buffer 的 capacity。
  • 读数据
  1. limit 表示 Buffer 里有多少可读数据(not null 的数据)。
  2. 因此能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)。

四、Buffer 类型

  • Java NIO Buffer 类型:
  1. ByteBuffer 字节。
  2. MappedByteBuffer 映射字节。
  3. CharBuffer 字符
  4. ShortBuffer
  5. IntBuffer
  6. LongBuffer
  7. FloatBuffer
  8. DoubleBuffer

五、向 Buffer 写入数据


1. 分配 Buffer

  • 获得一个 Buffer 对象,首先要进行分配容量。
  1. 每一个 Buffer 类,都有一个 allocate() 方法。
// 1. 分配一个`48`字节`capacity`的`ByteBuffer`
ByteBuffer byteBuffer = ByteBuffer.allocate(48);

// 2. 分配一个`1024`字符`capacity`的`CharBuffer`
CharBuffer charBuffer = CharBuffer.allocate(1024);

2. 向 Buffer 写入数据

  1. 从 Channel 写入 Buffer。
  2. 通过 Buffer 的 put() 方法写入 Buffer。
2.1 从 Channel 写入 Buffer
// 1. 创建容量为`1024`个字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2. 读入缓冲区
int bytesRead = channel.read(buffer);
2.2 put() 方法写入 Buffer
// 1. 创建容量为`1024`个字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
// 2. 加入缓冲区
buffer.put("qs".getBytes());
  • put() 方法有很多重载,允许不同的方式把数据写入到 Buffer 中。
  1. 如:写到一个指定的位置。
  2. 或者把一个字节数组写入 Buffer。

3. flip() 方法

  • flip() 方法将 Buffer 从写模式 切换到 读模式。
  1. 调用 flip() 方法会将 position 设回 0,并将 limit 设置成之前 position 的值。
  2. 换句话说,position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。

六、从 Buffer 读取数据

  1. 从 Buffer 读取数据到 Channel。
  2. get() 方法从 Buffer 读取数据。

1. 从 Buffer 读取数据到 Channel

// 1. 创建容量为`1024`个字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2. 从缓冲区读入通道
int bytesRead = channel.write(buffer);

2. get() 方法从 Buffer 读取数据

// 1. 创建容量为`1024`个字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2. 从缓冲区中获取数据
byte b = buffer.get();
  • get() 方法有很多重载,允许不同的方式从 Buffer 中读取数据。
  1. 如:从指定 position 读取。
  2. 或者从 Buffer 中读取数据到字节数组。

七、Buffer 常用方法


1. rewind() 方法

  • Buffer.rewind() 将 position 设回 0,可以重读 Buffer 中的所有数据。
  1. limit 不变,仍表示能从 Buffer 中读取多少个元素(byte、char 等)。

2. clear()compact() 方法

  • 读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。
  1. 通过 clear() 或 compact() 方法来完成。
  2. clear() 方法:position 将被设回 0,limit 被设置成 capacity 的值。
  3. 换句话说,Buffer 被清空了。Buffer 中的数据并未清除,这些标记告诉可以从哪里开始往 Buffer 中写数据。
  4. compact() 方法:将所有未读的数据拷贝到 Buffer 起始处。
  5. 然后将 position 设到最后一个未读元素正后面。
  6. limit 属性依然像 clear() 方法一样,设置成 capacity。
  7. 现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
  • 如果 Buffer 中有一些未读的数据,调用 clear() 方法,数据将被遗忘
  1. 意味不再有标记会告诉你哪些数据被读过,哪些还没有。
  • 如果 Buffer 中仍有未读的数据,且后续还需要这些数据。
  1. 此时想要先写些数据,那么使用 compact() 方法。

3. mark()reset() 方法

  • Buffer.mark() 方法,可以标记 Buffer 中的一个特定 position。
  • Buffer.reset() 方法,可以恢复到这个特定 position。
buffer.mark();
// 多次调用 buffer.get() 例如在解析过程中。
buffer.get();
// 将位置设置回标记。
buffer.reset();

八、Buffer 操作


1. 缓冲区分片

  • 在 NIO 中,除了可以分配或者包装一个缓冲区对象。
  • 还可以根据现有的缓冲区对象,来创建一个子缓冲区。
  1. 即在现有缓冲区上切出一片来作为一个新的缓冲区。
  2. 但现有的缓冲区与创建的子缓冲区,在底层数组层面上是数据共享的。
  3. 也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。
  4. 调用 slice() 方法可以创建一个子缓冲区。
/**
 * @author: wy
 * describe: 缓冲区分片
 */
public class ByteBuffer2 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 1. 缓冲区中的数据为 0-9
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }

        // 2. 创建子缓冲区
        buffer.position(3);
        buffer.limit(7);
        ByteBuffer slice = buffer.slice();

        // 3. 改变子缓冲区内容
        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());
        }
        // 0、1、2、30、40、50、60、7、8、9、
    }
}

2. 只读缓冲区

  • 只读缓冲区,可以读取,但是不能写入数据。
  1. 通过调用 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区。
  2. 这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。
  3. 如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。
/**
 * @author: wy
 * describe: 只读缓冲区
 */
public class ByteBuffer3 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 1. 缓冲区中的数据 0-9
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }

        // 2. 创建只读缓冲区
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

        // 3. 改变原缓冲区内容
        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());

        // 4. 只读缓冲区的内容也随之改变
        while (readOnlyBuffer.remaining() > 0) {
            System.out.printf("%s、", readOnlyBuffer.get());
        }
        // 0、10、20、30、40、50、60、70、80、90、
    }
}
  • 如果修改只读缓冲区的内容,会报 ReadOnlyBufferException 异常。
  1. 只读缓冲区对保护数据很有用。
  2. 将缓冲区传递给某个方法时,无法知道这个方法是否会修改缓冲区中的数据。
  3. 创建一个只读的缓冲区,可以保证该缓冲区不会被修改。
  4. 只能将常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

3. 直接缓冲区

  • 直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区。
  1. JDK 的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。
  2. 也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中,或者从一个中间缓冲区中拷贝数据。
  3. 要分配直接缓冲区,需要调用 allocateDirect() 方法,而不是 allocate() 方法,使用方式与普通缓冲区并无区别。
/**
 * @author: wy
 * describe: 直接缓冲区,拷贝文件示例
 */
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();

        // 1. 创建直接缓冲区,使用 allocateDirect() 而不是 allocate()
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

        while (true) {
            buffer.clear();
            int read = inChannel.read(buffer);
            if (read == -1) {
                break;
            }
            buffer.flip();
            outChannel.write(buffer);
        }

        // 2. 关闭 Channel
        outChannel.close();
        inChannel.close();
        fileOutputStream.close();
        fileInputStream.close();
    }
}

4. 内存映射文件 I/O

  • 内存映射文件 I/O,是一种读和写文件数据的方法。
  1. 可以比常规的基于流,或者基于通道的 I/O 快的多。
  2. 内存映射文件 I/O,是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。
  3. 这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射)到内存中。
  4. 内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。
  5. Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
  6. 尽管创建内存映射文件相当简单,但是向它写入可能是危险的。
  7. 仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
/**
 * @author: wy
 * describe: 内存映射文件 I/O
 */
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();

        // 1. 内存映射文件 IO
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, start, size);
        buffer.put(0, (byte) 97);
        buffer.put(1023, (byte) 122);

        // 2. 关闭 Channel
        channel.close();
        accessFile.close();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值