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

本文深入探讨Java NIO系统中的通道(FileChannel)与缓冲器(Buffer),重点解析ByteBuffer的使用方法及其实现原理,包括其类结构定义、构造函数、核心方法等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值