java NIO之缓冲区篇(一)

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

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:

新创建的ByteBuffer

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

缓冲区API

以下是Buffer类的方法签名:


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<ByteBuffer>
{ 
    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');
调用put()之后的缓冲区.png

注意本例中的每个字符都必须被强制转换为byte。
更改缓冲区内容:

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

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

修改后的buffer.png

翻转

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

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

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

buffer.flip();

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

被翻转后的缓冲区.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,这使得缓冲区可以被重新填入。缓冲区如下图所示:

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<string.length();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<ByteBuffer> {
  public abstract ByteBuffer compact( );
}

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

被部分释放的缓冲区.png
压缩后的缓冲区.png

在这里发生了以下几件事:
1.数据元素2-5被复制到0-3的位置。位置4和5不受影响,但现在已经超出了当前的位置,因此可以被之后的put()调用重写。
2.位置已经被设置为被复制的数据元素的数目。
3.上界属性被设置为容量的值,因此缓冲区可以再次被填满

标记

标记:使缓冲区能够记住一个位置并在之后将其返回。
缓冲区的标记在mark()函数被调用之前是未定义的,调用时被设置为当前位置的值reset()函数将位置设置为当前的标记值。运行如下代码:

buffer.position(2).mark().position(4);
调用mark()前的缓冲区.png
调用mark()后的缓冲区.png

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


调用reset()后的缓冲区

比较

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

public abstract class ByteBuffer extends Buffer
    implements Comparable<ByteBuffer>
{
    public boolean equals(Object ob)
    public int compareTo(ByteBuffer that)
}

如果每个缓冲区剩余的内容都相同,那么equals()方法将返回true,否则返回false。两个缓冲区被认为相等的充要条件为:
1.两个对象类型相同。
2.两个对象都剩余同样数量的元素。Buffer的容量不需要相同,而且缓冲区中剩余数据的索引页不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
3.在每个缓冲区中应被get()方法返回的剩余数据元素序列必须一致。


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

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

批量移动

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

public abstract class CharBuffer
    extends Buffer
    implements Comparable<CharBuffer>, 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<CharBuffer>, 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之缓冲区篇(二)中讲解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容