前面翻译过一篇类似的文章,发现讲的不够透彻,这篇是一篇扩展型的文章。
本文注意结合buffer类的API解析buffer内部的机制,并且只介绍读写,其他的请参考buffer的原代码,自己可以进行分析【可能后续也有相关的补充】,本文使用的实现类为ByteBuffer 。
也可以看看我翻译blog了解下buffer的基本东西
http://xianglp.iteye.com/blog/1842063
缓冲区是能原始数据类型的数组包装对象,Buffer类比数组的最大好处为它将数据的信息和数据内容封装到唯一的对象中。Buffer类和它的实现子类定义了处理数据缓冲区的API。下图为buffer类的集成及buffer类的实现。
(图一:buffer类继承图)
属性
每一种Java基本类型的缓冲区都是抽象类Buffer的子类,从Buffer的源代码中可以发现,它定义了四个私有属性。 四个私有属性用于处理缓冲区的数据。buffer类中定义如下:
private int mark = -1; private int position = 0; private int limit; private int capacity;
Capacity(容量):
缓冲区能装载的最大数据量,缓冲区被创建时被设置,并不会被更改。
Position(位置):
被读取或被写入的下一个元素的索引,调用get( ) 和 put( )方法时会关联的更新position,换句话说,写入buffer时,Position代表buffer第一个未被写入元素的索引【即put下个元素存放的位置】,读取时,Position代表buffer代表第一个未被读取的索引【即get取时第一个元素位置】。
Limit(限制):
缓冲区中不能被读取或被写入的第一元素。换句话说,在缓冲区中可以使用的元素个数,写入buffer时,Limit为Capacity即能写入buffer最多元素个数,读取buffer是Limit为buffer中元素的个数即写入状态是Position的值。
Mark(标记):
被记录的位置,调用mark( )方法时设置mark=position。调用reset( )设置position = mark,mark属性未被设置时,默认为-1;
四个属性的关系如下:
mark <= position <= limit <= capacity
创建buffer
使用的是实现类中的allocate方法。创建一个容量大小为10的ByteBuffer示例如下:
ByteBuffer readBuffer = ByteBuffer.allocate(10);
前面我们提到的各个属性情况怎么样呢,示例图显示如下:
(图二:创建buffer时存储结构示例图)
创建完毕buffer后可以被写入【即写模式】,position代表未被写入第一元素为0,被写入时limit 等于capacity为10代表缓冲区能写入的byte数,mark为-1(后面有原代码说明)。capacity为固定值10,其他三个属性在buffer被使用的能被修改。
mark属性的看下原代码,在buffer的allocate方法如下
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); }
HeapByteBuffer的构造方法如下
class HeapByteBuffer extends ByteBuffer { // For speed these fields are actually declared in X-Buffer; // these declarations are here as documentation /* protected final byte[] hb; protected final int offset; */ HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); /* hb = new byte[cap]; offset = 0; */ }
super为 ByteBuffer,super的各个值属性如下,
ByteBuffer(int mark, int pos, int lim, int cap, // package-private byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; }
从这里我们可以发现mark的被设置为-1。
上面的源码我们发现buffer实际是数组的包装,实际数据存储对象为一个数组,byteBuffer的构造函数中 new byte[cap],创建的就是一个byte的数组。
Accessing(访问,读写buffer)
API如下:
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public abstract byte get( ); public abstract byte get (int index); public abstract ByteBuffer put (byte b); public abstract ByteBuffer put (int index, byte b); }
填充:
ByteBuffer在子类HeapByteBuffer有put方法的实现如下
public ByteBuffer put(byte x) { hb[ix(nextPutIndex())] = x; return this; } //获取写入索引位置 final int nextPutIndex() { // package-private //写入范围校验,必须为position和limit之间 if (position >= limit) throw new BufferOverflowException(); //先将position返回,position再自加1 return position++; }
从代码中我们可以看出put就是数据放置到数组对应的position位置,然后position=position+1指向下个空闲位置。从这里我们也看可以看出put值放入的范围为position位置到limit之间。
示例:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l') .put((byte)'o');
各属性值变化如下
(图三:写入buffer时存储结构示例图)
往里进行put时,只有position值发生变化,例如上面的填充了5个字节的大小,position值为5,指上buffer里面的第6个元素。
put (int index, byte b)可以让我们操作buffer中已经操作过位置的值。
buffer.put(0, (byte)'M').put((byte)'w');
变化后的图示如下
(图四:调用 put (int index, byte b)时存储结构示例图)
使用 put (int index, byte b)并不会影响到buffer的相关属性值的变化
Flipping(将读状态转换到写状态)
buffer被填充后,我们怎样将数据读出来呢,flip(),能将buffer由写模式切换至读模式。
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
其实flip主要是调整position位置为0,limit设置为position ,mark 设置为-1。
从这里我们可以看出如果执行了flip方法,直接往buffer里面写入值的话,整个buffer从0开始被重新,但是最多只能写入到limit,而如果执行读的话,我们从索引为0的开始读,最多读取limit个数据。
上个buffer执行flip后各属性状态变成为
(图五:flip方法执行后存储结构示例图)
从图可以看出buffer的position位置变为0,代表可以从0索引开始读取数据,limit设置为原来的position位置6,代表我能从索引位置0读取到5。
Draining(从buffer读取)
get方法是从buffer里面读取数据,在子类HeapByteBuffer有put方法的实现如下
public byte get() { return hb[ix(nextGetIndex())]; } //注意这里offset值为0 protected int ix(int i) { return i + offset; } //获取position值 final int nextGetIndex() { // package-private if (position >= limit) throw new BufferUnderflowException(); return position++; }
可以看出get方法获取的是position与limit之间的数据,每获取后position位置+1,即下个get索引所在位置,所以无论我们调用什么方法操作buffer后,最后使用get方法获取的都是position与limit之间的数据。
调用示例如下
buffer.flip(); System.out.println( (char)buffer.get() );
上面的buffer我们flip后调用get整个存储变成
(图六:调用get方法是buffer的存储示例图)
position指向下个索引位置即为1,其他属性不变化
clear(清空buffer)
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
清空比较明确,即调整position调整为0,limit 调整为capacity,mark调整为-1。我们的上面的示例buffer执行clear后各个属性情况如下
(图七:清空buffer时各属性的变化)
从这里我们可以看出,clear()并未实际的清空数据,而只是调整相关属性。
mark的使用
API如下:
//mark 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; }
从API中可以发现MARK实际就用来标记当前position位置,reset方法是将position重新设置为mark,这样我们就可以使用mark进行重复读取。但是为调用mark() 函数直接使用reset方法将抛出异常,因为我们的mark属性默认为-1。
完整的示例
package sample; import java.nio.ByteBuffer; public class BufferReadWrite { public static void main( String args[] ){ ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l') .put((byte)'o'); buffer.put(0, (byte)'M').put((byte)'w'); buffer.flip(); //获取能读取的数据个数limit - position int count = buffer.remaining( ); for (int i = 0; i < count; i++) { // System.out.println(buffer.limit()); // System.out.println(buffer.capacity()); // System.out.println(buffer.position()); System.out.println("读取前的:"+buffer.toString()); System.out.println( (char)buffer.get() ); System.out.println("读取后的:"+buffer.toString()); } } }
结果如下:
读取前的:java.nio.HeapByteBuffer[pos=0 lim=6 cap=10] M 读取后的:java.nio.HeapByteBuffer[pos=1 lim=6 cap=10] 读取前的:java.nio.HeapByteBuffer[pos=1 lim=6 cap=10] e 读取后的:java.nio.HeapByteBuffer[pos=2 lim=6 cap=10] 读取前的:java.nio.HeapByteBuffer[pos=2 lim=6 cap=10] l 读取后的:java.nio.HeapByteBuffer[pos=3 lim=6 cap=10] 读取前的:java.nio.HeapByteBuffer[pos=3 lim=6 cap=10] l 读取后的:java.nio.HeapByteBuffer[pos=4 lim=6 cap=10] 读取前的:java.nio.HeapByteBuffer[pos=4 lim=6 cap=10] o 读取后的:java.nio.HeapByteBuffer[pos=5 lim=6 cap=10] 读取前的:java.nio.HeapByteBuffer[pos=5 lim=6 cap=10] w 读取后的:java.nio.HeapByteBuffer[pos=6 lim=6 cap=10]
其他读写情况分析
(1)我们在写入buffer后,不是用flip方法,再进行get会出现什么情况呢代码如下:
//未使用flip方法 public void noflip( ){ ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l') .put((byte)'o'); buffer.put(0, (byte)'M').put((byte)'w'); int count = buffer.remaining( ); for (int i = 0; i < count; i++) { // System.out.println(buffer.limit()); // System.out.println(buffer.capacity()); // System.out.println(buffer.position()); System.out.println("读取前的:"+buffer.toString()); System.out.println( (char)buffer.get() ); System.out.println("读取后的:"+buffer.toString()); }
结果输出如下
从图四我们知道,buffer写入后,不执行flip方法,buffer的position=6,limit=10。我们知道buffer的读取是读取position与limit之间的数据,所以我们使用get方法时读取到的是该buffer未填充的数值。
2: put (int index, byte b)中index大于position的位置时,是什么样的情况,是否能读取到。
代码示例如下:
//放入的索引大于position的情况 public void putInedexbig( ){ ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l') .put((byte)'o'); buffer.put(8, (byte)'M'); int count = buffer.remaining( ); buffer.flip(); count = buffer.remaining( ); for (int i = 0; i < count; i++) { // System.out.println(buffer.limit()); // System.out.println(buffer.capacity()); // System.out.println(buffer.position()); System.out.println("filp后读取前的:"+buffer.toString()); System.out.println( (char)buffer.get() ); System.out.println("filp后读取后的:"+buffer.toString()); } }
结果我直接说了读取不到, put (int index, byte b)并未影响到position与limit的变化源代码如下:
public ByteBuffer put(int i, byte x) { hb[ix(checkIndex(i))] = x; return this; } final int checkIndex(int i) { // package-private if ((i < 0) || (i >= limit)) throw new IndexOutOfBoundsException(); return i; }
所以flip后position变成了0,limit为原来position即为5,因此put到index为8的记录无法获取。
注意的:
put( byte b )方法却会影响到position的变化,put (int index, byte b)并不影响到任何属性的变化。
get()方法会影响到position的变化, get(int i)不会影响到任何属性的变化。
总结:(1)buffer的实际存储对象为数组,buffer就是对数组操作的封装。
(2)buffer核心是四个属性【position,Limit,Capacity,mark】,相关的操作都是围绕着四个属性展开。
(3)buffer读取核心为position,Limit两个属性,写入时,写入索引开始位置为position,结束位置为Limit。读取是读取开始的索引位置为position,结束索引位置为Limit。
(4)buffer的清空不是实际的清空,而只是相关属性的调整。
(5)buffer的方法是非线程安全的,在高并发环境中注意同步。
相关推荐
NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。 1. **通道(Channel)**:在Java NIO中,通道代表了一个打开的I/O连接,如文件、套接字、网络流等。通道是双向的,数据可以从通道读取...
Java NIO的核心组件包括: - **Channels**:用于表示IO源或目标的一个连接点。 - **Buffers**:用来存储数据的对象,是数据传输的载体。 - **Selectors**:多路复用器,用于监控多个Channel的状态变化,支持高效地...
Java NIO的核心组件包括Channel(通道)、Buffer(缓冲区)和Selector(选择器)。 - **Channel**:通道用于连接源和目的地,支持数据的读写操作。与传统的流不同,通道支持双向通信,并且可以通过非阻塞的方式进行...
Java NIO提供了一种高效、灵活的文件操作方式,通过Buffer、Channel、Paths和Files等核心组件,可以实现复杂的文件读写操作。NIO的异步操作特性也使得它在处理大量并发文件操作时具有显著的性能优势。在实际开发中,...
根据提供的文件信息,本文将围绕Java NIO(New Input/Output)的相关知识点进行详细解析,同时探讨相关的视频学习资源。 ### Java NIO简介 Java NIO(New Input/Output),即新输入输出,是Java 1.4版本引入的一个...
"Java NIO Selector 机制解析" Java NIO(New I/O)类库是Java 1.4版本以后引入的新一代I/O机制,相比传统的I/O机制,NIO提供了高效、异步、多路复用的I/O操作模式。Selector机制是NIO类库中的一种核心机制,用于...
Java NIO(Non-blocking Input/Output)是一种在Java中处理I/O操作的新方式,相比于传统的BIO(Blocking I/O),NIO提供了更高效的数据传输能力,尤其适合于高并发、低延迟的网络应用,如聊天服务器。在这个场景下,...
NIO的核心概念包括通道(Channel)、缓冲区(Buffer)以及选择器(Selector)。 1. **通道(Channel)**:在Java NIO中,数据是通过通道进行传输的。通道是与I/O设备(如文件、网络套接字等)交互的接口。常见的...
选择器是Java NIO的核心组件,它允许单个线程监控多个通道的事件(如连接打开、数据到达、关闭等)。通过注册通道到选择器,并设置感兴趣的事件,当事件发生时,选择器会返回一个SelectionKey集,开发者可以通过这个...
为了高效地处理这类问题,我们可以利用Java的`java.nio`包中的BufferedReader和FileChannel等类,实现按行读取大文件,并将其内容解析后存储到数据库中。本文将详细讲解这一过程。 首先,我们需要了解`java.nio`包...
- **主要内容**:详细解析Buffer的三个核心属性(容量、位置、限制)以及不同类型Buffer的特点。 - **学习目标**:掌握Buffer的内部机制和不同类型的Buffer。 #### 15. Java NIO-Buffer-分配和读写数据 - **主要...
首先,我们需要理解Java NIO的核心组件: 1. **Selector(选择器)**:选择器允许单个线程检查多个通道(Channels)上的事件,如连接建立、数据到达等。通过注册通道到选择器,并设置感兴趣的事件类型,我们可以...
以下是对Java NIO的详细解析: 1. **通道(Channels)** - 通道是NIO的核心组件之一,它提供了与不同种类的底层I/O资源(如文件、套接字、网络流等)交互的方式。通道是双向的,可以用于读写数据。 - 主要的通道...
在这个"java基于NIO选择器Selector的多人聊天室"项目中,开发者利用NIO的核心特性构建了一个允许多个客户端同时进行交互的聊天平台。 首先,我们需要了解NIO的基本组件。在Java NIO中,`Selector`是核心角色,它...
NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。通道可以读写数据,缓冲区用于临时存储数据,选择器则用于监听多个通道的事件,实现了多路复用,从而实现非阻塞I/O。 3. **HttpCore NIO...