Buffer是入门Java NIO的基础,本文希望通过一些形象的比喻来解释一下缓冲区的概念,帮助读者快速理解和记忆。

本文灵感来自于Bilibili博主v若水若水分享的尚硅谷Java视频_NIO视频教程,有需要看视频学习的朋友可以在Bilibili上搜索Java NIO找到相关视频学习。

本文中的英文摘自java.nio中相关类的注释。

Buffer是什么?

A container for data of a specific primitive type.

Buffer是特定基本类型数据的容器。

  • 我愿意把Buffer比作 火车

Buffer能装什么?

  • Buffer能装int,float,char,double,short,long,byte
  • Buffer能装boolean
整数 浮点数 字符 字节数
byte(8位) 1字节
short(16位) char(16位) 2字节
int(32位) float(32位) 3字节
long(64位) double(64位) 4字节

Buffer的重要属性及对应的获取方法

the essential properties of a buffer are its capacity, limit, and position

  • capacity,容量。火车有 n 节车厢
  • position,位置。这里需要再假想一辆小车,行驶在火车旁边的站台上。position表示当前小车停留在第 n 节车厢前
  • limit,限制。火车的前 n 节车厢
属性 属性获取方法
capacity public final int capacity()
position public final int position()
limit public final int limit()

接下来的讨论都会紧紧围绕这三个属性

安排一列火车-allocate

以最常用的ByteBuffer为例:

/**
* 分配一个新的字节缓冲区。
* <p>新缓冲区的位置position将为零,其限制limit等于
* 容量capacity,其标记mark将是不确定的,并且其每个元素将是
* 初始化为零。 它将有一个{@link #array backing array},
*,其{@link #arrayOffset array offset}将为零。
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
  • 可以看到allocate方法会使用HeapXXXBuffer类来实例化。
  • allocate将分配一个指定容量的缓冲区。存储哪类基本数据类型,这取决于你使用哪类Buffer。

测试代码:

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}

控制台输出:

1024

1024

0

通俗解释:

安排一辆1024节车厢的火车。容量为1024节车厢,允许装货1024节车厢,现在小车停在第0节车厢上。

学过数组的都知道,计算机中,一个长度为1024的数组下标是从0到1023

装货-put

装单个"货"

测试用例1

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put((byte) 'a');
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}

控制台输出

1024

1024

1

通俗解释

把 a 装到火车的第 0 号车厢,成功装上之后,小车向前移了一个节车厢,停在了第 1 号车厢。

这里实际上装进ByteBuffer的是由 a 转化得到的 8 位二进制数(01100001)。

8 位二进制正好就是1个字节。

a 是怎么得到二进制数的?这个问题可以查看ASCII码表

测试用例2

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(11, (byte) 'a');
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}

控制台输出

1024

1024

0

结论

put(byte)会使得position发生改变,但是put(index, byte)不会使position发生改变

装一组"货"

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}

控制台输出

1024

1024

4

通俗解释

把[a,b,c,d]依次装到火车的第 0, 1, 2, 3 号车厢,现在车厢旁边的小车停在 4 号车厢前。

这里实际上装进ByteBuffer的是由 a,b,c,d 转化得到的二进制数。

结论

无论是put一个字节(byte)还是一个字节数组(byte[]),put成功后position都会向后移动。

装货时超载-BufferOverflowException

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.limit(2);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position()); buffer.put("abc".getBytes());
}

控制台输出

1024

2

0

Exception in thread "main" java.nio.BufferOverflowException

  at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:189)

  at java.nio.ByteBuffer.put(ByteBuffer.java:859)

  at nio.Main.main(Main.java:14)

通俗解释

虽然这列火车有1024节车厢,但是规定 只能装在前2节车厢内 。因此当你同时派来了 3 辆小车,分别装着 a,b,c,从 0 号车厢排列到 2 号车厢,当你想同时装货到火车上的时候,你被列车管理员拒绝了,理由是你超载了!因此,现在*** a,b,c全都没装上火车 ***。

代码

public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
} public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}

结论:全装or全部不给装

这里是先校验,后写入数据。要么全都正常写入,要么一个也不会写入。

可重复读-get

读单个

测试用例1

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put((byte) 'a');
System.out.println(buffer.get(0));
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.position());
}

控制台输出

97

1

97

1

结论

get(index)不会改变position的位置,且可以重复读,读取结果总是一样的。

测试用例2

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position()); byte[] bytes = new byte[2];
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position());
}

控制台输出

4

[0, 0]

6

通俗解释

在装货 [a, b, c, d] 之后,小车的位置已经移动到4号车厢前(第5节车厢),此时是不能从当前车厢读取到有效的货物信息的。另外,小车因为成功读取了2个车厢的货物信息(虽然结果为0),小车的位置已经发生了改变,目前小车移动到了6号车厢前(第7节车厢)。

如何让小车回到起始位置呢?

flip

/**
* 在执行一系列<i> put </ i>操作之后,调用此方法以准备一系列相对
* 的<i> get </ i>操作。
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}

rewind

/**
* 假设已正确设置了限制,请在执行一系列<i> get </ i>操作之前调用此方法。
*/
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}

clear

/**
* 在使用一系列<i> put </ i>操作填充此缓冲区之前,请调用此方法。
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}

结论

flip()rewind()相同点,都是在准备调用get之前调用。

flip()rewind()不同点,rewind需要用户事先确保limit正确。

clear()用于在准备重新调用put之前调用。

再探get

测试用例

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position()); byte[] bytes = new byte[2];
buffer.flip();
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position()); buffer.rewind();
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
System.out.println(buffer.position());
}

控制台输出

4

[97, 98]

2

[97, 98]

2

结论

在调用get(byte[])之前事先调用flip()则此次可以正常读取数据到数组中。调用rewind()则可以“倒带”(我愿意理解成把小车开回原点),又可以再次读取。

小车越界-BufferUnderflowException

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position()); byte[] bytes = new byte[6];
buffer.flip();
buffer.get(bytes);
}

控制台输出

Exception in thread "main" java.nio.BufferUnderflowException

  at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:151)

  at java.nio.ByteBuffer.get(ByteBuffer.java:715)

  at nio.Main.main(Main.java:15)

通俗解释

在调用flip()之后,列车管理员规定小车最多只能移到到4号车厢前面,因为只有0号到3号车厢装有货物可以读到货物信息,即只有4节车厢中是含有货物信息的,而小车却想获取6节车厢中货物信息,结果被列车管理员告知你越界了!

小车越界可不同于数组越界-IndexOutOfBoundsException

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes()); byte[] bytes = new byte[6];
buffer.flip();
buffer.get(bytes, 0, 7);
}

控制台输出

Exception in thread "main" java.lang.IndexOutOfBoundsException

  at java.nio.Buffer.checkBounds(Buffer.java:567)

  at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:149)

  at nio.Main.main(Main.java:14)

结论

数组越界是表示对于数组的操作就有问题,与Buffer中的属性关系不大。比如数组长度只有6,却想读取7个字节到数组中去,直接导致了数组越界。

标记和重置-mark&reset

Marking and resetting

A buffer's mark is the index to which its position will be reset when the {@link #reset reset} method is invoked. The mark is not always defined, but when it is defined it is never negative and is never greater than the position. If the mark is defined then it is discarded when the position or the limit is adjusted to a value smaller than the mark. If the mark is not defined then invoking the {@link #reset reset} method causes an {@link InvalidMarkException} to be thrown.

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.mark();
buffer.put("ab".getBytes());
System.out.println(buffer.position()); buffer.reset();
System.out.println(buffer.position()); byte[] bytes = new byte[2];
buffer.get(bytes);
System.out.println(Arrays.toString(bytes));
}

控制台输出

2

0

[97, 98]

通俗解释

在一开始,小车就在0号车厢做了个标记,然后在完成对0号车厢和1号车厢的装货后,一个重置,小车位置由2号车厢回到了事先标记好的0号车厢,然后就可以开始读了。!!注意:这不是一种安全的读方式,这里仅仅是示例作用。

拓展知识1:Buffer支持链式调用

测试代码

public static void main(String[] args) {
byte[] bytes = new byte[4]; byte result = ((ByteBuffer)ByteBuffer.allocate(1024)
.put("abcd".getBytes())
.flip())
.get(); System.out.println(result);
}

控制台输出

97

拓展知识2:clear不会清除buffer中原来存储的内容

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcd".getBytes());
System.out.println(buffer.position()); buffer.clear();
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.get(1));
System.out.println(buffer.get(2));
System.out.println(buffer.get(3));
}

控制台输出

4

0

97

98

99

100

拓展知识3:putChar('a')和put((byte)'a')有别

测试代码

public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putChar('a');
System.out.println(buffer.position());
System.out.println(buffer.get(0));
System.out.println(buffer.get(1)); buffer.put((byte)'a');
System.out.println(buffer.position());
System.out.println(buffer.get(2));
System.out.println(buffer.get(3));
}

控制台输出

2

0

97

3

97

0

结论

putChar('a')会放入2个字节到缓冲区,如果实际编码只有一个字节,那就会用0来表示前一个字节。而put((byte)'a')只会放入1个字节到缓冲区。你可以通过buffer.position()观察这一点。

JavaNIO第一话-Buffer的更多相关文章

  1. Android简易实战教程--第一话《最简单的计算器》

    转载请注明出处:http://blog.csdn.net/qq_32059827/article/details/51707931 从今天开始,本专栏持续更新Android简易实战类博客文章.和以往专 ...

  2. 漫话JavaScript与异步·第一话——异步:何处惹尘埃

    自JavaScript诞生之日起,频繁与异步打交道便是这门语言的使命,并为此衍生出了许多设计和理念.因此,深入理解异步的概念对于前端工程师来说极为重要. 什么是异步? 程序是分"块" ...

  3. 漫话JavaScript与异步·第二话——Promise:一诺千金

    一.难以掌控的回调 我在第一话中介绍了异步的概念.事件循环.以及JS编程中可能的3种异步情况(用户交互.I/O.定时器).在编写异步操作代码时,最直接.也是每个JSer最先接触的写法一定是回调函数(c ...

  4. 漫话JavaScript与异步·第三话——Generator:化异步为同步

    一.Promise并非完美 我在上一话中介绍了Promise,这种模式增强了事件订阅机制,很好地解决了控制反转带来的信任问题.硬编码回调执行顺序造成的"回调金字塔"问题,无疑大大提 ...

  5. java jvm学习笔记四(安全管理器)

    欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一 ...

  6. 【转】漫谈ANN(2):BP神经网络

    上一次我们讲了M-P模型,它实际上就是对单个神经元的一种建模,还不足以模拟人脑神经系统的功能.由这些人工神经元构建出来的网络,才能够具有学习.联想.记忆和模式识别的能力.BP网络就是一种简单的人工神经 ...

  7. java之jvm学习笔记四(安全管理器)

    java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...

  8. 简明CSS属性:定位

    简明CSS属性:定位 第一话 定位 (Positioning) 关键词:position/z-index/top/bottom/right/left/clip POSITION 该属性用来决定元素在页 ...

  9. NIO的初步入门

    NIO java NIO简介 Java NIO 简介 是从java1.4版本开始引入的一个新的IO AP可以替代标准java  IO API NIO与原来的IO有同样的作用和目的,但是使用方式完全不同 ...

随机推荐

  1. 201771010135杨蓉庆 《面对对象程序设计(java)》第八周学习总结

    1.实验目的与要求 (1) 掌握接口定义方法: (2) 掌握实现接口类的定义要求: (3) 掌握实现了接口类的使用要求: (4) 掌握程序回调设计模式: (5) 掌握Comparator接口用法: ( ...

  2. ES5-bind用法及与以前的apply和call

    当我们调用一个函数的时候,函数中的this一般是指向调用者的.但是我们其实可以在调用函数的时候,传入一个对象,让函数中的this指向我们传入的对象,而不是调用者本身. apply,call,bind都 ...

  3. Python学习第二十课——自定property and classmethod

    自定制property class Lazyproperty: def __init__(self,func): # print('==========>',func) self.func=fu ...

  4. N-tier architecture N层架构 (转)

    下面的内容既有我的理解,也有翻译的内容,翻译的书名为: <<Expert C# 2008 Business Objects >>http://www.douban.com/su ...

  5. asp.net mvc Bundle

    在使用ASP.NET MVC4中使用BundleConfig 将 js css文件 合并压缩使用,但是文件名含有min及特殊字符的将不引用 ,也不提示其他信息.

  6. sourceTree的忽略文件

    在没有导入项目之前,就要把忽略配置好: *~ .DS_Store xcuserdata

  7. unity优化-GPU(网上整理)

    优化-GPUGPU与CPU不同,所以侧重点自然也不一样.GPU的瓶颈主要存在在如下的方面: 填充率,可以简单的理解为图形处理单元每秒渲染的像素数量.像素的复杂度,比如动态阴影,光照,复杂的shader ...

  8. shell脚本添加脚本执行时间和当前运行次数current running time

    #!/bin/bash ############################ #Author:Bing #Create time:3/31/2017 ####################### ...

  9. Java基础知识笔记第八章:常用的实体类

    String类 String类位于lang包下 java会默认导入lang包下的类,所以可以直接使用,注意String是final类所以不能有子类 构造String对象: 常量对象:String常量也 ...

  10. 夯实Java基础系列目录

    自进入大学以来,学习的编程语言从最初的C语言.C++,到后来的Java,. NET.而在学习编程语言的同时也逐渐决定了以后自己要学习的是哪一门语言(Java).到现在为止,学习Java语言也有很长一段 ...