Buffer 类是 java.nio 的构造基础。一个 Buffer 对象是固定数量的数据的容器,其做用是一个存储器,或者分段运输区,在这里,数据可被存储并在以后用于检索。缓冲区能够被写满或释放。对于每一个非布尔原始数据类型都有一个缓冲区类,即 Buffer 的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是没有 BooleanBuffer 之说的。尽管缓冲区做用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区能够在后台执行从字节或到字节的转换,这取决于缓冲区是如何建立的。
◇ 缓冲区的四个属性
全部的缓冲区都具备四个属性来提供关于其所包含的数据元素的信息,这四个属性尽管简单,但其相当重要,需熟记于心: javascript
请切记,在使用 Buffer 时,咱们实际操做的就是这四个属性的值。咱们发现,Buffer 类并无包括 get() 或 put() 函数。可是,每个Buffer 的子类都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每一个子类来讲都是惟一的,因此它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所听从。若不加特殊说明,咱们在下面讨论的一些内容,都是以 ByteBuffer 为例,固然,它固然有 get() 和 put() 方法了。
◇ 相对存取和绝对存取 java
Java代码 api
来看看上面的代码,有不带索引参数的方法和带索引参数的方法。不带索引的 get 和 put,这些调用执行完后,position 的值会自动前进。固然,对于 put,若是调用屡次致使位置超出上界(注意,是 limit 而不是 capacity),则会抛出 BufferOverflowException 异常;对于 get,若是位置不小于上界(一样是 limit 而不是 capacity),则会抛出 BufferUnderflowException 异常。这种不带索引参数的方法,称为相对存取,相对存取会自动影响缓冲区的位置属性。带索引参数的方法,称为绝对存取,绝对存储不会影响缓冲区的位置属性,但若是你提供的索引值超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。
◇ 翻转
咱们把 hello 这个串经过 put 存入一 ByteBuffer 中,以下所示:将 hello 存入 ByteBuffer 中 数组
Java代码 缓存
此时,position = 6,limit = capacity = 1024。如今咱们要从正确的位置从 buffer 读数据,咱们能够把 position 置为 0,那么字符串的结束位置在哪呢?这里上界该出场了。若是把上界设置成当前 position 的位置,即 6,那么 limit 就是结束的位置。上界属性指明了缓冲区有效内容的末端。人工实现翻转: 安全
Java代码 网络
但这种从填充到释放状态的缓冲区翻转是API设计者预先设计好的,他们为咱们提供了一个很是便利的函数:buffer.flip()。另外,rewind() 函数与 flip() 类似,但不影响上界属性,它只是将位置值设回 0。
◇ 释放(Drain)
这里的释放,指的是缓冲区经过 put 填充数据后,而后被读出的过程。上面讲了,要读数据,首先得翻转。那么怎么读呢?hasRemaining() 会在释放缓冲区时告诉你是否已经达到缓冲区的上界: 多线程
Java代码 app
很明显,上面的代码,每次都要判断元素是否到达上界。咱们能够作:改变后的释放过程 函数
Java代码
第二段代码看起来很高效,但请注意,缓冲区并非多线程安全的。若是你想以多线程同时存取特定的缓冲区,你须要在存取缓冲区以前进行同步。所以,使用第二段代码的前提是,你对缓冲区有专门的控制。
◇ buffer.clear()
clear() 函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将 limit 设为容量的值,并把 position 设回 0。
◇ Compact(不知咋翻译,压缩?紧凑?)
有时候,咱们只想释放出一部分数据,即只读取部分数据。固然,你能够把 postion 指向你要读取的第一个数据的位置,将 limit 设置成最后一个元素的位置 + 1。可是,一旦缓冲区对象完成填充并释放,它就能够被从新使用了。因此,缓冲区一旦被读取出来,已经没有使用价值了。
以 Mellow 为例,填充后为 Mellow,但若是咱们仅仅想读取 llow。读取完后,缓冲区就能够从新使用了。Me 这两个位置对于咱们而言是没用的。咱们能够将 llow 复制至 0 - 3 上,Me 则被冲掉。可是 4 和 5 仍然为 o 和 w。这个事咱们固然能够自行经过 get 和 put 来完成,但 api 给咱们提供了一个 compact() 的函数,此函数比咱们本身使用 get 和 put 要高效的多。
Compact 以前的缓冲区
buffer.compact() 会使缓冲区的状态图以下图所示:
Compact 以后的缓冲区
这里发生了几件事:
◇ 缓冲区的比较
有时候比较两个缓冲区所包含的数据是颇有必要的。全部的缓冲区都提供了一个常规的 equals() 函数用以测试两个缓冲区的是否相等,以及一个 compareTo() 函数用以比较缓冲区。
两个缓冲区被认为相等的充要条件是:
两个被认为是相等的缓冲区
两个被认为是不相等的缓冲区
缓冲区也支持用 compareTo() 函数以词典顺序进行比较,固然,这是全部的缓冲区实现了 java.lang.Comparable 语义化的接口。这也意味着缓冲区数组能够经过调用 java.util.Arrays.sort() 函数按照它们的内容进行排序。
与 equals() 类似,compareTo() 不容许不一样对象间进行比较。但 compareTo()更为严格:若是你传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals() 只会返回 false。
比较是针对每一个缓冲区你剩余数据(从 position 到 limit)进行的,与它们在 equals() 中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。若是一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。这里有个顺序问题:下面小于零的结果(表达式的值为 true)的含义是 buffer2 < buffer1。切记,这表明的并非 buffer1 < buffer2。
Java代码
◇ 批量移动
缓冲区的设计目的就是为了可以高效传输数据,一次移动一个数据元素并不高效。如你在下面的程序清单中所看到的那样,buffer API 提供了向缓冲区你外批量移动数据元素的函数:
Java代码
如你在上面的程序清单中所看到的那样,buffer API 提供了向缓冲区内外批量移动数据元素的函数。以 get 为例,它将缓冲区中的内容复制到指定的数组中,固然是从 position 开始咯。第二种形式使用 offset 和 length 参数来指定复制到目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,可是这些方法可能高效得多,由于这种缓冲区实现可以利用本地代码或其余的优化来移动数据。
批量移动老是具备指定的长度。也就是说,你老是要求移动固定数量的数据元素。所以,get(dist) 和 get(dist, 0, dist.length) 是等价的。
对于如下几种状况的数据复制会发生异常:
若是缓冲区存有比数组能容纳的数量更多的数据,你能够重复利用以下代码进行读取:
Java代码
put() 的批量版本工做方式类似,只不过它是将数组里的元素写入 buffer 中而已,这里再也不赘述。
◇ 建立缓冲区
Buffer 的七种子类,没有一种可以直接实例化,它们都是抽象类,可是都包含静态工厂方法来建立相应类的新实例。这部分讨论中,将以 CharBuffer 类为例,对于其它六种主要的缓冲区类也是适用的。下面是建立一个缓冲区的关键函数,对全部的缓冲区类通用(要按照须要替换类名):
Java代码
新的缓冲区是由分配(allocate)或包装(wrap)操做建立的。分配(allocate)操做建立一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。包装(wrap)操做建立一个缓冲区对象可是不分配任何空间来储存数据元素。它使用你所提供的数组做为存储空间来储存缓冲区中的数据元素。demos:
Java代码
经过 allocate() 或者 wrap() 函数建立的缓冲区一般都是间接的。间接的缓冲区使用备份数组,你能够经过上面列出的 api 函数得到对这些数组的存取权。
boolean 型函数 hasArray() 告诉你这个缓冲区是否有一个可存取的备份数组。若是这个函数的返回 true,array() 函数会返回这个缓冲区对象所使用的数组存储空间的引用。若是 hasArray() 函数返回 false,不要调用 array() 函数或者 arrayOffset() 函数。若是你这样作了你会获得一个 UnsupportedOperationException 异常。
若是一个缓冲区是只读的,它的备份数组将会是超出 limit 的,即便一个数组对象被提供给 wrap() 函数。调用 array() 函数或 arrayOffset() 会抛出一个 ReadOnlyBufferException 异常以阻止你获得存取权来修改只读缓冲区的内容。若是你经过其它的方式得到了对备份数组的存取权限,对这个数组的修改也会直接影响到这个只读缓冲区。
arrayOffset(),返回缓冲区数据在数组中存储的开始位置的偏移量(从数组头 0 开始计算)。若是你使用了带有三个参数的版本的 wrap() 函数来建立一个缓冲区,对于这个缓冲区,arrayOffset() 会一直返回 0。不理解吗?offset 和 length 只是指示了当前的 position 和 limit,是一个瞬间值,能够经过 clear() 来从 0 从新存数据,因此 arrayOffset() 返回的是 0。固然,若是你切分(slice() 函数)了由一个数组提供存储的缓冲区,获得的缓冲区可能会有一个非 0 的数组偏移量。
◇ 复制缓冲区
缓冲区不限于管理数组中的外部数据,它们也能管理其余缓冲区中的外部数据。当一个管理其余缓冲器所包含的数据元素的缓冲器被建立时,这个缓冲器被称为视图缓冲器。
视图存储器老是经过调用已存在的存储器实例中的函数来建立。使用已存在的存储器实例中的工厂方法意味着视图对象为原始存储器的你部实现细节私有。数据元素能够直接存取,不管它们是存储在数组中仍是以一些其余的方式,而不需通过原始缓冲区对象的 get()/put() API。若是原始缓冲区是直接缓冲区,该缓冲区(视图缓冲区)的视图会具备一样的效率优点。
继续以 CharBuffer 为例,但一样的操做可被用于任何基本的缓冲区类型。用于复制缓冲区的 api:
Java代码
● duplidate()
复制一个缓冲区会建立一个新的 Buffer 对象,但并不复制数据。原始缓冲区和副本都会操做一样的数据元素。
duplicate() 函数建立了一个与原始缓冲区类似的新缓冲区。两个缓冲区共享数据元素,拥有一样的容量,但每一个缓冲区拥有各自的 position、limit 和 mark 属性。对一个缓冲区你的数据元素所作的改变会反映在另一个缓冲区上。这一副本缓冲区具备与原始缓冲区一样的数据视图。若是原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。duplicate() 复制缓冲区:
Java代码
复制一个缓冲区
● asReadOnlyBuffer()
asReadOnlyBuffer() 函数来生成一个只读的缓冲区视图。这与duplicate() 相同,除了这个新的缓冲区不容许使用 put(),而且其 isReadOnly() 函数将会返回 true。
若是一个只读的缓冲区与一个可写的缓冲区共享数据,或者有包装好的备份数组,那么对这个可写的缓冲区或直接对这个数组的改变将反映在全部关联的缓冲区上,包括只读缓冲区。
● slice()
分割缓冲区与复制类似,但 slice() 建立一个从原始缓冲区的当前 position 开始的新缓冲区,而且其容量是原始缓冲区的剩余元素数量(limit - position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。slice() 分割缓冲区:
Java代码
建立分割缓冲区
◇ 字节缓冲区(ByteBuffer)
ByteBuffer 只是 Buffer 的一个子类,但字节缓冲区有字节的独特之处。字节缓冲区跟其余缓冲区类型最明显的不一样在于,它能够成为通道所执行的 I/O 的源头或目标,后面你会发现通道只接收 ByteBuffer 做为参数。
字节是操做系统及其 I/O 设备使用的基本数据类型。当在 JVM 和操做系统间传递数据时,将其余的数据类型拆分红构成它们的字节是十分必要的,系统层次的 I/O 面向字节的性质能够在整个缓冲区的设计以及它们互相配合的服务中感觉到。同时,操做系统是在内存区域中进行 I/O 操做。这些内存区域,就操做系统方面而言,是相连的字节序列。因而,毫无疑问,只有字节缓冲区有资格参与 I/O 操做。
非字节类型的基本类型,除了布尔型都是由组合在一块儿的几个字节组成的。那么必然要引出另一个问题:字节顺序。
多字节数值被存储在内存中的方式通常被称为 endian-ness(字节顺序)。若是数字数值的最高字节 - big end(大端),位于低位地址(即 big end 先写入内存,先写入的内存的地址是低位的,后写入内存的地址是高位的),那么系统就是大端字节顺序。若是最低字节最早保存在内存中,那么系统就是小端字节顺序。在 java.nio 中,字节顺序由 ByteOrder 类封装:
Java代码
ByteOrder 类定义了决定从缓冲区中存储或检索多字节数值时使用哪一字节顺序的常量。若是你须要知道 JVM 运行的硬件平台的固有字节顺序,请调用静态类函数 nativeOrder()。
每一个缓冲区类都具备一个可以经过调用 order() 查询的当前字节顺序:
Java代码
这个函数从 ByteOrder 返回两个常量之一。对于除了 ByteBuffer 以外的其余缓冲区类,字节顺序是一个只读属性,而且可能根据缓冲区的创建方式而采用不一样的值。除了 ByteBuffer,其余经过 allocate() 或 wrap() 一个数组所建立的缓冲区将从 order() 返回与 ByteOrder.nativeOrder() 相同的数值。这是由于包含在缓冲区中的元素在 JVM 中将会被做为基本数据直接存取。
ByteBuffer 类有所不一样:默认字节顺序老是 ByteBuffer.BIG_ENDIAN,不管系统的固有字节顺序是什么。Java 的默认字节顺序是大端字节顺序,这容许类文件等以及串行化的对象能够在任何 JVM 中工做。若是固有硬件字节顺序是小端,这会有性能隐患。在使用固有硬件字节顺序时,将 ByteBuffer 的内容看成其余数据类型存取极可能高效得多。
为何 ByteBuffer 类须要一个字节顺序?字节不就是字节吗?ByteBuffer 对象像其余基本数据类型同样,具备大量便利的函数用于获取和存放缓冲区内容。这些函数对字节进行编码或解码的方式取决于 ByteBuffer 当前字节顺序的设定。ByteBuffer 的字节顺序能够随时经过调用以 ByteOrder.BIG_ENDIAN 或 ByteOrder.LITTL_ENDIAN 为参数的 order() 函数来改变:
Java代码
若是一个缓冲区被建立为一个 ByteBuffer 对象的视图,,那么 order() 返回的数值就是视图被建立时其建立源头的 ByteBuffer 的字节顺序。视图的字节顺序设定在建立后不能被改变,并且若是原始的字节缓冲区的字节顺序在以后被改变,它也不会受到影响。
◇ 直接缓冲区
内核空间(与之相对的是用户空间,如 JVM)是操做系统所在区域,它能与设备控制器(硬件)通信,控制着用户区域进程(如 JVM)的运行状态。最重要的是,全部的 I/O 都直接(物理内存)或间接(虚拟内存)经过内核空间。
当进程(如 JVM)请求 I/O 操做的时候,它执行一个系统调用将控制权移交给内核。当内核以这种方式被调用,它随即采起任何须要步骤,找到进程所需数据,并把数据传送到用户空间你的指定缓冲区。内核试图对数据进行高速缓存或预读取,所以进程所需数据可能已经在内核空间里了。若是是这样,该数据只需简单地拷贝出来便可。若是数据不在内核空间,则进程被挂起,内核着手把数据读进内存。
I/O 缓冲区操做简图
从图中你可能会以为,把数据从内核空间拷贝到用户空间彷佛有些多余。为何不直接让磁盘控制器把数据送到用户空间的缓冲区呢?首先,硬件一般不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操做的是固定大小的数据块,而用户进程请求的多是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程当中,内核负责数据的分解、再组合工做,所以充当着中间人的角色。
所以,操做系统是在内存区域中进行 I/O 操做。这些内存区域,就操做系统方面而言,是相连的字节序列,这也意味着I/O操做的目标内存区域必须是连续的字节序列。在 JVM中,字节数组可能不会在内存中连续存储(由于 JAVA 有 GC 机制),或者无用存储单元(会被垃圾回收)收集可能随时对其进行移动。
出于这个缘由,引入了直接缓冲区的概念。直接字节缓冲区一般是 I/O 操做最好的选择。非直接字节缓冲区(即经过 allocate() 或 wrap() 建立的缓冲区)能够被传递给通道,可是这样可能致使性能损耗。一般非直接缓冲不可能成为一个本地 I/O 操做的目标。
若是你向一个通道中传递一个非直接 ByteBuffer 对象用于写入,通道可能会在每次调用中隐含地进行下面的操做:
这可能致使缓冲区在每一个 I/O 上复制并产生大量对象,而这种事都是咱们极力避免的。若是你仅仅为一次使用而建立了一个缓冲区,区别并非很明显。另外一方面,若是你将在一段高性能脚本中重复使用缓冲区,分配直接缓冲区并从新使用它们会使你游刃有余。
直接缓冲区可能比建立非直接缓冲区要花费更高的成本,它使用的内存是经过调用本地操做系统方面的代码分配的,绕过了标准 JVM 堆栈,不受垃圾回收支配,由于它们位于标准 JVM 堆栈以外。
直接 ByteBuffer 是经过调用具备所需容量的 ByteBuffer.allocateDirect() 函数产生的。注意,wrap() 函数所建立的被包装的缓冲区老是非直接的。与直接缓冲区相关的 api:
Java代码
全部的缓冲区都提供了一个叫作 isDirect() 的 boolean 函数,来测试特定缓冲区是否为直接缓冲区。可是,ByteBuffer 是惟一能够被分配成直接缓冲区的 Buffer。尽管如此,若是基础缓冲区是一个直接 ByteBuffer,对于非字节视图缓冲区,isDirect() 能够是 true。
◇ 视图缓冲区
I/O 基本上能够归结成组字节数据的四处传递,在进行大数据量的 I/O 操做时,很又可能你会使用各类 ByteBuffer 类去读取文件内容,接收来自网络链接的数据,等等。ByteBuffer 类提供了丰富的 API 来建立视图缓冲区。
视图缓冲区经过已存在的缓冲区对象实例的工厂方法来建立。这种视图对象维护它本身的属性,容量,位置,上界和标记,可是和原来的缓冲区共享数据元素。
每个工厂方法都在原有的 ByteBuffer 对象上建立一个视图缓冲区。调用其中的任何一个方法都会建立对应的缓冲区类型,这个缓冲区是基础缓冲区的一个切分,由基础缓冲区的位置和上界决定。新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型中组成一个数据类型的字节数,在切分中任一个超过上界的元素对于这个视图缓冲区都是不可见的。视图缓冲区的第一个元素从建立它的 ByteBuffer 对象的位置开始(positon() 函数的返回值)。来自 ByteBuffer 建立视图缓冲区的工厂方法:
Java代码
下面的代码建立了一个 ByteBuffer 缓冲区的 CharBuffer 视图。演示 7 个字节的 ByteBuffer 的 CharBuffer 视图:
Java代码
7 个 字节的 ByteBuffer 的 CharBuffer 视图
◇ 数据元素视图
ByteBuffer 类为每一种原始数据类型提供了存取的和转化的方法:
Java代码
这些函数从当前位置开始存取 ByteBuffer 的字节数据,就好像一个数据元素被存储在那里同样。根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成须要的原始数据类型。
若是 getInt() 函数被调用,从当前的位置开始的四个字节会被包装成一个 int 类型的变量而后做为函数的返回值返回。实际的返回值取决于缓冲区的当前的比特排序(byte-order)设置。不一样字节顺序取得的值是不一样的:
Java代码
若是你试图获取的原始类型须要比缓冲区中存在的字节数更多的字节,会抛出 BufferUnderflowException。