NIO 三大组件之Buffer

Buffer 基本信息

Java NIO 中的 Buffer 是用于和 NIO 中的 Channel 进行交互的,数据从 Channel 中读入 Buffer 中,从 Buffer 写入 Channel中。

Buffer 是一个固定数量的数据容器,其作用就是一个存储器,或者分段运输区,在这里数据可以被存储,且可以用于检索。Buffer是一个包在一个对象内的基本数据元素数组,在NIO中所有缓冲区类型都继承自抽象类Buffer,最常用的是ByteBuffer。对于Java 中的基本类型,都有一个与之对应的

Buffer 具有四个属性:

  • Capacity(容量)
    • 缓冲区能够容纳数据元素的最大数量,这个容量值是在缓冲区创建时设定的,并且永远不能改变。
  • Limit(上线)
    • 缓冲区的第一不能被读或写的元素,或者说时缓冲区现存的元素计数。
  • Position(位置)
    • position 表示写入数据时到当前位置,初始值为 0,position 最大值 capacity - 1。
  • Mark(标记)
    • 一个备忘位置,调用mark()来设定 mark = position。调用 reset()设定position = mark。标记设定前时未定义的。

这四个属性之间遵循的关系是:

0 <= mark <= position <= limiti <= capacity

来看看 Buffer 类的设计:

public abstract class Buffer {

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    
    public final int capacity() {
    return capacity;
    }

    public final int position() {
        return position;
    }

    public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }
    
    public final int limit() {
        return limit;
    }

    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }
 
    public final Buffer mark() {
        mark = position;
        return this;
    }

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
    
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    
    public final int remaining() {
        return limit - position;
    }
    
    public final boolean hasRemaining() {
        return position < limit;
    }
    
    public abstract boolean isReadOnly();    
    // .......
}

在上面代码中,值得注意的一点是,像clear() 这样的函数,它的返回值应该是void类型,但是这里却是 Buffer引用,这其实是一个联动的类设计方法。

还有 isReadOnly()函数。在NIO 中所有的缓冲区都可读,但并非所有的都可写,每个具体的缓冲区的都可通过isReadOnly()来标识其是否允许该缓存区的内容被修改。

Buffer 基本用法

1、Buffer 读写数据的基本流程

  1. 写入数据到 Buffer
  2. 调用 flip() 方法
  3. 从 Buffer 中读取数据
  4. 调用 clear() 方法或者 compact() 方法

当向 buffer 写入数据时,buffer 会记录写入了多少数据,一旦要读取数据时,需通过调用 flip() 方法将 Buffer 从写模式切换到读模式。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。这里清空缓冲区有两种方式:调用 clear() 方法或者 compact() 方法。这里 clear() 会清空整个缓冲区,而 compact() 则只是清空已经读过的缓冲区数据。任何未被读过的数据都会被移到缓冲区的起始位置,新写入的数据将会放到未读数据的后面。

2、Buffer 中的几个方法

  • rewind 方法
    • rewind 方法将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保持不变,表示仍然可以从buffer中读取多少个元素。
  • clear 和 compact 方法
    • 当Buffer 中的数据读完的时候,需要让Buffer 准备笑下次继续写入,可以通过clear() 或compact() 方法来完成。
    • 如果调用clear() 方法,position将会被设回0,limit 被设置成Capacity的值。其实这时buffer中的数据并未被清空只是这些标识告诉可以从什么位置开始写数据。如果Buffer中有未读的数据,此时调用了clear() 方法,这些数据将不会再被读到,因为没有任何标记能告诉你哪些数据时未读的。
    • compact() 方法会将未读的数据拷贝到Buffer的起始处,然后将position设置为最后一个未读元素正后面,limit属性仍然和clear() 方法一样,设成 Capacity,此时写数据,但并不会覆盖数据。
  • mark 和 reset 方法
    • 通过调用 Buffer.mark()方法,可以标记 Buffer 中的一个特定 position,之后可以通过调用 Buffer.reset()方法恢复到这个 position。
  • flip 方法
    • flip 方法将 Buffer 从写模式切换到读模式,调用 flip 方法会将 position 设置成 0,并将limit设置成 position 之前的值。也就是说position 现在等同于标记读的位置,limit 表示之前写入了多少byte、char等(以及现在能读取多少)。

关于Buffer的相关设计与意义

get() 和 put() 方法在Buffer抽象类中时没有设计的,每一个 Buffer 实现类都有这 两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一 的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵 从。

Buffer中的缓冲区操作

1、分片缓冲区

NIO 中除了分配或包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,就是在现有的缓冲区中切分出一个新的缓冲区,但是现有的缓冲区与所创建的自缓冲区在数据层面上时共享的,也就是说子缓冲区相当于现有缓冲区的一个视图窗口,在代码中调用slice()方法,便可创建一个子缓冲区。示例如下:

private static void sliceBuffer() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    for (int i = 0; i < byteBuffer.capacity(); i++) {
        byteBuffer.put((byte)i);
    }

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

    // 改变缓冲区内容
    for (int i = 0; i < slice.capacity(); i++) {
        byte b = slice.get();
        b *= 10;
        slice.put(i, b);
    }
    byteBuffer.position(0);
    byteBuffer.limit(byteBuffer.capacity());
    while (byteBuffer.remaining() > 0) {
        System.out.println(byteBuffer.get());
    }
}

2、只读缓冲区

只读缓冲区顾名思义就是它的数据是可以读取,不可以对其进行写操作,可以通过调用缓冲 asReadOnlyBuffer()方法,将任何常规缓冲区转换成只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。

如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException 异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是

否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。示例如下:

private static void readOnlyBuffer() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    for (int i = 0; i < byteBuffer.capacity(); i++) {
        byteBuffer.put((byte)i);
    }
    ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();

    for (int i = 0; i < byteBuffer.capacity(); i++) {
        byte b = byteBuffer.get(i);
        b *= 10;
        byteBuffer.put(i, b);
    }

    readOnlyBuffer.position(0);
    readOnlyBuffer.limit(byteBuffer.capacity());

    while (readOnlyBuffer.remaining() > 0) {
        System.out.println(readOnlyBuffer.get());
    }
}

3、 直接缓冲区

直接缓冲区时为了加快 I/O 的速度,使用一种特殊的方式为其分配内存的缓冲区,jdk的文档中描述:给定一个直接的字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作,就是说它会在每一次调用底层系统的本地I/O之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区调用 allocateDirect()方法即可(不是 allocate()方法),使用方法与普通缓冲区并无区别。示例如下:

private static void directBuffer() throws Exception {
    String inFilePath = "/Users/alisha/IdeaProjects/JavaWork/src/main/resources/nio/01.text";
    FileInputStream inputStream = new FileInputStream(inFilePath);
    FileChannel inputStreamChannel = inputStream.getChannel();

    String outFilePath = "/Users/alisha/IdeaProjects/JavaWork/src/main/resources/nio/04.text";
    FileOutputStream outputStream = new FileOutputStream(outFilePath);
    FileChannel outputStreamChannel = outputStream.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

    while (true) {
        byteBuffer.clear();
        int read = inputStreamChannel.read(byteBuffer);
        if (read == -1) {
            break;
        }
        byteBuffer.flip();
        outputStreamChannel.write(byteBuffer);
    }

}

4、内存映射文件 I/O

内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O通过文件中的数据数据出现为内存数组来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。示例如下:

private static void inMemoryBufferIO() throws Exception {
    RandomAccessFile raf = new RandomAccessFile("/Users/alisha/IdeaProjects/JavaWork/src/main/resources/nio/01.tex",
            "rw");
    FileChannel fc = raf.getChannel();
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
            start, size);
    mbb.put(0, (byte) 97);
    mbb.put(1023, (byte) 122);
    raf.close();
}

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/121777610