作者:星巴刻
? ? ? ? 作为 Java Nio 的一个基础部分,其提供的 java.nio.ByteBuffer 不易被正确使用简直让人无语,无人愿意为它辩白。ByteBuffer 本质只是 byte 数组的封装,但是与 byte 数组相比起来,要理解好,需要耗费点脑力。本文尝试用一种新的结构来解释 ByteBuffer,用以加速正确、轻松地掌握 ByteBuffer 的使用,希望通过本文的铺垫再去看 ByteBuffer 的注释、源代码以及应用代码,能掌握一种操控感。
一、工作区
? ? ? ? ByteBuffer 虽然是 byte 数组的封装,但是应用程序如何使用数组是受约束的,极少直接通过指定数组下标的方式使用 ByteBuffer。
? ? ? ? 在此引入「工作区」的概念,用来助力理解。
? ? ? ? 工作区是一个两边伸缩变动长度的区域,它的最左边是始点(用 position 表示),右边是它的终点(用 limit 表示)。现在请用你的右手掌挡住工作区的终点(limit 处),左手掌从工作区的左边 position 处往右压。这个过程中,右边将保持静止(淡定的静止),随着左手掌往右动,有节奏地一动一停地往右,就像脉冲一样的节奏往右。这样,整个工作区将越来越小,position 位置渐渐地向 limit 点靠拢,直至两只手掌合在一起,此时 position 点和 limit 点重合。
? ? ? ? 要是觉得这个动作有点幼稚,那就对了,说明完全掌握 ByteBuffer 其实也没有很大难度。
? ? ? ? 工作区的大小,可由 limit - position 来表示,这也就是 ByteBuffer.remaining() 的实现
二、完成区 & 禁区
? ? ? ? 在工作区的左右两侧另外分别有 1 个区域:工作区的左侧是完成区,右侧是禁区,如下图。
? ? ? ? 这样整个 ByteBuffer 3 个区的结构就构建完毕了。这 3 个区先后顺序是固定的,但大小是变化的。3个区的宽度大小,最小可为 0,最大可为 capacity 大。3 个区看起来还是很乱,请不要被这个影响,一定要把注意力优先投资到两个手掌之间的工作区,这样已经足够。现在竖起双手掌,由于手是可以动的,所以左手表示的 position 以及右手表示的 limit 是可以变动的,特别是左手变动是最频繁。随着动作,工作区大小产生了变化,自然而然地也带动了左边完成区以及右边禁区的变化。
三、读/写操作
? ? ? ? 当对 ByteBuffer 进行操作时,所有操作都是在工作区上完成的!
? ? ? ? 进行 get() 时,每一次 get() 的调用,工作区中的 position 位置的字节被读出来,随后工作区的始点向右运动一个位置。随着不断地 get(),工作区的始点一点一点地往右运动,越变越小。// 手势做起来哈,右手掌不动,左手掌往右手掌的方向动,get 一次,动一次。尽量多做几遍
? ? ? ? 同理的,进行 put() 时,每一次 put() 调用,字节都写入到工作区的 position 位置中,随后工作区的始点向右运动一个位置。随着不断地 put(),工作区的始点一点一点地往右运动,越变越小。// put 和 get 的手势完全一样
? ? ? ? 一旦工作区大小变为 0 了,读写操作就不能再进行了,禁区是不可用于读写操作的。如果强行继续读取或写入,ByteBuffer 将分别抛出 BufferUnderflowException 或 BufferOverflowException 异常。
四、reset() 回到原先设置的 mark 处
? ? ? ? 随着 ByteBuffer 不断地工作,工作区始点逐渐往右运动,工作区越变越小。此时如果要重读刚才读取的内容,或者覆盖原先写入的内容,就可以调用 reset() 方法来满足这个需求,将工作区的始点拉回之前设置的 mark 点。
? ? ? ? reset() 操作必须和 mark() 操作结合使用。调用 mark() 时候,ByteBuffer 会把当时工作区的始点记录下来(用 mark 表示这个位置),
? ? ? ? 调用 reset() 方法并不会把 mark 标识清除,后续可以多次使用。如果之前没有 mark() 过或者 mark 标识被 rewind()、flip()、clear() 这些操作清理过,调用 reset() 没有意义,ByteBuffer 会抛出异常。此时如果要回到某个点,建议直接使用 ByteBuffer.position(int) 搞定,所谓调用 position(int) 的本质也就是应用程序自己来维护 mark 记录,这也是一个好办法。
? ? ? ? 注意:reset 方法不是把缓冲区的字节设置为 0。
? ? ? ? 练习:如何用手势来模拟 reset() 操作呢?其实非常简单,保持右手掌不懂,左手掌向左稍微挪动几步。
五、rewind() 倒带重来
? ? ? ? 英文单词 rewind 有重倒的意思。调用 rewind() 就是把工作区的始点拉到 0 处,使得接下来的工作区从 ByteBuffer 的最开始处工作。这个有啥用呢?想来想去可能在「复读」这个场景比较有用:
? ? ? 当一个 ByteBuffer 要写到多个输出源的时候可以用得上:写入到第一个输出源后,完成区变大,工作区变小,通过调用 rewind() ,把工作区的始点拉到 ByteBuffer 最开始的地方,这样就可以重新从读取刚才已经读取的字节了。
? ? ? ? 在 ByteBuffer下 rewind() 就是 position(0)。所以,实际使用起来,直接使用 position(0) 可能更容易理解?另外一个区别点, position(int) 方法在 ByteBuffer 上,没在 Buffer 上。
? ? ? ? 练习:如何用手势来模拟 rewind() 操作呢?保持右手掌不懂,左手掌向左伸直移动到最大的可能就是了。// reset() 和 rewind() 在手势上的区分就是看左手伸的多少,到之前标记的是 reset(),伸到尽头的是 rewind()
四、flip() 翻转工作区
? ? ? ? 英文单词 flip 的意思有翻、转的意思,比如海狮在沙滩上玩耍翻来翻去,调皮的同学在地上做个腾空翻等等类似的意思。
? ? ? ? 把 flip 用在 ByteBuffer 上,主要是用来表达一个动机:对 ByteBuffer 完成写入的工作后,要开始从它里面读取信息。ByteBuffer要求,当对它从写入到读取的变化,需要应用程序来告知 ByteBuffer 提前做一些内部翻转工作,flip() 方法充当这个作用,由应用程序来调用。
? ? ? ? 现在深入到 flip() 内部。当程序不断把数据写到 ByteBuffer,完成区 将越来越大,充满了刚刚写入的数据,此时如果要将写入的数据读取出来,根据 ByteBuffer 的哲学,就需要先把这块完成区区域设置为 工作区 才能在这片区域上工作,按应用程序的预期完成任务。把完成区完全设置为工作区的操作工程中要注意 3 个细节就是:(1)新的工作区的终点就是原来完成区的终点、原来工作区的始点;(2)新的工作区的始点在最左边,因此新的工作区和旧的工作区大小没有任何关系,所以两者大小也不相等。(3) 旧的工作区变成现在新的工作区的右边了,所以它成为禁区的一部分。
? ? ? ? flip() 这个方法是 ByteBuffer 的关键方法,重点记住这个方法吧。
? ? ? ? 练习:竖起两手的手掌,两只手中间代表的是 flip() 之前的工作区。然后两只手掌一起往左运动运动。左手掌拉伸到最左边的尽头,右手掌变动原来左手掌的位置。
六、clear() 全部变为工作区
? ? ? ? clear() 把缓冲区全部变为工作区,工作区最大也不过如此了。clear() 操作是唯一一个把禁区变为工作区一部分的操作??杉?clear() 目的,就是让 ByteBuffer 有最大的工作空间去容纳一会进来的字节。显然,当 ByteBuffer 的信息全部被用来后,准备要从输入源中读出新的信息写入 ByteBuffer 时,要调用 clear()。
? ? ? ? 注意:clear 方法不是把缓冲区的字节设置为 0。
? ? ? ? 练习:竖起两手的手掌,然后分别向两边拉伸到尽头!
七、总结
? ? ? ? ? 用工作区的概念及其图解、手势的方式来理解 ByteBuffer,是本文的创新点。借助两手手掌模拟工作区,并演示 get、put、reset、rewind、position(i)、flip、clear 操作对手掌位置的影响可以有效地理解和记忆。这种办法对其他人有没有用我不清楚,反正自己是用上了,也轻松了许多。
? ? ? ? ? 如果以上有助于理解,接下来可以直接看下 java.nio.Buffer 的 Java Doc ,看看是否可以清晰一些,这个过程也是一次「思维加固」。
2017-11-23