【原创】掌握对 ByteBuffer 的操控感

作者:星巴刻

        作为 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() 的实现

工作区是 ByteBuffer 中最重要的要点(没有之一)

二、完成区 & 禁区

        在工作区的左右两侧另外分别有 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 处


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() 使 Buffer 进行了一次内部翻转

        现在深入到 flip() 内部。当程序不断把数据写到 ByteBuffer,完成区 将越来越大,充满了刚刚写入的数据,此时如果要将写入的数据读取出来,根据 ByteBuffer 的哲学,就需要先把这块完成区区域设置为 工作区 才能在这片区域上工作,按应用程序的预期完成任务。把完成区完全设置为工作区的操作工程中要注意 3 个细节就是:(1)新的工作区的终点就是原来完成区的终点、原来工作区的始点;(2)新的工作区的始点在最左边,因此新的工作区和旧的工作区大小没有任何关系,所以两者大小也不相等。(3) 旧的工作区变成现在新的工作区的右边了,所以它成为禁区的一部分。

        flip() 这个方法是 ByteBuffer 的关键方法,重点记住这个方法吧。

        练习:竖起两手的手掌,两只手中间代表的是 flip() 之前的工作区。然后两只手掌一起往左运动运动。左手掌拉伸到最左边的尽头,右手掌变动原来左手掌的位置。

六、clear() 全部变为工作区


clear() 让 ByteBuffer 做好了最大准备

        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

原文地址:http://www.jianshu.com/p/0343289302a8

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269