Java NIO系列3:缓冲区

前言

一直想把NIO系列的文章更新下去,只不过发现在入职支付宝后工作实在是忙,所以一直拖到现在。直接从一个学生过狗成为一名加班狗,好吧,这就是互联网公司的现状吧,但是每天都是充实的,而且发现其他的员工也基本非常乐意加班,难道这就是阿里的文化熏陶?!

废话不多说,还是进入今天的正题,在前面的文章中,我们已经对Java的NIO有了一个粗浅的认识——主要之为了支持非阻塞I/O的操作,之前的BIO则是阻塞的。因为异步性的引入,大大提高了Java在处理异步任务的效率。下面认识下NIO中的缓冲区:

认识缓冲区

缓冲区可以理解为存储固定数量的容器,这些数据可以在存储后被检索,缓冲区的数据可以被写满或者被释放,而这些操作的实现需要认识缓冲区的属性:容量(capacity)、上界(limit)、位置(position)和标记(mark)。这四个属性的关系可以用下图来表示:

理解好这张图,缓冲区的精髓就掌握的四五分了,还有四五分从哪里来呢?当然是实践中来了。这张图传达了如下的信息:

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

容量

容量是指缓冲区可以容纳的最大元素个数,这一容量在缓冲区被创建的时候就被指定了。

上界

就是缓冲区现存元素的计数

位置

下一个要被读取的元素的索引

标记

由于在读取过程中position的值是动态变化,如果在读到某一个位置的时候需要做一个标记以便下次继续读,就可以做一个“书签”,让mark的值等于当前读到的position的值。

在java.nio包中Buffer类是缓冲区的顶层类,其他的缓冲区类都是基于这个类展开的。一个缓冲区类的主要方法如下:

public abstract class Buffer {
    // 获取缓冲区的容量
    public final int capacity( );
    // 获取下一个将要被读取的元素的索引
    public final int position( );
    // 设置缓冲区的位置为新位置
    public final Buffer position (int newPositio);
    // 返回上界
    public final int limit( );
    // 设置新的上界
    public final Buffer limit (int newLimit);
    // 返回标记的值
    public final Buffer mark( );
    // 将position重置为mark的值
    public final Buffer reset( );
    // 清空对上界和位置的操作
    public final Buffer clear( );
    // 重置position为0并将上界设为远position的值
    public final Buffer flip( );
    // 将position设置0
    public final Buffer rewind( );
    // 返回剩余未读的元素个数
    public final int remaining( );
    // 判断是否还有未读的元素
    public final boolean hasRemaining( );
    // 判断是否是只读
    public abstract boolean isReadOnly( );
}

作为一个缓冲区,肯定是需要存取数据的,但是在Buffer类中并没有get和put类似的方法,实际上Java 设计者将get和put方法放到子类实现了。这样设计主要是考虑到在只需要关心缓冲区钟部分数据的情况,因为在这种情况可能有不同的存取策略。

缓冲区实战

现在已经对缓冲区有了一个基本认识,缓冲区的操作主要涉及以下几个部分:

存取

翻转

释放

压缩

标记

比较

批量移动

存取、翻转和释放

在缓冲区存取元素,需要创建缓冲区,存取操作的两个核心方法是:get和put方法,完成翻转只需要调用flip方法。下面是一个简单示例:

package com.rhwayfun.patchwork.nio.buffer;

import java.nio.CharBuffer;

/**
 * @description: Buffer缓冲区演示
 * @author: rhwayfun
 * @since: 2016-05-21
 */
public class BufferFillDrain {

    // 位置
    private static int index = 0;
    // 需要填充入缓冲区字符串数组
    private static String[] strings = new String[]{
            "Hello, this is a buffer",
            "I am learning buffer",
            "It is a wonderful world"
    };

    public static void main(String[] args) {
        // 申请100字节的缓冲区
        CharBuffer buffer = CharBuffer.allocate(100);
        // 如果缓存区没有达到上界则可以继续填充
        while (fillBuffer(buffer)){
            // 缓冲区翻转
            // flip可以将上界设为当前position的位置
            // 并将position重置为0
            buffer.flip();
            // 从缓冲区释放数据
            drainBuffer(buffer);
            // 清空缓冲区
            // 调用clear方法可本质上干了两件事
            // 一:将上界limit设为容量大小的位置
            // 二:将position重置为0
            buffer.clear();
        }
    }

    private static void drainBuffer(CharBuffer buffer) {
        // 得到剩余的元素个数
        int count = buffer.remaining();
        for (int i = 0; i < count; i++){
            System.out.println(i + "--->" + buffer.get());
        }
    }

    private static boolean fillBuffer(CharBuffer buffer) {
        if (index >= strings.length) {
            return false;
        }
        // 依次放入每个字符串数组
        String s = strings[index++];
        for (int i = 0; i < s.length(); i++){
            buffer.put(s.charAt(i));
        }
        return true;
    }
}

压缩

压缩使用在需要释放缓冲区的空间的场景,执行压缩需要调用compact方法。该方法的会首先将position位置之后的元素(如果没有调用flip方法的话)复制从0开始的位置上,而position位置及之后的元素保持不变,在进行元素复制的过程中position的值也会发生改变。压缩的过程可以用如下的图表示:

下面的代码有演示了这个过程:

package com.rhwayfun.patchwork.nio.buffer;

import java.nio.CharBuffer;

/**
 * @description: 使用compact方法释放一部分数据
 * @author: rhwayfun
 * @since: 2016-05-21
 */
public class BufferCompact {
    public static void main(String[] args) {
        String s = "hello";

        CharBuffer buffer = CharBuffer.allocate(10);
        for (int i = 0; i < s.length(); i++){
            buffer.put(s.charAt(i));
        }

        // 将第一个位置的字符h换为M,并填入一个新的字符w
        buffer.put(0,‘M‘).put(‘w‘);

        // 现在缓冲区的元素是Mellow,position的位置是6,limit和capacity都是10
        // 调用flip方法得到有效元素的位置,其实也就是position的位置
        buffer.flip();

        // 将M和e两个个字符释放掉
        System.out.println("release char " + buffer.get());
        System.out.println("release char " + buffer.get());

        // 调用compact方法可以回收M和e字符占用的空间
        // 调用compact后position位置是4,
        // 0-3位置的字符依次是llow,4-5位置的字符依次是ow
        // 虽然4-5位置有字符,但是调用put方法后会被重写
        // 并且调用compact方法后limit又回到了capacity的位置,也就是10
        buffer.compact();

        // 将4-5位置的字符修改为ab
        buffer.put(‘a‘).put(‘b‘);

        // 输出缓冲区的元素,根据上面的分析,最后输出的字符应该是llowab
        // 需要先调用flip方法将position设为0,这样才能读到完整的字符
        buffer.flip();
        while (buffer.hasRemaining()){
            System.out.println("print output " + buffer.get());
        }
    }
}

比较

缓冲区的比较可以使用equals方法,进行缓冲区的比较的意义在于检测缓冲区包含的数据是否相同。当然,缓冲区的比较也支持使用compareTo方法进行比较。如果buffer1.equals(buffer2)要返回true,那么需要满足以下条件才可以:

  • 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer绝不会等于非 buffer 对象。
  • 两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
  • 在每个缓冲区中应被 Get()函数返回的剩余数据元素序列必须一致。

下面的代码演示了进行缓冲区比较的过程:

package com.rhwayfun.patchwork.nio.buffer;

import java.nio.CharBuffer;

/**
 * @description: 使用equals方法比较两个缓冲区是否相等
 * @author: rhwayfun
 * @since: 2016-05-21
 */
public class BufferCompareWithEquals {
    public static void main(String[] args) {
        String s1 = "two com";
        String s2 = "com";
        CharBuffer buffer1 = CharBuffer.allocate(10);
        CharBuffer buffer2 = CharBuffer.allocate(10);
        for (int i = 0; i < s1.length(); i++){
            buffer1.put(s1.charAt(i));
        }
        // 调用flip方法后limit为7
        buffer1.flip();
        // 继续往缓冲区放入一个字符,当前position的位置是1
        buffer1.put(‘A‘);
        // 将当前位置设为4,也就是字符c开始的位置
        buffer1.position(4);

        /*System.out.print("buffer1: ");
        while (buffer1.hasRemaining()){
            System.out.print(buffer1.get() + " ");
        }*/

        for (int i = 0; i < s2.length();i++){
            buffer2.put(s2.charAt(i));
        }
        // limit的位置是3,position的位置是0
        buffer2.flip();
        // 现在执行put方法会把位置0的字符c替换为B
        buffer2.limit(4);
        buffer2.put(3,‘B‘);
        buffer2.position(0);
        buffer2.limit(3);

        /*System.out.print("buffer2: ");
        while (buffer2.hasRemaining()){
            System.out.print(buffer2.get() + " ");
        }*/

        // 使用equals方法比较两个缓冲区是否相等
        // 必须满足三个条件才能返回true
        // 1)两个缓冲区的对象的类型必须相同
        // 2)缓冲区的元素不必完全相同,但是如果position到limit之间的元素不相同,也返回false
        // 3)每个缓冲区get方法得到元素序列必须一致

        System.out.println(buffer1.equals(buffer2));

        // 还可以使用compareTo方法比较两个缓冲区,但是如果两个缓冲区的元素的类型不一致,equals方法返回false
        // 而compareTo方法则直接抛出ClassCastException的异常
    }
}

批量操作

在之前的get和put方法,一次只能支持操作一个元素,从效率的角度讲还是很低的,那么有没有批量操作的方法呢?在Buffer API中可以使用重载的get和put方法进行批量的操作。下面的代码演示了如何进行缓冲区的批量操作:

package com.rhwayfun.patchwork.nio.buffer;

import java.nio.CharBuffer;

/**
 * @description: 批量操作缓冲区
 * @author: rhwayfun
 * @since: 2016-05-21
 */
public class BufferBatchOperation {
    public static void main(String[] args) {
        copyToBigArray();
        copyToSmallArray();
        System.out.println();
        putStringToBuffer();
    }

    private static void copyToBigArray() {
        // 将一个缓冲区释放到一个大数组中
        String s = "hello,rhwayfun";
        CharBuffer buffer = CharBuffer.allocate(20);
        for (int i = 0; i < s.length(); i++){
            buffer.put(s.charAt(i));
        }
        buffer.flip();
        // 创建一个大数组
        char[] array = new char[100];
        // 如果直接使用get方法将元素释放到array数组中
        // 则实际上将数组的0位置到最后一个位置,这里是
        // 99,依次将缓冲区的元素释放到数组中。由于缓冲
        // 区的元素根本不够,所以会抛出BufferUnderflowException异常
        // buffer.get(array);
        int remaining = buffer.remaining();
        buffer.get(array,0,remaining);

        // 测试array数组
        for (char c : array){
            System.out.print(c + " ");
        }
    }

    private static void copyToSmallArray(){
        // 将一个缓冲区释放到一个大数组中
        String s = "hello,rhwayfun";
        CharBuffer buffer = CharBuffer.allocate(20);
        for (int i = 0; i < s.length(); i++){
            buffer.put(s.charAt(i));
        }
        buffer.flip();
        char[] array = new char[10];
        while (buffer.hasRemaining()){
            int length = Math.min(array.length,buffer.remaining());
            buffer.get(array,0,length);
        }
    }

    private static  void putStringToBuffer(){
        String s = "Good morning";
        CharBuffer buffer = CharBuffer.allocate(20);
        buffer.put(s);
        buffer.flip();
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get() + " ");
        }
    }
}

小结

通过以上的叙述以及针对缓冲区的也也演示操作,我们对缓冲区的掌握也更加深入了,其实对缓冲区的理解最关键的还是缓冲区的模型图。这个图理解了,掌握Buffer类的API也就驾轻就熟了。

时间: 2024-11-05 11:19:06

Java NIO系列3:缓冲区的相关文章

Java NIO系列教程(三) Buffer

原文链接:http://ifeve.com/buffers/ 声明:Java NIO系列教材并非本人原创,只因阅读原文之后有感于文章之精妙,意欲与诸位共享,故而出此下策,忘原作者见谅.另附上原文地址. Java NIO的通道类似流,但又有些不同: Java NIO中的Buffer用于和NIO通道进行交互.如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问

Java NIO系列教程(二) Channel

原文地址:http://ifeve.com/channels/ 声明:Java NIO系列教材并非本人原创,只因阅读原文之后有感于文章之精妙,意欲与诸位共享,故而出此下策,忘原作者见谅.另附上原文地址. Java NIO的通道类似流,但又有些不同: 既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的. 通道可以异步地读写. 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入. 正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道.如下图所示: C

java NIO系列教程1

ava NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式. Java NIO: Channels and Buffers(通道和缓冲区) 标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Java NIO: Asynchronous IO(异步IO) Java NIO可以让你

Java NIO中的缓冲区Buffer(一)缓冲区基础

什么是缓冲区(Buffer) 定义 简单地说就是一块存储区域,哈哈哈,可能太简单了,或者可以换种说法,从代码的角度来讲(可以查看JDK中Buffer.ByteBuffer.DoubleBuffer等的源码),Buffer类内部其实就是一个基本数据类型的数组,以及对这个缓冲数组的各种操作: 常见的缓冲区如ByteBuffer.IntBuffer.DoubleBuffer...内部对应的数组依次是byte.int.double... 与通道的关系 在Java NIO中,缓冲区主要是跟通道(Chann

java NIO系列教程2

7.FileChannel Java NIO中的FileChannel是一个连接到文件的通道.可以通过文件通道读写文件. FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下. 打开FileChannel 在使用FileChannel之前,必须先打开它.但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream.OutputStream或RandomAccessFile来获取一个FileChannel实例.下面是通过RandomAccessFile打开

Java NIO系列教程(五) 通道之间的数据传输

原文地址:http://tutorials.jenkov.com/java-nio/scatter-gather.html 作者:Jakob Jenkov   译者:郭蕾     校对:周泰 在Java NIO中,如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel(译者注:channel中文常译作通道)传输到另外一个channel. transferFrom() FileChannel的transferFrom()方法可以将数据从源通道传输到FileChanne

Java NIO系列(二) - Buffer

前言 在Java NIO中,缓冲区用来临时存储数据,可以理解为是I/O操作中数据暂存的中转站.缓冲区直接为通道(Channel)服务,数据是从通道读入缓冲区,从缓冲区写入到通道中的. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存.这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问这块内存. 正文 Buffer的类型 Java NIO提供以下几种Buffer类型: ByteBuffer MappedByteBuffer ShortBuffer LongBuff

Java NIO 系列教程(转)

原文中说了最重要的3个概念,Channel 通道Buffer 缓冲区Selector 选择器其中Channel对应以前的流,Buffer不是什么新东西,Selector是因为nio可以使用异步的非堵塞模式才加入的东西.以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于水管没有阀门,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上.nio的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也只能接一个水管的水,但依赖轮换策略,在水量不大的时候,各

Java NIO 系列教程

转载于http://www.iteye.com/magazines/132-Java-NIO Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.本系列教程将有助于你学习和理解Java NIO.感谢并发编程网的翻译和投递. (关注ITeye官微,随时随地查看最新开发资讯.技术文章.) Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流