理解ByteBuffer

Buffer 的使用

我们通过 Java 中 NIO 包中实现的 Buffer 来给大家讲解,Buffer 总共有 7 种实现,就包含了 Java 中实现的所有数据类型。

本篇文章中,我们使用的是 ByteBuffer,其常用的方法都有:

  • put
  • get
  • flip
  • rewind
  • mark
  • reset
  • clear

接下来我们就通过实际的例子来了解这些方法。

put

put 就是往 ByteBuffer 里写入数据,其有有很多重载的实现:

public ByteBuffer put(ByteBuffer src) {...}

public ByteBuffer put(byte[] src, int offset, int length) {...}

public final ByteBuffer put(byte[] src) {...}

我们可以直接传入 ByteBuffer 对象,也可以直接传入原生的 byte 数组,还可以指定写入的 offset 和长度等等。接下来看个具体的例子:

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    buffer.put(new byte[]{'s','h'});
}

为了能让大家更直观的看出 ByteBuffer 内部的情况,我将它整理成了图的形式。当上面的代码运行完之后 buffer 的内部长这样:

当你尝试使用 System.out.println(buffer) 去打印变量 buffer 的时候,你会看到这样的结果:

java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]

图里、控制台里都有 position 和 limit 变量,capacity 大家能理解,就是我们创建这个 ByteBuffer 的制定的大小 16

而至于另外两个变量,相信大家从图中也可以看出来,position 变量指向的是下一次要写入的下标,上面的代码我们只写入了 2 个字节,所以 position 指向的是 2,而这个 limit 就比较有意思了,这个在后面的使用中结合例子一起讲。

get

get 是从 ByteBuffer 中获取数据。

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s','h'});
  System.out.println(buffer.get());
}

如果你运行完上面的代码你会发现,打印出来的结果是 0 ,并不是我们期望的 s 的 ASCII 码 115

首先告诉大家结论,这是符合预期的,这个时候就不应该能获取到值。我们来看看 get 的源码:

public byte get() { return hb[ix(nextGetIndex())]; }

protected int ix(int i) { return i + offset; }

final int nextGetIndex() {                          
  int p = position;
  if (p >= limit)
    throw new BufferUnderflowException();
  // 这里 position 会往后移动一位
  position = p + 1;
  return p;
}

当前 position 是 2,而 limit 是 16,所以最终 nextGetIndex 计算出来的值就是变量 p 的值 2 ,再过一次 ix ,那就是 2 + 0 = 2,这里的 offset 的值默认为 0 。

所以简单来说,最终会取到下标为 2 的数据,也就是下图这样。

所以我们当然获取不到数据。但是这里需要关注的是,调用 get 方法虽然没有获取到任何数据,但是会使得 position 指针往后移动。换句话说,会占用一个位置。如果连续调用几次这种 get 之后,再调用 put 方法写入数据,就会造成有几个位置没有赋值。举个例子,假设我们运行以下代码:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s','h'});

  buffer.get();
  buffer.get();
  buffer.get();
  buffer.get();

  buffer.put(new byte[]{'e'});
}

数据就会变成下图这样,position 会往后移动

那你可能会问,那我真的需要获取数据咋办?在这种情况下,可以像这样获取:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s'});
  System.out.println(buffer.get(0)); // 115
}

传入我们想要获取的下标,就可以直接获取到,并且不会造成 position 的后移。

看到这那你更懵逼了,合着 get() 就没法用呗?还必须要给个 index。这就需要聊一下另一个方法 flip了。

flip

废话不多说,先看看例子:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s', 'h'}); // java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]
  buffer.flip();
  System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=2 cap=16]
}

有意思的事情发生了,调用了 flip 之后,position 从 2 变成了 0,limit 从 16 变成了 2。

这个单词是「 翻动」的意思,我个人的理解是像翻东西一样把之前存的东西全部翻一遍

你会发现,position 变成了 0,而 limit 变成 2,这个范围刚好是有值的区间

接下来就更有意思了:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s', 'h'});
  buffer.flip();
  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h
}

调用了 flip 之后,之前没法用的 get() 居然能用了。结合 get 中给的源码不难分析出来,由于 position 变成了 0,最终计算出来的结果就是 0,同时使 position 向后移动一位。

终于到这了,你可以理解成 Buffer 有两种状态,分别是:

  • 读模式
  • 写模式

刚刚创建出来的 ByteBuffer 就处于一个写模式的状态,通过调用 flip 我们可以将 ByteBuffer 切换成读模式。但需要注意,这里讲的读、写模式只是一个逻辑上的概念

举个例子,当调用 flip 切换到所谓的写模式之后,依然能够调用 put 方法向 ByteBuffer 中写入数据。

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s', 'h'});
  buffer.flip();
  buffer.put(new byte[]{'e'});
}

这里的 put 操作依然能成功,但你会发现最后写入的 e 覆盖了之前的数据,现在 ByteBuffer 的值变成了 eh 而不是 sh 了。

所以你现在应该能够明白,读模式、写模式更多的含义应该是:

  • 方便你模式
  • 方便你模式
顺带一提,调用 flip 进入写读模式之后,后续如果调用  get() 导致  position  大于等于了  limit 的值,程序会抛出  BufferUnderflowException 异常。这点从之前  get 的源码也可以看出来。

rewind

rewind 你也可以理解成是运行在读模式下的命令,给大家看个例子:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'s', 'h'});
  buffer.flip();
  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h

  // 从头开始读
  buffer.rewind();

  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h
}

所谓的从头开始读就是把 position 给归位到下标为 0 的位置,其源码也很简单:

public final Buffer rewind() {
  position = 0;
  mark = -1;
  return this;
}

就是简单的把 position 赋值为 0,把 mark 赋值为 -1。那这个 mark 又是啥东西?这就是我们下一个要聊的方法。

mark & reset

mark 用于标记当前 postion 的位置,而 reset 之所以要放到一起讲是因为 reset 是 reset 到 mark 的位置,直接看例子:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'a', 'b', 'c', 'd'});
  
  // 切换到读模式
  buffer.flip();
  System.out.println((char) buffer.get()); // a
  System.out.println((char) buffer.get()); // b

  // 控记住当前的 position
  buffer.mark();
  
  System.out.println((char) buffer.get()); // c
  System.out.println((char) buffer.get()); // d

  // 将 position reset 到 mark 的位置
  buffer.reset();
  System.out.println((char) buffer.get()); // c
  System.out.println((char) buffer.get()); // d
}

可以看到的是 ,我们在 position 等于 2 的时候,调用了 mark 记住了 position 的位置。然后遍历完了所有的数据。然后调用 reset 使得 position 回到了 2 的位置,我们继续调用 get ,c d 就又可以被打印出来了。

clear

clear 表面意思看起来是将 buffer 清空的意思,但其实不是,看这个:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'a', 'b', 'c', 'd'});
}

put 完之后,buffer 的情况是这样的。

当我们调用完 clear 之后,buffer 就会变成这样。

所以,你可以理解为,调用 clear 之后只是切换到了写模式,因为这个时候往里面写数据,会覆盖之前写的数据,相当于起到了 clear 作用,再举个例子:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[]{'a', 'b', 'c', 'd'});
  buffer.clear();
  buffer.put(new byte[]{'s','h'});
}

可以看到,运行完之后 buffer 的数据变成了 shcd,后写入的数据将之前的数据给覆盖掉了。

除了 clear 可以切换到写模式之外,还有另一个方法可以切换,这就是本篇要讲的最后一个方法 compact

compact

先一句话给出 compact 的作用:将还没有读完的数据挪到 Buffer 的首部,并切换到写模式,代码如下:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put("abcd".getBytes(StandardCharsets.UTF_8));

  // 切换到读模式
  buffer.flip();
  System.out.println((char) buffer.get()); // a

  // 将没读过的数据, 移到 buffer 的首部
  buffer.compact(); // 此时 buffer 的数据就会变成 bcdd
}

当运行完 flip 之后,buffer 的状态应该没什么问题了:

而 compact 之后发生了什么呢?简单来说就两件事:

  1. 将 position 移动至对应的位置
  2. 将没有读过的数据移动到 buffer 的首部

这个对应是啥呢?先给大家举例子;例如没有读的数据是 bcd,那么 position 就为 3;如果没有读的数据为 cdposition 就为 2。所以你发现了,position 的值为没有读过的数据的长度

从 buffer 内部实现机制来看,凡是在 position - limit 这个区间内的,都算没有读过的数据

所以,当运行完 compact 之后,buffer 长这样:

limit 为 16 是因为 compact 使 buffer 进入了所谓的 写模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/281314.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【基础】【Python网络爬虫】【2.请求与响应】常用请求报头和常用响应方法

Python网络爬虫基础 爬虫基础请求与相应HTTP/HTTPS 协议HTTP/HTTPS的优缺点HTTP 的缺点HTTPS的优点 请求与响应概述请求请求目标(url)请求体(response)常用的请求报头查看请求体(requests 模块) 响应HTTP响…

微服务(10)

目录 46.k8s中镜像的下载策略是什么? 47.image的状态有哪些? 48.如何控制滚动更新过程? 49.DaemonSet资源对象的特性? 50.说说你对Job这种资源对象的了解? 46.k8s中镜像的下载策略是什么? 可通过命令k…

报错大全(未完待续)

springboot Could not find artifact org.springframework.boot:spring-boot-maven-plugin 报错环境:昨天的springboot项目的pom文件正常,今天再打开就会有些依赖爆红 解决步骤: 去maven的仓库里找你下载的依赖文件,路径是你的…

证明:切线垂直于半径

证明: 切线垂直于过切点的半径。 下面是网上最简单的证明方法。 证明: 利用反证法。 如下图所示,直线AB和圆O切于点A,假设OA 不垂直于 AB,而 O B ⊥ A B OB \perp AB OB⊥AB,则 ∠ O B A 90 \angle OB…

感染了后缀为.[sqlback@memeware.net].2700勒索病毒如何应对?数据能够恢复吗?

导言: 近期,[sqlbackmemeware.net].2700 勒索病毒成为网络安全的一大威胁。该勒索病毒采用高度复杂的加密算法,将用户文件加密并勒索赎金。了解该病毒的特征对于有效恢复被加密数据以及预防进一步感染至关重要。如果受感染的数据确实有恢复的…

Spring Cloud Gateway + Nacos 实现动态路由

1、maven 依赖 主要依赖 <!-- 网关 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency>案件差不多完整主要依赖 <!--Spring boot 依赖(微服务基…

每日一题——LeetCode961

方法一 排序法&#xff1a; 2*n长度的数组里面有一个元素重复了n次&#xff0c;那么将数组排序&#xff0c;求出排序后数组的中间值&#xff08;因为长度是偶数&#xff0c;没有刚好的中间值&#xff0c;默认求的中间值是偏左边的那个&#xff09;那么共有三种情况&#xff1a;…

Java时间工具类LocalTime

Java时间工具类LocalTime 嘚吧嘚LocalTime - API获取时间获取时分秒增减时分秒替换时间比较时间 嘚吧嘚 本篇文章整理一下有关LocalTime工具类的用法。个人感觉单独使用LocalTime类的几率不大&#x1f643;&#xff0c;使用频率最高的应该是LocalDateTime&#xff0c;但是既然…

变量的存储类型

auto类型 简述&#xff1a;c和c中的区别 auto类型表示自动存储类型 : 就是系统自动分配内存,使用结束后系统回收。 其实auto类型就是我们平时定义的局部变量&#xff0c;所以我们定义一个auto类型的变量&#xff0c;加auto都可以。 { auto int a 10; } 上面这行代码,…

Java EE Servlet之Servlet API详解

文章目录 1. HttpServlet1.1 核心方法 2. HttpServletRequest3. HttpServletResponse 接下来我们来学习 Servlet API 里面的详细情况 1. HttpServlet 写一个 Servlet 代码&#xff0c;都是要继承这个类&#xff0c;重写里面的方法 Servlet 这里的代码&#xff0c;只需要继承…

CSS与JavaScript的简单认识

CSS&#xff1a;是一门语言&#xff0c;用于控制网页表现&#xff0c;让页面更好看的。 CSS&#xff08;Cascading Style Sheet&#xff09;&#xff1a;层叠样式表 CSS与html结合的三种方式&#xff1a; 1、内部样式&#xff1a;用style标签&#xff0c;在标签内部定义CSS样式…

DevOps持续交付之容器化CICD流水线

DevOps持续交付 随着DevOps⼤规模化的落地和应⽤&#xff0c;持续集成以及持续交付已经是⼀种常态的。CI指的是持续集成&#xff0c;使⽤的开源⼯具是Jenkins&#xff0c;CD指的是持续交付和持续部署&#xff0c;⼀个完整的软件开发⽣命周期为: 主要流程可以具体为: 构建阶段…

12.26

key_it.c #include"key_it.h" void led_init() {// 设置GPIOE/GPIOF时钟使能RCC->MP_AHB4ENSETR | (0x3 << 4);// 设置PE10/PE8/PF10为输出模式GPIOE->MODER & (~(0x3 << 20));GPIOE->MODER | (0x1 << 20);GPIOE->MODER & (~…

如何在iterm2的命令行中快速移动

文章目录 一、打开Preferences设置二、进行key的映射三、修改键值四、参考文献 一、打开Preferences设置 二、进行key的映射 三、修改键值 四、参考文献 Mac下iTerm2光标按照单词快速移动设置

【回顾 展望】我的2023年度总结

2023年度关键词 学生时代终将绕不开的学习我的健身水平何时能重回巅峰&#xff1f;对生活前所未有的渴望接下来的2024是否能够微笑面对 最近发现了一个比较有意思的事&#xff0c;各大APP都在推我在这个APP的使用方面的2023年度总结&#xff0c;各个up主&#xff0c;也都在发各…

开发知识点-JAVA图形化-JavaFX

JavaFX JavaFX通用漏洞利用工具开发从无到无环境配置vscode配置JavaFX JavaFX基本程序结构创建项目新建一个包新建入口类&#xff08;主类&#xff09;新建&#xff08;FX&#xff09;label标签 创建按钮 JavaFX应用的 Stage窗口JavaFX应用的 Scene 场景类Node UI控件的通用属性…

纯CSS3制作优惠券线性UI效果

纯CSS3制作优惠券线性UI效果-遇见你与你分享

自激振荡电路笔记 电弧打火机

三极管相关 三极管的形象描述 二极管 简单求解&#xff08;理想&#xff09; 优先导通&#xff08;理想&#xff09; 恒压降 稳压管&#xff08;二极管plus&#xff09; 基础工作模块 理想稳压管的工作特性 晶体管之三极管(“两个二极管的组合” ) 电弧打火机电路 1.闭合开…

nodejs微信小程序+python+PHP的冷链物流配送系统-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

推荐几个开源HTTP服务接口快速生成工具

在现在流行微服务、前后端分离软件开发架构下&#xff0c;基于标准RESTful/JSON的HTTP接口已经成为主流。在实际业务中有很多需要快速开发调用数据服务接口的需求&#xff0c;但团队中缺乏专业的后端开发人员&#xff0c;比如&#xff1a; &#xff08;1&#xff09;数据库表已…