JAVA NIO 系列- 02Buffer
一、介绍
缓冲区Buffer 是一个存储器,是一个固定数量的数据容器, 或者为 分段运输区,缓冲区 直接与通道Channel 紧密联系,
Java NIO缓冲区在与NIO通道交互时使用。数据从通道读取到缓冲区,然后从缓冲区写入通道。
缓冲区本质上是一块内存,您可以将数据写入其中,然后再读取数据。这个内存块被包装在一个NIO缓冲区对象中,该对象提供了一组方法,使得使用内存块更容易。
二、Buffer基本使用
2.1 基本使用
通常使用 Buffer 进行读写数据 遵循以下4个步骤:
1. 写入数据到 Buffer
2. 调用 buffer.flip()
3. 从Buffer 里面读取数据
4. 调用buffer.clear() 或者 buffer.compact()
当我们向Buffer 写入数据时, buffer 会跟踪记录已写入的数据, 一旦需要读取数据,需要切换模式,通过调用 flip() 方法,将从写入模式切换到读取模式。
在读取模式下,我们可以读取到所有写入的数据,一旦我们读取到了所有的数据,需要清除缓存以便可以再次为写入数据做准备,有两种方式 可以清除缓存 : 调用 clear() 或者 compact(). 两者的区别时:
clear() :清除所有的数据
compact() : 仅仅清除已经读取过的数据,未读过的数据将会移动到buffer 的开头,并且后续要写入的数据将会接着未读取数据后面.
下面是一个 简单的Buffer 使用例子,主要是为了演示 Buffer 的 常用方法 flip(),.clear() 这些 方法 ,并对之有一个了解.
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
// 创建容量为 48 bytes 的 ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//写入buffer
while (inChannel.read(buf) != -1) {
// 将buffer 模式改为 读模式
buf.flip();
while (buf.hasRemaining()) {
// 一次读取一个字节, 直到读完
System.out.print((char) buf.get());
}
// 清空buffer
buf.clear();
}
aFile.close();
}
2.2 Buffer 类图
从图上可以看出 只有 boolean 这个类型没有继承 Buffer , 其他的7种基本类型都有相应的 继承实现类, 可以进行相关的操作.
2.3 Buffer属性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
Buffer 具有四个基本属性 来提供其所包含的数据元素的信息:
-
容量(capacity): 缓冲区能够容纳的数据元素的最大数量, 这一容量在 缓冲区创建时被设定, 永远不能再被改变.
-
上界(Limit) : 缓冲区的第一个 不能被 读或者写的元素,或者说: 缓冲区中 现存元素的计数.
在写入模式下,Buffer 的limit 是指 可以写入多少数据量到 Buffer, 写入模式下, limit 即等于 Buffer 的 capacity
当切换到 读模式下时, limit 是指 可以从Buffer 最多读取多少数据,因此, 当切换模式为 读模式时,limit 就是写模式下的 写入位置,换句话说,就是 能读到的字节数与写入的字节数一样多.
-
位置(Position) : 下一个 要被读或者写的元素的索引,位置会自动由相应的get() 或者 put() 变更.
当写入数据时,position 就是下一个要被写入元素的索引位置,最初的位置是0 ,当数据写入之后, position 就会被提前指向缓冲区中的下一个单元格,Position 最大值为 Capacity -1.
当读取数据时, position 就是 下一个要被读取的元素的索引位置,从 写模式切到读模式时,position 就好被 reset() 重置为零,当读取到数据之后, position 就会被提前移至下一个读取位置.
-
标记(Mark) : 一个备忘位置,调用mark() 来设定 mark= position,调用reset() 设定 position = mark , 标记在设定前是未定义的.
这四个属性 总是遵循 以下关系: 0<= mark <= position <= limit <= capacity
每次创建一个10 容量的 Buffer , 位置(position) 被设置为 0 , 容量(capacity)和上界(limit) 被设置10 , 标记(mark) 未定义, 容量( capactiy) 是不变的,其他的可以在缓冲区使用时改变.
2.3 Buffer方法API
这里列一下Buffer 里面的主要方法:
public abstract class Buffer {
public final int capacity();
public final Buffer clear();
public final Buffer flip();
public abstract boolean isReadOnly();
public final int limit();
public final Buffer mark() ;
public final boolean hasRemaining() ;
public final int position() ;
public final Buffer reset();
public final Buffer rewind();
}
这里有两点需要注意:
-
当多线程并发调用 一个缓冲区 Buffer ,是线程不安全的, 需要通过 适当的同步机制加以控制
-
支持级联调用
b.flip();
b.position(23);
b.limit(42);
//上面可以替换成 一条组合的语句:
b.flip().position(23).limit(42);
2.3.1 创建缓冲区
从上面Buffer 类图 看, 除了布尔类型没有对应的继承类, 其他的的类型都有, 并且 都是 抽象类, 抽象类 是没法直接实例化的 但是每一个 类里面都有一个 allocate(int capacity) 方法,这个还是一个静态方法,基于类的, 不需要new Object 之后再调用 传入对应的capacity ,就会创建一个 对应大小类型的数组.
public static T allocate(int capacity);
例如 创建一个 大小为 48 的ByteBuffer :
ByteBuffer buf = ByteBuffer.allocate(48);
创建一个 1024 的CharBuffer :
CharBuffer buf = CharBuffer.allocate(1024);
此外, 我们这里以 CharBuffer 类型分析(其他几种类型都类似), 还额外提供了 wrap 方法用于创建.
如果想使用自己的数组作为缓冲区的存储器,那便可以传入自定义的数组
public abstract class CharBuffer{
public static CharBuffer wrap(char[] array);
public static CharBuffer wrap(char[] array,
int offset, int length);
}
如下给出了例子, 创建的 charBuffer 是使用 自定义的 cArr 作为缓存容器,写入的数据实际也在cArr 里面, 这里的Capacity /limit 就是数组的长度 ,position 就是 0
char[] cArr = new char[10];
CharBuffer charBuffer = CharBuffer.wrap(cArr);
charBuffer.put("A"); // 数据就是写在了 cArr 里面
下面这个方法,这里的capacity 就是数组的长度,position 就是 offset , limit 就是 offset+length , 所以这边 offset+length 需要小于 数组的长度. 这些只是初始状态的设置,可以通过 clear() 方法进行调整.
public static CharBuffer wrap(char[] array,
int offset, int length);
2.3.2 存取
存取都比较熟悉.
如果使用里面的共享子数组时,需要注意, 但是绝大部分都用不到 提供的 slice() 方法 ,主要是 put(int index, char c) , 这里的 子数组时, index 需要减去 offset
public abstract class CharBuffer{
public abstract CharBuffer put(char c);
public abstract CharBuffer put(int index, char c);
public abstract char get();
public abstract char get(int index);
}
put(int index, char c) 并不会使position 发生变化, 可以用来指定替换一些值.
2.3.3 翻转 -flip()
当我们将数据写入完成后,需要读取时, 这时就需要对 position、limit 这些参数进行调整, 所有 Buffer 给我们提供了 flip() 方法。
flip() 方法将缓冲区从写入模式切换到读取模式。调用flip() 将 position 设置回 0,并将limit 设置为 写模式时 position 最后的位置。
比如我们对一个 ByteBuffer 写入了 ‘M’、‘e’、‘l’、‘l’、‘o’、‘w’ 五个字节之后,如下图:
这时我们进行调用 flip() ,结果如下图:
从上面可以看出position 已经被重置为0 , limit 被置位了写模式时的position 位置.
提示: 这里有一个 rewind() 方法,也可以进行翻转,但是对limit 没有改动,这样就可以重复读取读取已经被翻转的数据, 这里贴一下对比
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
重要!重要! 重要!(三遍): 连续两次flip() 会导致 实际大小变为0
2.3.3 hasRemaining()
布尔函数 hasRemaining() 是在释放缓冲区时 告诉是否已经达到上界. 此外 remaining() 函数 会告诉 当前位置离上界的剩余数目.
int num = byteBuffer.remaining();
for(int i=0;i<num;i++){
System.out.println(byteBuffer.get());
}
2.3.4 clear() and compact()
一旦完成从缓冲区中读取数据,就需要缓冲区准备好再次写入。可以通过调用clear()或调用compact()来实现。
Clear() 方法 主要是将 position 置为0 , 将limit 置为 capacity, 换句话说,就是 缓冲区被清除了,但是数据没有清除,仅仅是标记 可以从哪里开始写入.
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
如果缓冲区Buffer 里面还有一些 未读数据, 调用了clear() 之后, 这些数据也就被作废了, 如果这些未读的数据,还是需要读取的,但是需要先写入部分数据,那就可以调用 compact() 方法, compact () 方法 主要是将 未读取的数据移动到缓冲区头部, 然后设置position 为最后一个未读元素的右侧,limit 属性还是设置为capacity, 这样就可以写入数据,但是 未读数据不会被覆盖.
public CharBuffer compact() {
// 将未读数据拷贝到数组开头
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
// 将position 位置设置为最后一个未读元素的右侧,也就是下次就是接着最后一个未读元素后面继续写入
position(remaining());
// 设置limit 上界为 capacity
limit(capacity());
//清除标记位
discardMark();
return this;
}
例子:
缓冲区 有数据 ‘M’,‘e’,‘l’,‘l’,‘o’,‘w’ , 已经读取了 ‘M’,‘e’, position =2 已经指向了 ‘l’ , limit =6 , 此时需要写入:
调用了 compact() 之后, 如下图:
这里可以看到 剩下的 ‘l’,‘l’,‘o’,‘w’ , 已经被移到最前面, position 指向了最后一个未读元素 ‘0’的后面, 后续就从这里开始接着写入, 会覆盖 后面的 ‘o’, ‘w’ , 这两个数据已经被移动到最前面了.
2.3.5 mark() and reset()
可以通过调用mark() 方法来 标记 当前位置, 在后续可以 返回到这里继续读,调用 reset() 方法。
reset() 和clear() 方法 有区别, clear() 是清除缓存区, reset() 是 返回上次标记的 position 位置
下面是代码块, 有一个判断是否之前设置过,如果没有就报错.
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
调用 reset() 之后, 如下图, position 从 位置 4 切到了 2的 位置.
2.3.6 equals() and compareTo()
比较两个缓冲区 是否相等时,可以用 equals() 或者 compareTo() .
两个Buffer 相等的条件是:
- 两个对象类型相同
- 两个对象 剩余元素的数量相同,Buffer 的容量不需要相同,缓冲区中剩余数据的索引值也不需要相同,但是缓冲区中 剩余数据的 数目(从当前位置 到 上界) 必须相同
- Buffer 中所有剩余的byte、char等都相同, 并且元素顺序一致.
public boolean equals(Object ob) {
// 如果是同一个对象,直接返回true
if (this == ob)
return true;
// 如果不是 当前比较类型, 直接false
if (!(ob instanceof CharBuffer))
return false;
CharBuffer that = (CharBuffer)ob;
int thisPos = this.position();
int thisRem = this.limit() - thisPos;
int thatPos = that.position();
int thatRem = that.limit() - thatPos;
// 如果两个缓冲区 的 剩余数量不相等,返回false
if (thisRem < 0 || thisRem != thatRem)
return false;
// 剩余元素中, 每一个都需要相同,顺序一致,才返回true
for (int i = this.limit() - 1, j = that.limit() - 1; i >= thisPos; i--, j--)
if (!equals(this.get(i), that.get(j)))
return false;
return true;
}
下面两个就是相对的, 虽然总的长度不一致, 但是 从 position -> limit 都是 剩余3个, 元素都是一致的, 都是
‘c’,‘o’,‘m’
下面两个被认为不相等的,不满足上面的3个条件
compareTo() 是比较两个缓冲区的 剩余元素, 这里强制指定同一个类型, 如果满足以下条件,则认为一个缓冲区比另一个缓冲区“小”:
- 与另一个缓冲区中的对应元素相等的第一个元素比另一个缓冲区中的元素小。
- 所有元素都相等,但第一个缓冲区在第二个缓冲区之前用完了元素(它的元素更少)。
public int compareTo(ByteBuffer that) {
int thisPos = this.position();
int thisRem = this.limit() - thisPos;
int thatPos = that.position();
int thatRem = that.limit() - thatPos;
int length = Math.min(thisRem, thatRem);
// 正常情况下 position 是 肯定小于 limit ,length 正常都是 大于等于0
if (length < 0)
return -1;
int n = thisPos + Math.min(thisRem, thatRem);
for (int i = thisPos, j = thatPos; i < n; i++, j++) {
int cmp = compare(this.get(i), that.get(j));
// 只要第一次出现不相等,就返回
if (cmp != 0)
return cmp;
}
// 如果Remaing 里面都相等,那就比较剩余元素的长度.
return thisRem - thatRem;
}
三、小结
本章主要分析了Buffer 的一些常用属性和方法.
支付宝 | 微信 |
---|---|
如果有帮助记得打赏哦 | 特别需要您的打赏哦 |