小师妹学JavaIO之Buffer和Buff

14次阅读

共计 6434 个字符,预计需要花费 17 分钟才能阅读完成。

简介

小师妹在学习 NIO 的路上越走越远,唯一能够帮到她的就是在她需要的时候给她以全力的支持。什么都不说了,今天介绍的是 NIO 的基础 Buffer。老铁给我上个 Buff。

Buffer 是什么

小师妹:F 师兄,这个 Buffer 是我们纵横王者峡谷中那句:老铁给我加个 Buff 的意思吗?

当然不是了,此 Buffer 非彼 Buff,Buffer 是 NIO 的基础,没有 Buffer 就没有 NIO,没有 Buffer 就没有今天的 java。

因为 NIO 是按 Block 来读取数据的,这个一个 Block 就可以看做是一个 Buffer。我们在 Buffer 中存储要读取的数据和要写入的数据,通过 Buffer 来提高读取和写入的效率。

更多精彩内容且看:

  • 区块链从入门到放弃系列教程 - 涵盖密码学, 超级账本, 以太坊,Libra, 比特币等持续更新
  • Spring Boot 2.X 系列教程: 七天从无到有掌握 Spring Boot- 持续更新
  • Spring 5.X 系列教程: 满足你对 Spring5 的一切想象 - 持续更新
  • java 程序员从小工到专家成神之路(2020 版)- 持续更新中, 附详细文章教程

更多内容请访问 www.flydean.com

还记得 java 对象的底层存储单位是什么吗?

小师妹:这个我知道,java 对象的底层存储单位是字节 Byte。

对,我们看下 Buffer 的继承图:

Buffer 是一个接口,它下面有诸多实现,包括最基本的 ByteBuffer 和其他的基本类型封装的其他 Buffer。

小师妹:F 师兄,有 ByteBuffer 不就够了吗?还要其他的类型 Buffer 做什么?

小师妹,山珍再好,也有吃腻的时候,偶尔也要换个萝卜白菜啥的,你以为乾隆下江南都干了些啥?

ByteBuffer 虽然好用,但是它毕竟是最小的单位,在它之上我们还有 Char,int,Double,Short 等等基础类型,为了简单起见,我们也给他们都搞一套 Buffer。

Buffer 进阶

小师妹:F 师兄,既然 Buffer 是这些基础类型的集合,为什么不直接用结合来表示呢?给他们封装成一个对象,好像有点多余。

我们既然在面向对象的世界,从表面来看自然是使用 Object 比较合乎情理,从底层的本质上看,这些封装的 Buffer 包含了一些额外的元数据信息,并且还提供了一些意想不到的功能。

上图列出了 Buffer 中的几个关键的概念,分别是 Capacity,Limit,Position 和 Mark。Buffer 底层的本质是数组,我们以 ByteBuffer 为例,它的底层是:

final byte[] hb; 
  • Capacity 表示的是该 Buffer 能够承载元素的最大数目,这个是在 Buffer 创建初期就设置的,不可以被改变。
  • Limit 表示的 Buffer 中可以被访问的元素个数,也就是说 Buffer 中存活的元素个数。
  • Position 表示的是下一个可以被访问元素的 index,可以通过 put 和 get 方法进行自动更新。
  • Mark 表示的是历史 index,当我们调用 mark 方法的时候,会把设置 Mark 为当前的 position,通过调用 reset 方法把 Mark 的值恢复到 position 中。

创建 Buffer

小师妹:F 师兄呀,这么多 Buffer 创建起来是不是很麻烦?有没有什么快捷的使用办法?

一般来说创建 Buffer 有两种方法,一种叫做 allocate,一种叫做 wrap。

public void createBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);
        log.info("{}",intBuffer);
        log.info("{}",intBuffer.hasArray());
        int[] intArray=new int[10];
        IntBuffer intBuffer2= IntBuffer.wrap(intArray);
        log.info("{}",intBuffer2);
        IntBuffer intBuffer3= IntBuffer.wrap(intArray,2,5);
        log.info("{}",intBuffer3);
        intBuffer3.clear();
        log.info("{}",intBuffer3);
        log.info("{}",intBuffer3.hasArray());
    }

allocate 可以为 Buffer 分配一个空间,wrap 同样为 Buffer 分配一个空间,不同的是这个空间背后的数组是自定义的,wrap 还支持三个参数的方法,后面两个参数分别是 offset 和 length。

INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - true
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=7 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - true

hasArray 用来判断该 Buffer 的底层是不是数组实现的,可以看到,不管是 wrap 还是 allocate,其底层都是数组。

需要注意的一点,最后,我们调用了 clear 方法,clear 方法调用之后,我们发现 Buffer 的 position 和 limit 都被重置了。这说明 wrap 的三个参数方法设定的只是初始值,可以被重置。

Direct VS non-Direct

小师妹:F 师兄,你说了两种创建 Buffer 的方法,但是两种 Buffer 的后台都是数组,难道还有非数组的 Buffer 吗?

自然是有的, 但是只有 ByteBuffer 有。ByteBuffer 有一个 allocateDirect 方法,可以分配 Direct Buffer。

小师妹:Direct 和非 Direct 有什么区别呢?

Direct Buffer 就是说,不需要在用户空间再复制拷贝一份数据,直接在虚拟地址映射空间中进行操作。这叫 Direct。这样做的好处就是快。缺点就是在分配和销毁的时候会占用更多的资源,并且因为 Direct Buffer 不在用户空间之内,所以也不受垃圾回收机制的管辖。

所以通常来说只有在数据量比较大,生命周期比较长的数据来使用 Direct Buffer。

看下代码:

public void createByteBuffer() throws IOException {ByteBuffer byteBuffer= ByteBuffer.allocateDirect(10);
        log.info("{}",byteBuffer);
        log.info("{}",byteBuffer.hasArray());
        log.info("{}",byteBuffer.isDirect());

        try (RandomAccessFile aFile = new RandomAccessFile("src/main/resources/www.flydean.com", "r");
             FileChannel inChannel = aFile.getChannel()) {MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
            log.info("{}",buffer);
            log.info("{}",buffer.hasArray());
            log.info("{}",buffer.isDirect());
        }
    }

除了 allocateDirect, 使用 FileChannel 的 map 方法也可以得到一个 Direct 的 MappedByteBuffer。

上面的例子输出结果:

INFO com.flydean.BufferUsage - java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]
INFO com.flydean.BufferUsage - false
INFO com.flydean.BufferUsage - true
INFO com.flydean.BufferUsage - java.nio.DirectByteBufferR[pos=0 lim=0 cap=0]
INFO com.flydean.BufferUsage - false
INFO com.flydean.BufferUsage - true

Buffer 的日常操作

小师妹:F 师兄,看起来 Buffer 确实有那么一点复杂,那么 Buffer 都有哪些操作呢?

Buffer 的操作有很多,下面我们一一来讲解。

向 Buffer 写数据

向 Buffer 写数据可以调用 Buffer 的 put 方法:

public void putBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);
        intBuffer.put(1).put(2).put(3);
        log.info("{}",intBuffer.array());
        intBuffer.put(0,4);
        log.info("{}",intBuffer.array());
    }

因为 put 方法返回的还是一个 IntBuffer 类,所以 Buffer 的 put 方法可以像 Stream 那样连写。

同时,我们还可以指定 put 在什么位置。上面的代码输出:

INFO com.flydean.BufferUsage - [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
INFO com.flydean.BufferUsage - [4, 2, 3, 0, 0, 0, 0, 0, 0, 0]

从 Buffer 读数据

读数据使用 get 方法,但是在 get 方法之前我们需要调用 flip 方法。

flip 方法是做什么用的呢?上面讲到 Buffer 有个 position 和 limit 字段,position 会随着 get 或者 put 的方法自动指向后面一个元素,而 limit 表示的是该 Buffer 中有多少可用元素。

如果我们要读取 Buffer 的值则会从 positon 开始到 limit 结束:

public void getBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);
        intBuffer.put(1).put(2).put(3);
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {log.info("{}",intBuffer.get());
        }
        intBuffer.clear();}

可以通过 hasRemaining 来判断是否还有下一个元素。通过调用 clear 来清除 Buffer,以供下次使用。

rewind Buffer

rewind 和 flip 很类似,不同之处在于 rewind 不会改变 limit 的值,只会将 position 重置为 0。

public void rewindBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);
        intBuffer.put(1).put(2).put(3);
        log.info("{}",intBuffer);
        intBuffer.rewind();
        log.info("{}",intBuffer);
    }

上面的结果输出:

INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]

Compact Buffer

Buffer 还有一个 compact 方法,顾名思义 compact 就是压缩的意思,就是把 Buffer 从当前 position 到 limit 的值赋值到 position 为 0 的位置:

public void useCompact(){IntBuffer intBuffer= IntBuffer.allocate(10);
        intBuffer.put(1).put(2).put(3);
        intBuffer.flip();
        log.info("{}",intBuffer);
        intBuffer.get();
        intBuffer.compact();
        log.info("{}",intBuffer);
        log.info("{}",intBuffer.array());
    }

上面代码输出:

INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=2 lim=10 cap=10]
INFO com.flydean.BufferUsage - [2, 3, 3, 0, 0, 0, 0, 0, 0, 0]

duplicate Buffer

最后我们讲一下复制 Buffer,有三种方法,duplicate,asReadOnlyBuffer,和 slice。

duplicate 就是拷贝原 Buffer 的 position,limit 和 mark,它和原 Buffer 是共享原始数据的。所以修改了 duplicate 之后的 Buffer 也会同时修改原 Buffer。

如果用 asReadOnlyBuffer 就不允许拷贝之后的 Buffer 进行修改。

slice 也是 readOnly 的,不过它拷贝的是从原 Buffer 的 position 到 limit-position 之间的部分。

public void duplicateBuffer(){IntBuffer intBuffer= IntBuffer.allocate(10);
        intBuffer.put(1).put(2).put(3);
        log.info("{}",intBuffer);
        IntBuffer duplicateBuffer=intBuffer.duplicate();
        log.info("{}",duplicateBuffer);
        IntBuffer readOnlyBuffer=intBuffer.asReadOnlyBuffer();
        log.info("{}",readOnlyBuffer);
        IntBuffer sliceBuffer=intBuffer.slice();
        log.info("{}",sliceBuffer);
    }

输出结果:

INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBufferR[pos=3 lim=10 cap=10]
INFO com.flydean.BufferUsage - java.nio.HeapIntBuffer[pos=0 lim=7 cap=7]

总结

今天给小师妹介绍了 Buffer 的原理和基本操作。

本文的例子 https://github.com/ddean2009/learn-java-io-nio

本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/java-io-nio-buffer/

本文来源:flydean 的博客

欢迎关注我的公众号: 程序那些事,更多精彩等着您!

正文完
 0