【NIO】Buffer:基本原理及高级使用

缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。

读/写 ==> Buffer。即用户的直接操作都是面向缓冲区。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。

PS:在面向流I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

在NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示:

在这里插入图片描述
下面来看一个使用 Buffer 的简单示例,其中包含了最基本的 API:

public class IntBufferDemo { 
    public static void main(String[] args) {
        // 1.分配新的 int 缓冲区,参数为缓冲区容量(capacity) 
        // 新缓冲区的当前位置将为零,其界限(limit)将为其容量。
        // 它将具有一个底层实现数组,其数组偏移量(position)将为零。 
        IntBuffer buffer = IntBuffer.allocate(8);
        
	    for (int i = 0; i < buffer.capacity(); ++i) { 
            int j = 2 * (i + 1); 
            // 2.将给定整数写入此缓冲区的当前位置,当前位置递增 
            // 注:因为底层是数组,所以还可以 put(index,value)
            buffer.put(j); 
        } 
        
        // 3.重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为 0 
        buffer.flip(); 
        
        // 查看在当前位置和限制位置之间是否有元素 
        // 注:这里也可以 buffer.remaining() > 0
        while (buffer.hasRemaining()) {
            // 4.读取此缓冲区当前位置的整数,然后当前位置递增
            // 注:因为底层是数组,所以还可以 get(index)
            int j = buffer.get(); 
            System.out.print(j + " ");
        }
    }
}

在这里插入图片描述

1.Buffer 基本原理

在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如果我们使用 get()方法从缓冲区获取数据或者使用 put()方法把数据写入缓冲
区,都会引起缓冲区状态的变化。

在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:

  • position:游标,指定下一个将要被写入或者读取的元素索引,它的值由 get()/put()方法自动更新,在新创建一个 Buffer 对象时,position 被初始化为0。
  • limit:界限,指定还有多少数据需要取出(从缓冲区写入通道时),或者还有多少空间可以放入数据(从通道读入缓冲区时)。
  • capacity:容量,指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。

以上三个属性值之间有一些相对大小的关系:0 <=position <= limit <=capacity

如果我们创建一个新的容量大小为 10 的 ByteBuffer 对象,在初始化的时候,position 设置为0,limit 和 capacity 被设置为 10,在以后使用 ByteBuffer 对象过程中,capacity 的值不会再发生变化,而其它两个将会随着使用而变化。

下面我们用代码来演示一遍,准备一个txt文档,存放的 C 盘,输入以下内容:

Zhangsan

下面我们用一段代码来验证 position、limit和 capacity 这几个值的变化过程,代码如下

public class BufferDemo {

    public static void main(String[] args) throws Exception {
        FileInputStream fin = new FileInputStream("C://test.txt");
        // 1.创建文件操作管道
        // 注:BIO中没有这一步
        FileChannel fc = fin.getChannel();
	    
        // 2.创建缓冲区(Buffer)
        // 初始化容量为10,就是创建一个大小为10的byte数组
        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);
        
		// 3.将管道中的数据读到Buffer中
        fc.read(buffer);
        output("调用read()", buffer);
		
        // 4.准备操作之前,先锁定操作范围
        buffer.flip();
        output("调用flip()", buffer);
		
        // 5.读取Buffer中的数据
        // 注:这里是逐字节读取
        while (buffer.remaining() > 0) {
            byte b = buffer.get();
        }
        output("调用get()", buffer);
		
        // 6.clear可以理解为解锁
        buffer.clear();
        output("调用clear()", buffer);

        fin.close();
    }

    private static void output(String step, ByteBuffer buffer) {
        System.out.println(step + " : ");
        // capacity,容量
        System.out.print("capacity: " + buffer.capacity() + ", ");
        // position,游标,记录要操作数据位置
        System.out.print("position: " + buffer.position() + ", ");
        // limit,界限,数据操作范围在position-limit之间
        System.out.print("limit: " + buffer.limit());
        System.out.println("\n");
    }
}

输出结果:

1)初始化,limit=capacitty,position=0

在这里插入图片描述

2) read(),读取数据进Buffer,position=数据大小
在这里插入图片描述

3)filp(),锁定,将limit移动到position,将position置0

在这里插入图片描述

4)get(),逐位读取Buffer中的数据,positon向limit移动,且移动范围不超过limit

在这里插入图片描述

5)clear(),解锁,position=0,limit=capacity

在这里插入图片描述

2.Buffer 高级使用

在文章开篇我们演示了的 Buffer 的基本使用,下面我们来看看关于 Buffer 的一些高级用法…

2.1 缓冲区分配

在前面的几个例子中,我们已经看到了,在创建一个缓冲区对象时,会调用静态方法allocate()来指定缓冲区的容量,其实调用allocate()相当于创建了一个指定大小的数组,并把它包装为缓冲区对象。

或者我们也可以直接将一个现有的数组,包装为缓冲区对象,如下示例代码所示:

public Buffer wrap() {
        // 方式一:分配指定大小缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
		
        // 方式二:手动包装一个现有数组
        byte[] arr = new byte[10];
        ByteBuffer buffer2 = ByteBuffer.wrap(arr);
        
        return buffer2;
    }

2.2 缓冲区分片

在NIO中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的。也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。

调用slice()方法可以创建一个子缓冲区,让我们通过例子来看一下:

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
	
    for (int i = 0; i < buffer.capacity(); i++) {
        // 注:这里要将int强转为byte
        buffer.put((byte)i);
    }
	
    // 创建[3, 7)的分片
    // 注:创建分片的方式是[position,limit),
    //     可以通过buffer.capacity()/limit()手动设置
    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);
    }
	
    // 注:因为修改了p,l,所以在最后读去原数组时要将position与limit还原
    buffer.position(0);
    buffer.limit(buffer.capacity());

    while (buffer.hasRemaining()) {
        System.out.print(buffer.get());
    }

}

在这里插入图片描述

2.3 只读缓冲区

只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。

可以通过调用缓冲区的 asReadOnlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。

PS:如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化

public class ReadOnlyBuffer {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
		
		// put
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte)i);
        }
	   
        // 通过原Buffer创建只读Buffer
        ByteBuffer readOnly = buffer.asReadOnlyBuffer();
		
        // 修改原Buffer
        for (int i = 0; i < buffer.capacity(); i++) {
            byte b = buffer.get(i);
            b *= 10;
            buffer.put(i, b);
        }
		
        readOnly.position(0);
        readOnly.limit(buffer.capacity());
		
        // 只读Buffer随之改变
        while (buffer.hasRemaining()) {
            System.out.println(readOnly.get());
        }
    }
}

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

创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

2.4 直接缓冲区

直接缓冲区是为加快I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。

也就是说,它会在每一次调用底层操作系统的本机I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区(JVM的)或者从一个中间缓冲区中拷贝数据。

要分配直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方法,使用方式与普通缓冲区并无区别,如下面的拷贝文件示例:

public class DirectBuffer { 
    static public void main( String args[] ) throws Exception {
		// 首先我们从磁盘上读取刚才我们写出的文件内容 
        String infile = "C://test.txt"; 
        FileInputStream fin = new FileInputStream( infile ); 
        FileChannel fcin = fin.getChannel();
        
		// 把刚刚读取的内容写入到一个新的文件中 
        String outfile = String.format("C://testcopy.txt"); 
        FileOutputStream fout = new FileOutputStream( outfile ); 
        FileChannel fcout = fout.getChannel();
        
		// 使用 allocateDirect,而不是 allocate 
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        
        while (true) { 
            buffer.clear();
            	
        	int r = fcin.read(buffer);
            if (r==-1) { 
                break; 
            }
        	buffer.flip();
        	fcout.write(buffer);
        }
	}
}

2.5 内存映射

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

public class MappedBuffer { 
    static private final int start = 0; 
    static private final int size = 1024;
    
	static public void main( String args[] ) throws Exception { 
        RandomAccessFile raf = new RandomAccessFile( "C://test.txt", "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();
	}
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

A minor

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

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

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

打赏作者

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

抵扣说明:

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

余额充值