java缓冲器的作用,java NIO之缓冲区篇(一)

我们以Buffer类开始我们对java.nio软件包的浏览历程,这些类是java.nio的构造基础。一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。对于每一种非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。缓冲区的工作与通道紧密联系。通道I/O传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,想传递的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据置于提供的缓冲区中。

5e000ec49946

Buffer类族.png

上图是Buffer的类层次图。在顶部是通用的Buffer类。Buffer定义所有缓冲区类型共有的操作,无论是它们所包含的数据类型还是可能具有的特定行为。这一共同点将会成为我们的出发点。

缓冲区基础

概念上,缓冲区是包在一个对象内的基本数据元素数组。Buffer类相比于一个简单数组的优点是它将关于数据的数据内容和信息包含在一个对象中。

属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。

容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。

上界(limit):缓冲区第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。

位置(Position):下一个要被读或写的元素的索引。位置会自动由相应的get()和put()函数更新。

标记(Mark):一个备忘位置。调用mark()来设定mark = position。调用reset()设定position = mark。标记在设定前是未定义的(undefined)。

这四个属性之间总是遵循以下关系:

0 <= mark <= position <= limit <= capacity

新创建的ByteBuffer:

5e000ec49946

新创建的ByteBuffer

新建的ByteBuffer位置被设为0,而且容量和上界被设为10,刚好经过缓冲区能够容纳的最后一个字节。标记最初未定义。容量是固定的,但另外的三个属性可以在使用缓冲区时改变。

缓冲区API

以下是Buffer类的方法签名:

5e000ec49946

Buffer类的方法签名

关于这个API有一点需要注意的是,像clear()这类函数,通常应当返回void,而不是Buffer引用。这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:

buffer.mark();

buffer.position(5);

buffer.reset();

被简写为:

buffer.mark().position(5).reset();

对于API还要注意的一点是isReadOnly()函数。所有的缓冲区都是可读的,但并非所有都可写。每个具体的缓冲区类都通过执行isReadOnly()来标示其是否允许该缓冲区的内容被修改。一些类型的缓冲区类可能未使其数据元素存储在一个数组中。例如MappedByteBuffer的内容可能实际是一个只读文件。对只读的缓冲区的修改尝试将会导致ReadOnlyBufferException抛出。

存取

public abstract class ByteBuffer extends Buffer implements Comparable

{

public abstract byte get();

public abstract byte get(int index);

public abstract ByteBuffer put(byte b);

public abstract ByteBuffer put(int index, byte b);

}

get和put可以是相对的或者绝对的。在前面的程序列表中,相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛出异常。对于put(),如果运算会导致位置超出上界,就会抛出BufferOverException异常。对于get(),如果位置不小于上界,就会抛出BufferUnderflowException异常。绝对存取不会影响缓冲区的位置属性。

填充

将代表“Hello”字符串的ASCII码载入一个名为buffer的ByteBuffer对象中。执行如下代码后,缓冲区的状态如下图所示:

buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l')

.put((byte)'o');

5e000ec49946

调用put()之后的缓冲区.png

注意本例中的每个字符都必须被强制转换为byte。

更改缓冲区内容:

buffer.put(0, (byte)'M').put((byte)'w');

这里通过一次绝对方案的put将0位置的字符代替为M,并且将w放入当前位置(当前位置不会受到绝对put()的影响),并将位置属性加一。结果如下图所示:

5e000ec49946

修改后的buffer.png

翻转

上界属性指明了缓冲区有效内容的末端。将上界属性设置为当前位置,然后将位置重置为0,这就是当前缓冲区写入的数据了。可以用如下代码来实现:

buffer.limit(buffer.position()).position(0);

这种从填充到释放状态的缓冲区翻转是API设计者预先设计好的,他们为我们提供了一个非常便利的函数:

buffer.flip();

flip()方法将一个能够继续添加元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。在翻转之后,缓冲区会变成如下的样子:

5e000ec49946

被翻转后的缓冲区.png

如果将缓冲区翻转两次会怎么样呢?

它实际大小会变成0.第二次翻转时会把上界设为位置的值(第一次翻转后位置的值为0),并且把位置设为0.

释放

布尔函数hasRemaining()会在释放缓冲区时告知是否已经达到缓冲区的上界。以下是一种将数据元素从缓冲区释放到一个数组的方法:

for(int i=0; buffer.hasRemaining(); i++) {

myByteArray [i] = buffer.get();

}

remaining()函数告知从当前位置到上界还剩余的元素数目。也可以通过如下方法来释放缓冲区:

int count = buffer.remaining();

for(int i = 0; i < count; i++) {

myByteArray [i] = buffer.get();

}

第二种方法比第一种方法更高效,因为上界不会在每次循环重复时都被检查。

缓冲区并不是线程安全的,如果想以多线程同时存取特定的缓冲区,需要在存取缓冲区之前进行同步(例如对缓冲区对象进行跟踪)

一旦缓冲区对象完成填充并释放,它就可以被重新使用了。clear()方法将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回0,这使得缓冲区可以被重新填入。缓冲区如下图所示:

5e000ec49946

clear后的缓冲区.png

import java.nio.CharBuffer;

public class BufferFillDrain {

public static void main(String[] args) throws Exception{

CharBuffer buffer = CharBuffer.allocate(100);

while (fillBuffer(buffer)) {

buffer.flip();

drainBuffer(buffer);

buffer.clear();

}

}

private static void drainBuffer(CharBuffer buffer) {

while (buffer.hasRemaining()) {

System.out.print(buffer.get());

}

System.out.println(" ");

}

private static boolean fillBuffer(CharBuffer buffer) {

if(index > strings.length) {

return false;

}

String string = strings[index++];

for (int i=0; i

buffer.put(string.charAt(i));

}

return true;

}

private static int index = 0;

private static String[] strings = {

"each a contiguous range of virtual memory",

"Allocation in a heap region, top, between allocated and unallocated space",

"One region is the current region from is be- ing allocated",

"Since we are mainly concerned",

"mutator threads allocate only",

"or TLABs, directly in this heap region"

};

}

压缩

public abstract class ByteBuffer extends Buffer implements Comparable {

public abstract ByteBuffer compact( );

}

调用compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。

5e000ec49946

被部分释放的缓冲区.png

5e000ec49946

压缩后的缓冲区.png

在这里发生了以下几件事:

1.数据元素2-5被复制到0-3的位置。位置4和5不受影响,但现在已经超出了当前的位置,因此可以被之后的put()调用重写。

2.位置已经被设置为被复制的数据元素的数目。

3.上界属性被设置为容量的值,因此缓冲区可以再次被填满

标记

标记:使缓冲区能够记住一个位置并在之后将其返回。

缓冲区的标记在mark()函数被调用之前是未定义的,调用时被设置为当前位置的值reset()函数将位置设置为当前的标记值。运行如下代码:

buffer.position(2).mark().position(4);

5e000ec49946

调用mark()前的缓冲区.png

5e000ec49946

调用mark()后的缓冲区.png

如果我们此时调用reset(),位置将会被设置为标记:

5e000ec49946

调用reset()后的缓冲区

比较

所有的缓冲区都提供了一个常规的equals()函数用以测试两个缓冲区是否相等,以及一个compareTo()函数用以比较缓冲区。

public abstract class ByteBuffer extends Buffer

implements Comparable

{

public boolean equals(Object ob)

public int compareTo(ByteBuffer that)

}

如果每个缓冲区剩余的内容都相同,那么equals()方法将返回true,否则返回false。两个缓冲区被认为相等的充要条件为:

1.两个对象类型相同。

2.两个对象都剩余同样数量的元素。Buffer的容量不需要相同,而且缓冲区中剩余数据的索引页不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。

3.在每个缓冲区中应被get()方法返回的剩余数据元素序列必须一致。

5e000ec49946

两个被认为是相等的缓冲区.png

5e000ec49946

两个被认为是不相等的缓冲区.png

缓冲区也支持用compareTo()方法以字典序进行比较。这一方法在缓冲区参数小于、等于或者大于引用compareTo()的对象实例时,分别返回一个负整数、0和正整数。compareTo()不允许不同对象间进行比较,如果传递一个类型错误的对象,它会抛出ClassCastException异常。

批量移动

buffer API提供了想向缓冲区内外批量移动数据元素的方法:

public abstract class CharBuffer

extends Buffer

implements Comparable, Appendable, CharSequence, Readable

{

public CharBuffer get(char [] dest)

public CharBuffer get(char[] dst, int offset, int length)

public CharBuffer put(CharBuffer src)

public CharBuffer put(char[] src, int offset, int length)

public final CharBuffer put(char[] src)

//CharBuffer独有的方法

public final CharBuffer put(String src)

public CharBuffer put(String src, int start, int end)

}

有两种形式的get()可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。第二种形式使用offset和length参数来指定目标数组的子区间。这些批量移动的合成效果与前文讨论的循环是相同的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他优化来移动数据。

批量传输的大小总是固定的,省略长度意味着整个数组会被填满。CharBuffer的get(char [] dest)方法的实现如下所示:

public CharBuffer get(char[] dst) {

return get(dst, 0, dst.length);

}

如果缓冲区不能传送相应数量的数据,则缓冲区的状态不变,同时抛出BufferUnderflowException异常。

public CharBuffer get(char[] dst, int offset, int length) {

checkBounds(offset, length, dst.length);

//数据量不够,抛出异常

if (length > remaining())

throw new BufferUnderflowException();

int end = offset + length;

for (int i = offset; i < end; i++)

dst[i] = get();

return this;

}

为了避免异常,如果需要将一个缓冲区释放到一个大数组中,则要这样做:

char [] bigArray = new char [1000];

//获取缓冲区的数据量

int length = buffer.remaining();

//将缓冲区中的所有数据写入数组中,数组的数据长度即为length

buffer.get(bigArray, 0, length);

//数据处理,传入length

processData(bigArray, length);

反之,如果需要将大缓冲区拷贝到小数组中,则可以如下所示:

char [] smallArray = new char [];

while (buffer.hasRemaining()) {

int length = Math.min (buffer.remaining(), smallArray.length);

buffer.get(smallArray, length);

processData(smallArray, length);

}

put()的批量版本工作方式相似,但以相反的方向移动数据,从数组移动到缓冲区,这里不再赘述。

以上所讨论的适用于其它典型的缓冲区,但是由于String的特殊性,CharBuffer提供了两个特殊的方法用于批量移动数据:

public abstract class CharBuffer

extends Buffer

implements Comparable, Appendable, CharSequence, Readable

{

//CharBuffer独有的方法

public final CharBuffer put(String src)

public CharBuffer put(String src, int start, int end)

}

这些函数使用String作为参数,而且与作用于char数组的批量移动函数相似。String不同于char数组,但String确实包含char字符串,而且我们确实倾向于将其在概念上认为是char数组,由于这些原因,CharBuffer类提供了将String复制到CharBuffer中的便利方法。

public CharBuffer put(String src, int start, int end) {

checkBounds(start, end - start, src.length());

if (isReadOnly())

throw new ReadOnlyBufferException();

if (end - start > remaining())

throw new BufferOverflowException();

for (int i = start; i < end; i++)

this.put(src.charAt(i));

return this;

}

综上,这篇文章主要介绍了缓冲区的四个属性和相关的操作,关于缓冲区还有一些知识比如创建缓冲区、字节缓冲区、视图缓冲区需要介绍,这些知识将会放在java NIO之缓冲区篇(二)中讲解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值