49天精通Java,第43天,缓冲区数据结构bytebuffer

在这里插入图片描述

专栏导读

本专栏收录于《49天精通Java从入门到就业》,本专栏专门针对零基础和需要进阶提升的同学所准备的一套完整教学,从0开始,不断进阶深入,后续还有《手把手springboot+vue实战项目》,轻松应对面试,专栏订阅地址:https://blog.csdn.net/guorui_java/category_11461823.html

在这里插入图片描述

一、缓冲区

缓冲区是由具有相同类型的数值构成的数组,Buffer是一个抽象类,它有很多子类,包括ByteBuffer、CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ShortBuffer。

每个缓冲区都具有4个属性:

  1. 容量capacity,缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,且永远不能被改变;
  2. 读写位置position,下一个要被读或写的元素的索引。位置会自动由相应的 get() 和 put() 函数更新。 这里需要注意的是positon的位置是从0开始,比如,已经写入buffer 3个元素那那么position就是指向第4个位置,即position设置为3(数组从0开始计);
  3. 界限limit,缓冲区的第一个不能被读或写的元素。缓冲区创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512;
  4. 可选的标记 mark,标记,一个备忘位置。保存某个时刻的position指针的值,通过调用mark()实现,当mark被置为负值时,表示废弃标记。标记在设定前是未定义的(undefined)。使用场景是,假设缓冲区中有 10 个元素,position 目前的位置为 2(也就是如果get的话是第三个元素),现在只想发送 6 - 10 之间的缓冲数据,此时我们可以 buffer.mark(buffer.position());即把当前的 position 记入 mark 中,然后 buffer.postion(6);此时发送给 channel 的数据就是 6 - 10 的数据。发送完后,我们可以调用 buffer.reset() 使得 position = mark,因此这里的 mark 只是用于临时记录一下位置用的。

position和limit之间的距离指定了可读/写的字节数。

-1 <= mark <= position <= limit <= capacity
0<= position <= limit <= capacity

在这里插入图片描述

使用缓冲区的主要目的是执行读写循环操作。

假设我们有一个缓冲区,在一开始,它的位置是0,界限等于容量。我们不断地调用put将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切入到读操作了。

这时可以调用flip方法将界限设置到当前位置,并把位置复位到0.现在在remaining方法返回正数时(它返回的值是界限 - 位置),不断地调用get。在我们将缓冲区中所有的值都写入之后,调用clear使缓冲区为下一次写循环做好准备。clear方法将位置复位到0,并将界限复位到容量。

如果想重读缓冲区,可以使用rewind或mark/reset进行复位。

然后可以 用某个通道的数据填充缓冲区,或者将缓冲区的内容写出到通道中。

ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);

Buffer及其子类都不是线程安全的,若多线程操作该缓冲区,则应通过同步来控制对该缓冲区的访问。

二、常用方法

1、clear()

position设为0,把limit设为capacity,取消标记,一般在把数据写入Buffer前调用,Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法以后,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

2、 compact()

如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

3、flip()

把limit设为当前position,把position设为0,取消标记,一般在从Buffer中读取数据前调用。

4、rewind()

把position设为0,limit不变,一般在把数据重写入Buffer前调用。

5、hasRemaining()

当缓冲区至少还有一个元素时,返回true。

6、remaining()

position和limit之间字节个数

7、 reset()

将position的值还原成上次调用mark()方法后的position的值。

8、allocate()

分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中,用静态方法 allocate() 来分配缓冲区。

9、wrap()

将一个已有的byte数组包装出一个新的bytebuffer对象。

10、slice()

根据现有的缓冲区创建一个子缓冲区。也就是它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据,子缓冲区和缓冲区 共享同一个底层数据数组,分片操作是根据当前position以及limit的值来确定的。

11、order()

返回ByteBuffer的字节序。

三、通道获取

1、从 FileInputStream / FileOutputStream 中获取

从 FileInputStream 对象中获取的通道是以读的方式打开文件,从 FileOutpuStream 对象中获取的通道是以写的方式打开文件。

FileOutputStream ous = new FileOutputStream(new File("nezha.txt"));
// 获取一个只读通道
FileChannel out = ous.getChannel(); 
FileInputStream ins = new FileInputStream(new File("nezha.txt"));
// 获取一个只写通道
FileChannel in = ins.getChannel();  

2、从 RandomAccessFile 中获取

从 RandomAccessFaile 中获取的通道取决于 RandomAccessFaile 对象是以什么方式创建的,“r”, “w”, “rw” 分别对应着读模式,写模式,以及读写模式。

RandomAccessFile file = new RandomAccessFile("a.txt", "rw");
// 获取一个可读写文件通道
FileChannel channel = file.getChannel(); 

3、通过 FileChannel.open()获取

通过静态静态方法 FileChannel.open() 打开的通道可以指定打开模式,模式通过 StandardOpenOption 枚举类型指定。

// 以只读的方式打开一个文件 nezha.txt 
FileChannel channel = FileChannel.open(Paths.get("nezha.txt"), StandardOpenOption.READ); 的通道

四、写入数据

1、从单个缓冲区写入

单个缓冲区操作也非常简单,它返回往通道中写入的字节数。

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer buf = ByteBuffer.allocate(5);
byte[] data = "Hello, Java NIO.".getBytes();
for (int i = 0; i < data.length; ) {
    
    
    buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
    buf.flip();
    i += channel.write(buf);
    buf.compact();
}
channel.force(false);
channel.close();

2、从多个缓冲区写入

FileChannel 实现了 GatherringByteChannel 接口,与 ScatteringByteChannel 相呼应。可以一次性将多个缓冲区的数据写入到通道中。

FileChannel channel = FileChannel.open(Paths.get("nezha.txt"), StandardOpenOption.WRITE);
ByteBuffer key = ByteBuffer.allocate(10), value = ByteBuffer.allocate(10);
byte[] data = "017 Robothy".getBytes();
key.put(data, 0, 3);
value.put(data, 4, data.length-4);
ByteBuffer[] buffers = new ByteBuffer[]{
    
    key, value};
key.flip();
value.flip();
channel.write(buffers);
channel.force(false);
channel.close();

五、读取数据

读取数据的 read(ByteBuffer buf) 方法返回的值表示读取到的字节数,如果读到了文件末尾,返回值为 -1。读取数据时,position 会往后移动。

1、将数据读取到单个缓冲区

和一般通道的操作一样,数据也是需要读取到1个缓冲区中,然后从缓冲区取出数据。在调用 read 方法读取数据的时候,可以传入参数 position 和 length 来指定开始读取的位置和长度。

FileChannel channel = FileChannel.open(Paths.get("nezha.txt"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(5);
while(channel.read(buf)!=-1){
    
    
    buf.flip();
    System.out.print(new String(buf.array()));
    buf.clear();
}
channel.close();

2、读取到多个缓冲区

文件通道 FileChannel 实现了 ScatteringByteChannel 接口,可以将文件通道中的内容同时读取到多个 ByteBuffer 当中,这在处理包含若干长度固定数据块的文件时很有用。

ScatteringByteChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer key = ByteBuffer.allocate(5), value=ByteBuffer.allocate(10);
ByteBuffer[] buffers = new ByteBuffer[]{
    
    key, value};
while(channel.read(buffers)!=-1){
    
    
    key.flip();
    value.flip();
    System.out.println(new String(key.array()));
    System.out.println(new String(value.array()));
    key.clear();
    value.clear();
}
channel.close();

六、视图缓冲区

可以用ByteBuffer对象创建其他类型的缓冲区,新缓冲区共享原始ByteBuffer的全部或者部分内存,这样的缓冲区被叫做视图缓冲区,就是通过源缓冲区创建其他的基础数据类型的缓冲区,新缓冲区和源缓冲区共享数据,但各自维护自己的属性(capacity、limit、position、mark)。

七、使用Buffer读/写数据一般遵循以下四个步骤

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

当向buffer写入数据时,buffer会记录下写了多少数据;一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式;在读模式下,可以读取之前写入到buffer的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。

有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

猜你喜欢

转载自blog.csdn.net/guorui_java/article/details/132819027