1.简介

Java NIO 相关类在 JDK 1.4 中被引入,用于提高 I/O 的效率。Java NIO 包含了很多东西,但核心的东西不外乎 Buffer、Channel 和 Selector。本文中,我们先来聊聊的 Buffer 的实现。Channel 和 Selector 将在随后的文章中讲到。

2.继承体系

Buffer 的继承类比较多,用于存储各种类型的数据。包括 ByteBuffer、CharBuffer、IntBuffer、FloatBuffer 等等。这其中,ByteBuffer 最为常用。所以接下来将会主要分析 ByteBuffer 的实现。Buffer 的继承体系图如下:

3.属性及相关操作

Buffer 本质就是一个数组,只不过在数组的基础上进行适当的封装,方便使用。 Buffer 中有几个重要的属性,通过这几个属性来显示数据存储的信息。这个属性分别是:

属性 说明
capacity 容量 Buffer 所能容纳数据元素的最大数量,也就是底层数组的容量值。在创建时被指定,不可更改。
position 位置 下一个被读或被写的位置
limit 上界 可供读写的最大位置,用于限制 position,position < limit
mark 标记 位置标记,用于记录某一次的读写位置,可以通过 reset 重新回到这个位置

3.1 ByteBuffer 初始化

ByteBuffer 可通过 allocate、allocateDirect 和 wrap 等方法初始化,这里以 allocate 为例:

public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
} HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
} ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}

上面是 allocate 创建 ByteBuffer 的过程,ByteBuffer 是抽象类,所以实际上创建的是其子类 HeapByteBuffer。HeapByteBuffer 在构造方法里调用父类构造方法,将一些参数值传递给父类。最后父类再做一次中转,相关参数最终被传送到 Buffer 的构造方法中了。我们再来看一下 Buffer 的源码:

public abstract class Buffer {

    // Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity; Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
}

Buffer 创建完成后,底层数组的结构信息如下:

上面的几个属性作为公共属性,被放在了 Buffer 中,相关的操作方法也是封装在 Buffer 中。那么接下来,我们来看看这些方法吧。

3.2 ByteBuffer 读写操作

ByteBuffer 读写操作时通过 get 和 put 完成的,这两个方法都有重载,我们只看其中一个。

// 读操作
public byte get() {
return hb[ix(nextGetIndex())];
} final int nextGetIndex() {
if (position >= limit)
throw new BufferUnderflowException();
return position++;
} // 写操作
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
} final int nextPutIndex() {
if (position >= limit)
throw new BufferOverflowException();
return position++;
}

读写操作都会修改 position 的值,每次读写的位置是当前 position 的下一个位置。通过修改 position,我们可以读取指定位置的数据。当然,前提是 position < limit。Buffer 中提供了position(int) 方法用于修改 position 的值。

public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
if (mark > position) mark = -1;
return this;
}

当我们向一个刚初始化好的 Buffer 中写入一些数据时,数据存储示意图如下:

如果我们想读取刚刚写入的数据,就需要修改 position 的值。否则 position 将指向没有存储数据的空间上,读取空白空间是没意义的。如上图,我们可以将 position 设置为 0,这样就能从头读取刚刚写入的数据。

仅修改 position 的值是不够的,如果想正确读取刚刚写入的数据,还需修改 limit 的值,不然还是会读取到空白空间上的内容。我们将 limit 指向数据区域的尾部,即可避免这个问题。修改 limit 的值通过 limit(int) 方法进行。

public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}

修改后,数据存储示意图如下:

上面为了正确读取写入的数据,需要两步操作。Buffer 中提供了一个便利的方法,将这两步操作合二为一,即 flip 方法。

public final Buffer flip() {
// 1. 设置 limit 为当前位置
limit = position;
// 1. 设置 position 为0
position = 0;
mark = -1;
return this;
}

3.3 ByteBuffer 标记

我们在读取或写入的过程中,可以在感兴趣的位置打上一个标记,这样我们可以通过这个标记再次回到这个位置。Buffer 中,打标记的方法是 mark,回到标记位置的方法时 reset。简单看下源码吧。

public final Buffer mark() {
mark = position;
return this;
} public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}

打标记及回到标记位置的流程如下:

4.DirectByteBuffer

在 ByteBuffer 初始化一节中,我介绍了 ByteBuffer 的 allocate 方法,该方法实际上创建的是 HeapByteBuffer 对象。除了 allocate 方法,ByteBuffer 还有一个方法 allocateDirect。这个方法创建的是 DirectByteBuffer 对象。两者有什么区别呢?简单的说,allocate 方法所请求的空间是在 JVM 堆内进行分配的,而 allocateDirect 请求的空间则是在 JVM 堆外的,这部分空间不被 JVM 所管理。那么堆内空间和堆空间在使用上有什么不同呢?用一个表格列举一下吧。

空间类型 优点 缺点
堆内空间 分配速度快 JVM 整理内存空间时,堆内空间的位置会被搬动,比较笨重
堆外空间 1. 空间位置固定,不用担心空间被 JVM 随意搬动
2. 降低堆内空间的使用率
1. 分配速度慢
2. 回收策略比较复杂

DirectByteBuffer 牵涉的底层技术点比较多,想要弄懂,还需要好好打基础才行。由于本人目前能力很有限,关于 DirectByteBuffer 只能简单讲讲。待后续能力提高时,我会再来重写这部分的内容。如果想了解这方面的内容,建议大家看看其他的文章。

5.总结

Buffer 是 Java NIO 中一个重要的辅助类,使用比较频繁。在不熟悉 Buffer 的情况下,有时候很容易因为忘记调用 flip 或其他方法导致程序出错。不过好在 Buffer 的源码不难理解,大家可以自己看看,这样可以避免出现一些奇怪的错误。

好了,本文到这里就结束了,谢谢阅读!

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处

作者:coolblog

本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

Java NIO之缓冲区的更多相关文章

  1. Java NIO 之缓冲区

    缓冲区基础 所有的缓冲区都具有四个属性来 供关于其所包含的数据元素的信息. capacity(容量):缓冲区能够容纳数据的最大值,创建缓冲区后不能改变. limit(上界):缓冲区的第一个不能被读或写 ...

  2. Java NIO之缓冲区Buffer

    Java NIO的核心部件: Buffer Channel Selector Buffer 是一个数组,但具有内部状态.如下4个索引: capacity:总容量 position:下一个要读取/写入的 ...

  3. Java NIO——2 缓冲区

    一.缓冲区基础 1.缓冲区并不是多线程安全的. 2.属性(容量.上界.位置.标记) capacity limit  第一个不能被读或写的元素 position  下一个要被读或写的元素索引 mark ...

  4. Java NIO Buffer缓冲区

    原文链接:http://tutorials.jenkov.com/java-nio/buffers.html Java NIO Buffers用于和NIO Channel交互.正如你已经知道的,我们从 ...

  5. Java NIO ———— Buffer 缓冲区详解 入门

    引言缓冲区是一个用于特定基本类型的容器.由java.nio 包定义,所有缓冲区都是 Buffer 抽象类的子类. Java NIO 中的 Buffer ,主要用于与NIO 通道进行交互.数据从通道存入 ...

  6. Java NIO -- 直接缓冲区与非直接缓冲区

    直接缓冲区与非直接缓冲区: 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建 ...

  7. Java NIO流 -- 缓冲区(Buffer,ByteBuffer)

    用来定义缓冲区的所有类都以Buffer类为基类,Buffer定义了缓冲区的基本特征. 直接子类: ByteBuffer 用来存储byte类型的缓冲区,可以在这种缓冲区中存储任意其他基本类型的二进制值( ...

  8. 海纳百川而来的一篇相当全面的Java NIO教程

    目录 零.NIO包 一.Java NIO Channel通道 Channel的实现(Channel Implementations) Channel的基础示例(Basic Channel Exampl ...

  9. Java NIO:通道

    最近打算把Java网络编程相关的知识深入一下(IO.NIO.Socket编程.Netty) Java NIO主要需要理解缓冲区.通道.选择器三个核心概念,作为对Java I/O的补充, 以提升大批量数 ...

随机推荐

  1. Apache Shiro java安全框架

    什么是Apache Shiro? Apache Shiro(发音为“shee-roh”,日语“堡垒(Castle)”的意思)是一个强大易用的Java安全框架,提供了认证.授权.加密和会话管理功能,可为 ...

  2. CString(转)

    CString::Compare int Compare( LPCTSTR lpsz ) const; 返回值   字符串一样  返回0 小于lpsz  返回-1 大于lpsz  返回1 区分大小字符 ...

  3. find与tar的结合使用

    新建一个文件,自定义时间点[root@nhserver2 ~]# touch -t 1403010000.00 file1.txt   新建一个文件,自定义时间点[root@nhserver2 ~]# ...

  4. python selenium 鼠标悬停

    #鼠标悬停 chain = ActionChains(driver) implement = driver.find_element_by_link_text() chain.move_to_elem ...

  5. vs2012编译在win7 32位电脑和win xp电脑上运行的win32程序遇到的问题记录

    一.win7 32位电脑: vs2012编译的64位程序是没有问题的.但编译的32位程序在别的电脑(虚拟机模拟)出错: 感觉很无语,vs这么牛逼的东西,在设计时候都不考虑这些吗? 在自己电脑C:\Wi ...

  6. 02_Linux图形界面及文件系统结构介绍

    一.根目录 /       : 它跟Windows的C.D.E.F不同,在Linux中,所有的文件系统(光驱.U盘.硬盘)都挂载到根目录的某一个文件夹下 bin   : 存放二进制可执行文件 sbin ...

  7. window.print打印指定html元素中的内容

    通常有些时候我们项目过程中使用到打印功能,而wndow.print便是系统里提供的一个函数. 但是直接使用的话,它打印的将是整个页面的所有元素,而有些时候我们又只需要打印部分内容. <body& ...

  8. java之Servlet监听器Listener

    常用应用场景:单点登录.统计在线人数 一.简介 (一)概述 1.Listener 用于监听 java web程序中的事件,例如创建.修改.删除Session.request.context等,并触发响 ...

  9. python函数式编程之装饰器(一)

    1.开放封闭原则 简单来说,就是对扩展开放,对修改封闭 在面向对象的编程方式中,经常会定义各种函数. 一个函数的使用分为定义阶段和使用阶段,一个函数定义完成以后,可能会在很多位置被调用 这意味着如果函 ...

  10. Struts2的配置和一个简单的例子

    Struts2的配置和一个简单的例子 笔记仓库:https://github.com/nnngu/LearningNotes 简介 这篇文章主要讲如何在 IntelliJ IDEA 中使用 Strut ...