Java入门系列-23-NIO(使用缓冲区和通道对文件操作)

NIO 是什么

java.nio全称java non-blocking(非阻塞) IO(实际上是 new io),是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。

NIO与IO的区别

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞(Non Blocking IO)
选择器(Selectors)

NIO系统的核心是:通道(Channel)和缓冲区(Buffer)

缓冲区(Buffer)

位于 java.nio 包,所有缓冲区都是 Buffer 抽象类的子类,使用数组对数据进行缓冲。

除了 boolean 类型,Buffer 对每种基本数据类型都有针对的实现类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

创建缓冲区通过 xxxBuffer.allocate(int capacity)方法

ByteBuffer buf1 = ByteBuffer.allocate(512);
LongBuffer buf2 = LongBuffer.allocate(1024);
……

缓冲区的属性

容量(capacity):表示缓冲区存储数据的最大容量,不能为负数,创建后不可修改。

限制:第一个不可以读取或写入的数据的索引,即位于 limit 后的数据不能读写。不能为负数,不能大于容量。

位置(position):下一个要读取或写入的数据的索引,位置不能为负数,不能大于 limit

标记(mark):标记是一个索引,通过 Buffer 中的 mark() 方法指 Buffer 中一个特定的 position,之后可以通过 reset() 方法回到这个 postion。

Buffer 的常用方法

方法名称 说明
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 将缓冲区的 limit 设置为当前位置,并将当前位置重置为0
int capacity() 返回 Buffer 的容量大小
boolean hasRemaining() 判断缓冲区是否还有元素
int limit() 返回 限制的位置
Buffer limit(int n) 将设置缓冲区界限为 n,并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n,并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设置为 0,取消设置的 mark

Buffer 所有子类提供了两个操作的数据的方法:get() 方法和 put() 方法

缓冲区存取数据操作

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer1 {

    public static void main(String[] args) {
        testuse();
    }

    public static void testuse() {
        //1.分配一个指定大小的缓冲区
        ByteBuffer buf=ByteBuffer.allocate(1024);

        System.out.println("---------------allocate()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //2.利用 put() 存入数据到缓冲区中
        String str="hello";
        //将字符串转为 byte 数组存入缓冲区
        buf.put(str.getBytes());
        System.out.println("---------------put()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //3.切换读取数据模式
        buf.flip();
        System.out.println("---------------flip()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //4.利用get() 读取缓冲区中的数据
        byte[] data=new byte[buf.limit()];
        System.out.println("---------------get()----------------");
        buf.get(data);
        System.out.println(new String(data,0,data.length));
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //5.rewind() 重复读
        buf.rewind();
        System.out.println("---------------rewind()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //6.clear() 清空缓冲区,但缓冲区中的数据依然存在
        buf.clear();
        System.out.println("---------------clear()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        System.out.println((char)buf.get());
    }
}

使用 mark()方法标记

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer2 {

    public static void main(String[] args) {
        testmark();
    }

    public static void testmark() {
        String str="jikedaquan.com";
        //创建缓冲区
        ByteBuffer buf=ByteBuffer.allocate(1024);
        //存入数据
        buf.put(str.getBytes());
        //切换模式
        buf.flip();
        //临时数组用于接收缓冲区获取的数据,长度与缓冲区 limit 一值
        byte[] data=new byte[buf.limit()];
        //获取缓冲区的数据从0开始获取4个,存入 data 数组中
        buf.get(data, 0, 4);
        //将数组转为字符串打印
        System.out.println(new String(data,0,4));
        //打印 position
        System.out.println(buf.position());

        //标记
        buf.mark();
        System.out.println("---------------再次获取----------------");
        //从索引4开始,获取6个字节(余下数据)
        buf.get(data, 4, 6);
        System.out.println(new String(data,4,6));
        System.out.println(buf.position());

        //恢复到标记位置
        buf.reset();
        System.out.println("---------------reset()----------------");
        System.out.println(buf.position());

        //判断缓冲区是是有还有剩余数据
        if (buf.hasRemaining()) {
            //获取缓冲区中可以操作的数量
            System.out.println("可操作数量:"+buf.remaining());
        }
    }
}

mark <= position <= limit <= capacity

虽然使用了缓冲区提高了一定的IO速度,但这样的效率仍然不是最高的。非直接缓冲区在与物理磁盘操作中需要经过内核地址空间copy操作,直接缓冲区不经过copy操作,直接操作物理内存映射文件,
使用直接缓冲区将大大提高效率。

直接缓冲区进行分配和取消分配所需成本工厂高于非直接缓冲区,一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

直接缓冲区可以通过调用此类的 allocateDirect()工厂方法创建

创建直接缓冲区

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer3 {

    public static void main(String[] args) {
        testAllocateDirect();
    }

    public static void testAllocateDirect() {
        //创建直接缓冲区
        ByteBuffer buf=ByteBuffer.allocateDirect(1024);
        
        //是否是直接缓冲区
        System.out.println(buf.isDirect());
    }
}

通道(Channel)

缓冲区仅是运载数据的容器,需要对数据读写还需要有一条通道,这两者是密不可分的。

Channel 接口的主要实现类:

  • FileChannel:用于读取、写入、映射和操作文件的通道
  • DatagramChannel:通过 UDP 读写网络中的数据通道
  • ScoketChannel:通过 TCP 读写网络中的数据
  • ServerScoketChannel:可以监听新进来的 TCP 链接,对每一个新进来的连接都会创建一个 SocketChannel

如何获取通道?

1、通过支持通道的对象调用 getChannel() 方法

支持通道的类:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramScoket
  • Socket
  • ServerScoket

使用通道和缓冲区实现文件读和写

package testnio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestChannel {

    public static void main(String[] args) {
        FileInputStream fis=null;
        FileOutputStream fos=null;
        
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        
        try {
            //创建输入流
            fis=new FileInputStream("F:/1.jpg");
            //创建输出流
            fos=new FileOutputStream("F:/2.jpg");
            //获取通道
            inChannel=fis.getChannel();
            outChannel=fos.getChannel();
            
            //分配指定大小的缓冲区
            ByteBuffer buf=ByteBuffer.allocate(1024);
            
            //将通道中的数据存入缓存区
            while(inChannel.read(buf)!=-1) {
                //切换读取数据的模式
                buf.flip();
                //将读入的缓冲区存入写数据的管道
                outChannel.write(buf);
                //清空缓存区(清空才能再次读入)
                buf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fis!=null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos!=null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel!=null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
}

2、通过通道类的静态方法 open()

package testnio;

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestOpenAndMapped {

    public static void main(String[] args) {
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            //通过open创建通道
            inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
            outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);

            //内存映射文件   直接缓冲区
            MappedByteBuffer inMappedBuf=inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
            MappedByteBuffer outMappedBuf=outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());

            //直接对缓冲区进行数据的读写操作
            byte[] data=new byte[inMappedBuf.limit()];
            inMappedBuf.get(data);//读
            outMappedBuf.put(data);//写
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel!=null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JDK 1.7 新增的方法 open(),参数 path 通常代表一个依赖系统的文件路径,通过Paths.get()获取。

参数 StandardOpenOption 是一个枚举类型,常用值如下:

  • READ :打开读访问
  • WRITE:打开写访问
  • APPEND:向后追加
  • CREATE:创建新文件,存在则覆盖
  • CREATE_NEW:创建新文件,存在则报错

MappedByteBuffer:直接字节缓冲区,其内容是文件的内存映射区域。

通道数据传输

将数据从源通道传输到其他 Channel 中,transferTo() 和 transferFrom()

package testnio;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestChannelTransfer {

    public static void main(String[] args) {
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            //通过open创建管道
            inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
            outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
            //将inChannel中所有数据发送到outChannel
            //inChannel.transferTo(0, inChannel.size(), outChannel);
            outChannel.transferFrom(inChannel, 0, inChannel.size());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel!=null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Channel 负责传输,Buffer 负责存储