走进Java NIO的世界


Java NIO 简介

Java NIO,即Java New IO,是Java IO的2.0版本,since from JDK1.4。JDK1.4以前提供的都是传统的IO,即我们经常使用的InputStream/OutputStream/Reader/Writer等。对于传统IO,我们可以利用流的装饰功能,使其具有Buffer功能,本质上是利用byte[]完成的。而Java NIO单独把Buffer的功能抽取出来,而且还提供了很多特性,下面我们一起来看看吧~

Buffer家族

看下java.nio.Buffer的子类:

走进Java NIO的世界

对于基本数据类型,基本上都有与之对应的Buffer类,而ByteBuffer最常用,ByteBuffer下面又有2个特殊的子类。

ByteBuffer

先来一段代码,有点感性认识吧:

public static void main(String[] args) {
    //分配Buffer缓冲区大小  其本质是指定byte[]大小
    ByteBuffer buffer = ByteBuffer.allocate(10);
    printBuffer(buffer);
    buffer.put((byte)1);
    printBuffer(buffer);
    buffer.flip();
    printBuffer(buffer);
}
public static void printBuffer(Buffer buffer){
    System.out.println("--------");
    System.out.println("position : " + buffer.position());
    System.out.println("limit : " + buffer.limit());
    System.out.println("capacity : " + buffer.capacity());
    System.out.println("--------");
}

既然要使用ByteBuffer,必然要知道如何创建它!常见的创建ByteBuffer的方式有如下几种:

分配堆内存的方式

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

分配直接内存,即C HEAP的方式

  public static ByteBuffer allocateDirect(int capacity) {

        return new DirectByteBuffer(capacity);
  }

需要注意的是,DirectByteBuffer是MappedByteBuffer的子类!

直接包装byte[]形成ByteBuffer

public static ByteBuffer wrap(byte[] array,
    int offset, int length){
        try {
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }

由于ByteBuffer是atstract class,因此我们使用的都是它的2个具体子类:HeapByteBuffer/MappedByteBuffer。

我们可以跟踪下HeapByteBuffer/DirectByteBuffer的构造方法,发现它们其实就做了一件事:

初始化byte[]以及一些属性,比如mark,position,limit,capacity。

position vs limit vs capacity

Java NIO中ByteBuffer除了有byte[]之外,还提供了一些属性,这样相比传统IO,操作更加灵活方便。

首先来说,capacity是byte[]的容量大小,一般是初始化好后,就不会在变化了的,而position,limit这2个属性,会随着对缓冲区的read/write操作而发生变化。

position:下一个应该读取的位置

limit:在byte[]中有效读取位置的最大值

下面,我们来做一个例子具体说明:利用ByteBuffer来拷贝文件

public static void closeStream(Closeable... closeable){
    for(Closeable c : closeable){
        if(c != null){
            try {
                c.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args) throws IOException {
   
    FileInputStream srcFile = new FileInputStream("E:\\tmp\\Shell学习笔记.pdf");
    FileOutputStream  destFile = new FileOutputStream("E:\\tmp\\Shell学习笔记COPY.pdf");
   
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
   
    FileChannel in = srcFile.getChannel();
    FileChannel out = destFile.getChannel();
   
    while(in.read(byteBuffer) != -1){
        byteBuffer.flip();
        out.write(byteBuffer);
        byteBuffer.clear();
    }
    closeStream(srcFile,destFile);
}

其实,通过上面的代码,我们已经揭示了Java NIO的3个核心概念中的2个:缓冲区与通道

以前,对于传统IO,我们面对的是流,操作的是一个个字节,而NIO,我们面对的是缓冲区,操作的将是一个个块。

具体来说,是这样的:

读取输入,比如读取文件,那么应该通过FileInputStream/RandomAccessFile进行获取通道;创建缓冲区buffer;然后调用通道的read操作,将数据读入buffer。写操作,则相反。

上面代码中,调用了buffer的2个重要方法:flip()/clear(),他们是干嘛的呢?

直接看源码:

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

flip并没有做什么,只是将limit的位置设置为position,而position的位置回到0

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

clear则更加简单,回到最初的状态!

为什么要调用他们来改变limit/positon呢?

要知道,如果channel.read(buffer),这个操作,是要改变position的;如果我们继续otherchannel.write(buffer),那么将写入的是未知数据。最好的方式,是将position现在所处的位置交给limit,而position置为0,这样就到达了将缓冲区的内容重新读出!而调用clear的目的,就更加单纯,就是希望在read(buffer)的时候从0开始!

mark是来做什么的?

在buffer中,mark默认是被置为-1的。我们先来看看与mark有直接关系的2个方法:

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


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

通过mark()我们利用mark记住了position,而通过reset()我们将position的值还原到mark。

那么事实就清楚了,我们可以先调用mark()记住当前POSITION的位置,然后我们去做其他的一些事情,最后通过reset()在找回POSITION的位置开始下一步!

allocateDirect

allocateDirect方式创建的是一个DirectByteBuffer,直接内存,这是用来做什么的呢?

我们可以先来看看常规的IO操作流程:

走进Java NIO的世界

很显然,JVM只是普通的用户进程,能够和IO设备打交道的是KERNEL空间,JVM需要从KERNEL拷进INPUT DATA,拷出OUTPUT DATA到KERNEL。当然,频繁的拷进拷出操作是费时的。而DirectBuffer将跳过JVM拷进拷出这一层。

走进Java NIO的世界

MappedByteBuffer:内存映射IO

我们经常是在内存中分配一段空间,操作完毕后,写入磁盘;那么能不能在磁盘上直接分配一段空间,供我们进行IO操作呢?MappedByteBuffer就是这样的,它会在磁盘上分配一段缓冲区,对缓存区的操作就是对磁盘的操作!

来看看“高性能”的拷贝文件方式:利用MappedByteBuffer

public static void main(String[] args) throws IOException {
    //FileInputStream fis = new FileInputStream("E:\\tmp\\Shell学习笔记.pdf");
    //FileOutputStream fos = new FileOutputStream("E:\\tmp\\Shell学习笔记COPY.pdf");
    RandomAccessFile fis = new RandomAccessFile("E:\\tmp\\Shell学习笔记.pdf","r");
    RandomAccessFile fos = new RandomAccessFile("E:\\tmp\\Shell学习笔记COPY.pdf","rw");
    FileChannel in = fis.getChannel();
    FileChannel out = fos.getChannel();
    long size = in.size();
    ByteBuffer buffer = out.map(MapMode.READ_WRITE, 0, size);
    in.read(buffer);
    closeStream(fis,fos);
}

可以看得出,先利用FileChannel的map方法获取一个可读、可写的position=0,大小为size的MappedByteBuffer,对这个buffer的操作就将直接反映到磁盘上!

【注意到,利用FileInputStream获取到的CHANNEL是只读的,利用FileOutputStream获取到的CHANNEL是只写的,而map获取BUFFER需要读写权限,因此要利用RandromAccessFile来进行读写设置!】

到这里,JAVA NIO就介绍了一部分内容了,我也从知道有NIO,到开始实践NIO了,HAPPY.....

本文永久更新链接地址

相关内容