JDK8 NIO.Buffer源码解析 深入理解clear flip rewind compact

前言

在NIO体系中,Buffer至关重要,因为我们通过Buffer和Channel打交道,事实上我们永远无法和Channel直接打交道,只能通过Buffer作为媒介。如果把Channel比喻为蕴含着数据的矿山,那么Buffer就是用来装数据的矿车,我们只能把矿车送进矿车,并期待矿车回来时能塞满了来自矿山的数据,却不能直接进去挖矿。

成员

  • A buffer’s capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
  • A buffer’s position is the index of the next element to be read or written. A buffer’s position is never negative and is never greater than its limit.
  • A buffer’s limit is the index of the first element that should not be read or written. A buffer’s limit is never negative and is never greater than its capacity.
  • capacity代表不可能索引。即最大索引加一,capacity永远不变。
  • position代表即将读取或写入的元素的索引。所以初始化时position为0,因为即将读取或写入0号元素;当读取或写入了最后一个元素后,position就应该等于capacity了。
  • limit代表着读取或写入的限制,即不可以读取或写入位于limit索引的元素。当position已经增加到了limit,此时再读取或写入是不可以的。
  • 实际上我们可以操作(读取或写入)的范围就是position<= range < limit,即[ position, limit )左闭右开。
  • 必须保持0 <= mark <= position <= limit <= capacity

在这里插入图片描述
如图,初始状态时:mark为-1,position为0,capacity为最大索引+1(虚线框代表这是一个不存在的元素,capacity写在里面代表capacity永远不变,相对的,其他三个成员是可变的,所以是箭头表示),limit等于capacity。

传输数据

Buffer的子类用来传输数据的方法有get和put,但这些方法分为两个种类:

  • 相对操作。根据position成员来读取或写入数据,并且之后会根据读取写入的数据量来增加position。相对操作里,get操作可能抛出BufferUnderflowException;put操作可能抛出BufferOverflowException。
  • 绝对操作。不根据position成员,而是根据该方法的一个参数作为索引,然后根据这个索引get或put,不会改变position。可能抛出IndexOutOfBoundsException。

mark和reset

  • 上面还有一个成员mark没讲,它是用来标记位置的。当mark被调用时,position成员会赋值给mark成员;当reset函数被调用时,mark成员会赋值给position成员(一般情况下,position肯定是往后移动了)。
  • 但注意,肯定会保持不变关系0 <= mark <= position <= limit <= capacity。所以,当position或limit成员变得比mark成员还小的时候,mark会恢复默认。

深入理解clear flip rewind compact

前面也讲了,Buffer的索引只有一套,但我们却需要用来这套索引来进行读取或写入两种动作。既然只有一套索引(position和limit),那么肯定读模式下的索引,你再去写肯定是不对的;同样,写模式下的索引,你再去读肯定也是不对的。

  • clear() makes a buffer ready for a new sequence of channel-read or relative put operations: It sets the limit to the capacity and the position to zero.
  • flip() makes a buffer ready for a new sequence of channel-write or relative get operations: It sets the limit to the current position and then sets the position to zero.
  • rewind() makes a buffer ready for re-reading the data that it already contains: It leaves the limit unchanged and sets the position to zero.

clear和flip

上面三句api文档原话先不解释,先看里面提到的关键词:

  • channel-read or relative put operations。相对的put操作,肯定是往Buffer里放东西,然后索引增加;对channel的读操作,从channel里读出东西,再往Buffer里面放,然后索引增加。其实这就是对Buffer的写模式,既然要往Buffer写入数据,那就应该把position变成最小,limit变成最大,以便一次内获得最多的数据。所以在写模式进行之前,需要调用clear()。简单的说,写模式之前告诉别人Buffer的可写范围,别人不用管范围内是否已有数据,直接覆盖就好。在写模式的进行中,position会逐渐变大,但position不一定会到达limit,即不一定会写满数据,所以:写入数据的范围就是[0,position)
    public final Buffer clear() {
        position = 0;  // position变成最小
        limit = capacity;  // limit变成最大
        mark = -1;
        return this;
    }
  • channel-write or relative get operations。相对的get操作,从Buffer中读出数据,然后索引增加;对channel的写操作,即把Buffer里的数据,写入到channel里,然后索引增加。其实这就是对Buffer的读模式,既然要进行读模式,说明之前肯定执行过写模式好让Buffer塞好数据,上一条说了写入数据的范围是[0,position),所以在执行读模式之前,就应该把position赋值给limit,再把position置为0,这样,从position到limit的范围肯定就和之前执行写模式的写入数据的范围一样了。所以在执行读模式之前,需要调用flip()。这个也很好理解,既然是读模式,你肯定得把可读范围告诉人家啊,毕竟可读范围以外都是无效数据啊。
    public final Buffer flip() {
        limit = position;  // 把position赋值给limit
        position = 0;  // 再把position置为0
        mark = -1;
        return this;
    }

在这里插入图片描述
讲一下单词的意义吧:

  • clear的意思是清空,实际函数逻辑也是恢复初始状态。
  • clip的意思是翻转,从上图第二个状态到第三个状态也可以看出,且把position和limit两个指针当作整体,可以看出:一个指针的位置根本没有变化,另一个指针的位置从capacity变成0,这不就是翻转嘛。

rewind

  • re-reading the data。之前已经执行过读模式了,然后想再次读取数据,所以需要恢复到读模式之前的该有的状态,所以只需要把position归零,limit保持不变。所以,之前执行过读模式,想要再次从头读取数据,需要在第二次读模式之前调用rewind()。这样,调用rewind后,position和limit的位置,就和第一次读模式之前调用flip后的位置一样。
    public final Buffer rewind() {
        position = 0;  // position归零
        // limit保持不变
        mark = -1; 
        return this;
    }

在这里插入图片描述
讲一下单词的意义吧:rewind的意思是倒带,实际函数逻辑也只是把position恢复初始,这完全符合该函数的作用:为了第二次执行读模式,这不就是倒带嘛。

compact

既然这三个方法都讲了,还是提前讲一下compact方法吧:

  • 大家一定觉得clear方法有点太暴力了,因为它为了能够接下来进行写模式,把所有成员都恢复默认了。假设调用clear方法之前,进行了读模式,但却没有读完所有可读数据,在调用clear方法后,这些数据你再也无法去获得了,因为用来记录剩余可读数据的索引成员都被恢复默认了。
  • 如果有一种更温柔的方法,那么它的方法实现应该是:把剩余可读数据拷贝到Buffer的开头,然后把position设置为剩余可读数据的个数,把limit设置为capacity(还是为了可以在写模式中获取最多的数据)。这个方法正是compact方法。
  • 所以,在写模式之前,如果不用管剩余可读数据,那么调用clear();如果还需要剩余可读数据,那么调用compact()
//HeapByteBuffer.java
    public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());  // 剩余可读数据拷贝到Buffer的开头
        position(remaining());  // position设置为剩余可读数据的个数
        limit(capacity());  // 把limit设置为capacity
        discardMark();
        return this;
    }

在这里插入图片描述
如图,蓝色块为已读数据,绿色块为未读数据(剩余可读数据)。
讲一下单词的意义吧:compact的意思是压实,实际上也是把剩余可读数据压实到Buffer的前面去了。

抽象方法

  • public abstract boolean isReadOnly()。每个Buffer都是可读,但不一定是可写的。不可写的,那么就是只读的。
  • public abstract boolean hasArray()。一个Buffer它可以是由一个数组作为支撑的,即Buffer拥有一个数组成员作为数据来源。
  • public abstract Object array()。如果该Buffer是依靠数组的(the array that backs this buffer),那么就返回该数组。
  • public abstract int arrayOffset()。假设该Buffer是依靠数组的,但Buffer的第一个元素不一定在数组开头,所以该函数返回数组的偏移。
  • public abstract boolean isDirect()。该Buffer是不是直接的。

具体实现方法

相对的get操作,用到的方法

    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }

    final int nextGetIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }
  • 它们都是包的访问权限,只有一个包或者子类才能访问。这两个方法结束后,都会增加position。
  • 第一个方法是要相对地get到1个元素,所以索引加1。
  • 第二个方法是要相对地get到nb个元素,所以索引加nb。limit - position是剩余可get元素的个数,所以需要检查。
  • 两个方法都是返回position增加前的原position,比较原position才是get的起点。

相对的put操作,用到的方法

    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

    final int nextPutIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }

分析类似。

绝对的get/put操作,用到的方法

    final int checkIndex(int i) {                       // package-private
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }

    final int checkIndex(int i, int nb) {               // package-private
        if ((i < 0) || (nb > limit - i))
            throw new IndexOutOfBoundsException();
        return i;
    }

分析类似。两个方法都是从参数i开始get/put元素,第二个方法需要连续get/put nb个元素,所以需要检查当前可操作元素个数limit - i

其他方法

    static void checkBounds(int off, int len, int size) { // package-private
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }
  • 此静态方法用来检查边界。
  • 小于0是因为算出来的数字的符号位bit是1,所以会小于。
  • 括号里面有四个数字,且是用|与起来的,这四个数字只要有一个数字的符号位bit为1,算出来的数字就会小于0(因为|与的作用)。分别分析四种情况:
    • off小于0.
    • len小于0.
    • 虽然off len二者都不小于0,但是加起来后溢出,导致符号位bit为1.
    • off + len相当于limit,即第一个不应该操作的元素;size相当于capacity。当limit大于capacity时,肯定是错的啊。

链式操作

由于很多方法都return this,且他们的返回值类型都为Buffer,所以我们可以实现一些链式操作,可能会有用吧。

b.flip();
b.position(23);
b.limit(42);
//可以替换为下面这句
b.flip().position(23).limit(42);
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JDK 8的tar.gz下载是Java Development Kit(JDK)的一个版本,用于开发和运行Java应用程序。 首先,您需要访问Oracle官方网站以获取JDK 8的tar.gz文件。在浏览器中打开Oracle官方网站,在网站的顶部菜单中找到“Downloads”(下载)选项。点击此选项后,您将被带到一个新的页面。 在下载页面上,您将看到不同的JDK版本列表,找到JDK 8并选择它。然后,根据您操作系统的类型(Windows,macOS,Linux等),选择合适的JDK 8下载链接。 一旦您点击了链接,下载将开始。请注意,JDK 8的文件较大,可能需要一些时间来完成下载。一旦下载完成,您将得到一个名为jdk-8.tar.gz的文件。 接下来,您需要选择一个合适的位置来保存下载的tar.gz文件。您可以选择您计算机上的任意文件夹,只要您能方便地找到它即可。 现在,您可以使用解压工具(如WinRAR、7-Zip等)来解压tar.gz文件。打开您选择保存文件的文件夹,右键单击tar.gz文件,并选择“提取到当前文件夹”或类似选项。解压将开始,并且在完成后将生成一个新的文件夹。 在解压后的文件夹中,您将找到JDK 8的安装文件和其他重要文件。您可以根据您的需求和计算机系统进行配置和安装。 总结起来,JDK 8的tar.gz下载需要访问Oracle官方网站,选择合适的操作系统版本并下载文件。下载完成后,您需要使用解压工具来解压tar.gz文件,并在解压后的文件夹中找到JDK 8的安装文件。 ### 回答2: 要下载JDK8的tar.gz文件,首先需要前往官方网站(https://www.oracle.com/java/technologies/javase-jdk8-downloads.html)。在该页面中,可以选择适合自己操作系统的版本并点击下载。 下载完成后,可以在本地的下载文件夹中找到tar.gz文件。为了安装JDK8,需要将tar.gz文件解压缩。可以使用终端或命令行界面进入下载文件夹,然后执行以下命令: tar -zxvf jdk-8uXX-linux-x64.tar.gz 这里的"XX"应该替换为具体的版本号。执行该命令后,tar.gz文件将解压缩到当前目录中。 解压缩完成后,可以将解压后的文件夹移动到所需的位置。例如,将其移动到/opt目录下,可以使用以下命令: sudo mv jdk1.8.0_XX /opt 同样,"XX"应该替换为具体的版本号。执行该命令时,可能需要输入管理员密码来获取权限。 最后,为了能够在系统中正确使用Java,需要设置环境变量。通过编辑~/.bashrc文件,在末尾添加以下行: export JAVA_HOME=/opt/jdk1.8.0_XX export PATH=$PATH:$JAVA_HOME/bin 同样,"XX"应该替换为具体的版本号。保存并关闭文件。 最后,要使新的环境变量生效,可以打开新的终端窗口,或者执行以下命令: source ~/.bashrc 至此,JDK8已经成功安装并设置好环境变量。可以通过执行以下命令来验证: java -version 如果成功安装,将显示JDK8的版本信息。 希望以上回答对您有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值