java I/O系统(5)-Buffer类

来源:互联网 发布:见一页而知岁月将暮 编辑:程序博客网 时间:2024/05/16 12:59

引言

在java的IO系统中,在JDK1.4的时候引入了新的IO系统,也就是我们常说的nio,它的主要功能是提高速度。在本篇博文中,详细介绍关于nio的构造:通道和缓冲器,核心了解ByteBuffer,因为它是唯一与通道直接交互的缓冲器。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

通道与缓冲器

通道是指资源的真实存在,而缓冲器是运输资源的媒介。比如说我们喝水和时候:水就是通道,而杯子就是缓冲器。我们不直接喝水,通常我们都是用杯子装好水,然后再用杯子喝水。类似的,我们不直接操作通道,而是操作缓冲器,通过缓冲器来操作通道。
对于nio,jdk中有4个包,在java.nio包中主要是对缓冲器和缓冲器和缓冲器扩展等的描述;在java.nio.channels包中主要是对文件通道的描述。在本篇博文中主要是对通道和缓冲器的进一步解释。
通道(FileChannel),我们一般通过在老的IO系统中获取,主要包含3个类:FileInputStream,FileoutputStream和RandomAccessFile,对于这3个类,前面两者我们可以定义出2个输入通道和输出通道。对于后者,基于RandomAccessFile的性质,我们可以在文件中任意移动位置,获取恰当的资源。而缓冲器,我们一般需要自己创建一个恰当大小的对象。

有Buffer类实现的缓冲器有如下这些:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
其中直接与通道交互的是ByteBuffer,因为它是直接面未加工的字节的。

下面的代码就是最简单的建立了2个通道与一个缓冲器:

package com.brickworkers.io.nio;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class ChannelAndBufferTest {    public static void main(String[] args) throws FileNotFoundException {        //建立一个输入通道        FileChannel in = new FileInputStream("F:/java/io/in.txt").getChannel(),                //建立一个输出通道                    out = new FileInputStream("F:/java/io/out.txt").getChannel();        //建立一个缓冲器        ByteBuffer bf = ByteBuffer.allocate(1024);    }}

ByteBuffer缓冲器详解

1、类定义
以下是ByteBuffer的类结构定义:

public abstract class ByteBuffer    extends Buffer    implements Comparable<ByteBuffer>    }

从上面可以看出ByteBuffer是继承Buffer实现的,同时它自身是一个抽象类,而且它还实现了比较接口,两个ByteBuffer之间可以比较大小。我们不去讨论别的,在类定义中我们核心了解的是关于缓冲器的核心结构,我们先看一下Buffer的基本组成:

public abstract class Buffer {    /**     * The characteristics of Spliterators that traverse and split elements     * maintained in Buffers.     */    static final int SPLITERATOR_CHARACTERISTICS =        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;    // Invariants: mark <= position <= limit <= capacity    private int mark = -1;    private int position = 0;    private int limit;    private int capacity;    }

在Buffer中有4个重要的字段:①mark表示当前position的位置,也就是说当我们调用一次mark的时候,那么mark就会指向position所指向的位置。②position表示当前操作位置。在定义好大小的缓冲器中,我们每次读取或者写出数据的时候,这个position都会相应的移动,相当于上一篇博文中介绍的RandomAccessFile一样,文件指针的移动。③limit表示可读写的边界,当position的位置等于limit的时候就表示全部读取或者写入完成;④capacity表示定义的缓冲器容量大小。
在后面的demo中会详细展示这4个变量的具体运作情况。

2、构造函数
以下是ByteBuffer的构造函数源码:

    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;    }    // Creates a new buffer with the given mark, position, limit, and capacity    //    ByteBuffer(int mark, int pos, int lim, int cap) { // package-private        this(mark, pos, lim, cap, null, 0);    }

看完构造函数,我们就知道,我们是不能直接通过构造函数创建ByteBuffer对象的,因为它是包等级的私密方法。那么我们应该如何获取一个缓冲器对象呢,和很多的类一样,ByteBuffer也是通过静态工厂方法创建对象的,请看下面这个方法:

//对象创建静态方法1   public static ByteBuffer allocateDirect(int capacity) {        return new DirectByteBuffer(capacity);    }//对象创建静态方法2    public static ByteBuffer allocate(int capacity) {        if (capacity < 0)            throw new IllegalArgumentException();        return new HeapByteBuffer(capacity, capacity);    }//对象创建静态方法3   public static ByteBuffer wrap(byte[] array,                                    int offset, int length)    {        try {            return new HeapByteBuffer(array, offset, length);        } catch (IllegalArgumentException x) {            throw new IndexOutOfBoundsException();        }    }//对象创建静态方法4  public static ByteBuffer wrap(byte[] array) {        return wrap(array, 0, array.length);    }

从ByteBuffer的静态工厂方法可以看出:①和②是直接建立一个空的、指定大小的新缓冲器。那么两者有什么区别呢?查阅相关资料,发现allocateDirect方法比allocate方法效率会更高。③和④比较容易理解,都是把一个byte数组包装成一个ByteBuffer缓冲器,但是③方法可以指定需要包装的byte的数组的具体位置,它有一个偏移作为入参。

3、核心方法
①从Buffer类中继承下来的方法:
1、clear() 清空缓存区

    public final Buffer clear() {        position = 0;        limit = capacity;        mark = -1;        return this;    }

从源码中可以看出,并不是真正的把数据清楚,而是改变了各个参数的值,其实就是把各个指针还原到原始状态。通常这个方法在复用一个缓冲器的时候使用。

2、hasRemaining() : 判断当前缓冲器中是否有可用数据

    public final boolean hasRemaining() {        return position < limit;    }

从源码中就可以看出,其实就是看当前操作的position指针有没有和limit指针重合。这个方法主要是在获取缓冲器中的数据的时候进行判断。

3、flip(): 用于重置操作指针position,规整缓冲器中的数据,保证position与limit之间的数据都是有效的

   public final Buffer flip() {        limit = position;        position = 0;        mark = -1;        return this;    }

从源码中可以看出,相当于对缓冲器的操作复位一样,注意复位不是清空,而且,这个操作会把有效数据的结尾定义到当前操作的位置上。所以,这个方法的错误调用可能会导致缓冲器的失效,比如说你position为0的时候调用这个方法,那么有效数据就直接变为了0。

4、rewind():操作指针复位。相比于flip方法来说,这个方法不保证position与limit之间的数据都是有效的,而且不会意外的清除数据。

   public final Buffer rewind() {        position = 0;        mark = -1;        return this;    }

这个方法一般适用于需要循环遍历某一个缓冲器的时候,比如说第一次操作从头读到尾,第二次进来需要先调用rewind方法,然后开始读,不然就读不到数据。

5、mark() 标记当前position位置

   public final Buffer mark() {        mark = position;        return this;    }

这个方法主要用于标记当前操作位置,对于特定操作,比如说对一个缓冲器中所有奇数位的数据进行处理。那么我们需要记录上一次操作位置,这个时候就可以用mark来标记。

6、reset() 把当前操作指针position指向mark的地方

   public final Buffer reset() {        int m = mark;        if (m < 0)            throw new InvalidMarkException();        position = m;        return this;    }

一般的,如果使用到了这个方法,那么前提是已经使用了mark方法,不然mark的默认是-1位置。

其实Buffer中的方法不仅仅到此。下面这段代码囊括了上面表述的所有方法:

package com.brickworkers.io.nio;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class ChannelAndBufferTest {    public static void main(String[] args) throws IOException {        //建立2个通道,分别是输入和输出        FileChannel in = new FileInputStream("F:/java/io/bw.txt").getChannel(),                    out = new FileOutputStream("F:/java/io/bw.txt").getChannel();        //建立一个通用缓冲器        ByteBuffer bf = ByteBuffer.allocate(64);        //get这个缓冲器中包装一个值        bf.put("brickworkers".getBytes());        //注意,在这里其实position指针的位置已经变化了,必须把操作指针移动到开始处,建议使用filp方法,因为brickwork字节数是小于64的,避免不必要的开销        bf.flip();        //把缓冲器交给输出通道,把值写入文件        out.write(bf);        out.close();        //写完之后,要把bf清空用于存储从输入通道获取的值        bf.clear();        //输入通道把值交给缓冲器        in.read(bf);        in.close();        //先把position指针回归的原始位置,这里既可以用flip也可以用rewind。        bf.flip();        //我们要把相邻的字符进行位置交换        while(bf.hasRemaining()){            //先用mark进行标记,表示当前操作位置            bf.mark();            byte ch1 = bf.get();            byte ch2 = bf.get();            //操作完这些的时候其实position指针已经向后移动了2个字节。在进行交换的时候必须把position指针从新移动到交换对象之前            bf.reset();            //进行位置交换            bf.put(ch2).put(ch1);        }        //重置position,因为在上面置换操作已经结束,所以既可以使用filp方法,也可以使用rewind方法        bf.flip();        while(bf.hasRemaining()){            System.out.print((char)bf.get());        }    }}//输出结果://rbciwkroeksr//

但是,上面这段代码存在一个很大的问题,可能会抛出java.nio.BufferUnderflowException错误,之所以会产生这个错误是因为你写入的字符串是奇数还是偶数。如果是偶数经过两次的get(),其实position指针已经超过了limit。所以其实上面这段代码的第一个while中的hasRemaining()方法最好还是使用这个:remaining() > 2来判断会可靠的多,意思就是当最后不足2个的时候,最后一个字母不置换。大家可以自己尝试一下。

②创建一个合适的字节缓冲区视图,可以作为各种基本数据类型的缓冲区。其实就是获取一个更高级的缓冲器,比如说asCharBuffer,asIntBuffer,asDoubleBuffer等等。但是,能直接与通道交互的务必是ByteBuffer。
在上面的demo使用中,我们是用while循环输出字符,但是我们也可以用asCharBuffer直接把ByteBuffer转出CharBuffer,可以把上面的代码的最后while循环换成这样:

System.out.println(bf.asCharBuffer());

但是要注意,这里很有可能会产生乱码。在缓冲器ByteBuffer中存储的是字节,如果要转换成字符,要么对其输入的时候进行编码,要么就在输出的时候进行编码。我们一般用nio中的一个包java.nio.charset来指定编码,比如下面这段代码:

CharBuffer cb = Charset.forName("UTF-8").decode(bf);

我们就可以把ByteBuffer通过编码之后转换成CharBuffer。

②读取方法,读取缓冲器中的数据,get方法,可以读取对应的数据。可以读取完整字节数组,或者存在偏移的字节数组。甚至是getChar,getLong,getDouble等等方法。存储方法,把数据存储到position指针指向的位置。可以存储整个缓冲器的数据,或者存在偏移的字节数组。甚至是putChar,putLong,putDouble。其实这个相当于RandomAccessFile中的读写方式。具体请参考以下方法:

package com.brickworkers.io.nio;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.FileChannel;import java.nio.charset.Charset;public class Test {    public static void main(String[] args) throws IOException {        //建立2个通道,分别是输入和输出        FileChannel in = new FileInputStream("F:/java/io/bw.txt").getChannel(),                    out = new FileOutputStream("F:/java/io/bw.txt").getChannel();        //建立一个通用缓冲器        ByteBuffer bf = ByteBuffer.allocate(1024);        //先进行存储一个字节        bf.put((byte)'a');        //存入一个char        bf.putChar((char)'b');        //存入一个double        bf.putDouble(3.1415926D);        //存入一个long        bf.putLong(14L);        //存入一个int        bf.putInt(13);        //先把数据整理好,用flip方法。        bf.flip();        //把缓冲器中的数据运入输出通道        out.write(bf);        //先清空缓冲器        bf.clear();        in.read(bf);        bf.flip();        //接下来我们对数据进行读取        //读取一个字节        System.out.println(bf.get());        //读取一个char        System.out.println(bf.getChar());        //读取一个double        System.out.println(bf.getDouble());        //读取一个Long        System.out.println(bf.getLong());        //读取一个int        System.out.println(bf.getInt());    }}//输出结果://97//b//3.1415926//14//13////

有的小伙伴很奇怪,为什么我byte输入的是a,怎么变成97了呢?因为在ASCII编码中,a字母就是97。