JAVA IO与NIO

176 阅读8分钟

IO

IO是指Input/Output,即输入和输出。以内存为中心:
Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

IO的基类:
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

以下为文件IO,创建文件并写入数据和拷贝文件的示例代码:


    protected void createFileAndWrite(String content, String filePath) throws IOException {
        try(FileOutputStream outputStream = new FileOutputStream(filePath);) {
            //字节数据写入输出流
            outputStream.write(content.getBytes(StandardCharsets.UTF_8));
            //将缓冲区数据刷到文件
            outputStream.flush();
        }
    }

   
    protected void copy(String srcPath, String desPath) throws IOException {
        try (
                FileInputStream inputStream = new FileInputStream(srcPath);
                FileOutputStream outputStream = new FileOutputStream(desPath);
        ){
            byte[] bytes = new byte[1024];
            int len;
            while ((len = inputStream.read(bytes)) != -1){
                outputStream.write(bytes,0, len);
            }
            //将缓冲区数据刷到文件
            outputStream.flush();
        }
    }

NIO

java 1.4 引入nio,目的是提升速度,同时原来的io也用nio重新做了实现,即使我们不显示的使用Nio也能从中受益。

NIO由三个核心部分组成:缓冲区(buffer)、管道(channel)、选择器(seletor)

管道Channel可以比做铁路,缓冲区ByteBuffer可以比做运载货物的火车,管道不与数据打交道,只负责运输

以下为文件IO,创建文件并写入数据和拷贝文件的示例代码(本示例不涉及选择器):

    protected void createFileAndWrite(String content, String filePath) throws IOException {
        try(FileOutputStream outputStream = new FileOutputStream(filePath);
            //获取管道FileChannel
            FileChannel fileChannel = outputStream.getChannel();
        ){
            //数据写入缓冲区ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8));
            //缓冲区数据写入管道
            fileChannel.write(byteBuffer);
        }
    }

    protected void copy(String srcPath, String desPath) throws IOException {
        try(FileInputStream inputStream = new FileInputStream(srcPath);
            FileOutputStream outputStream = new FileOutputStream(desPath);
            //分别获取输入输出对应的管道
            FileChannel inChannel = inputStream.getChannel();
            FileChannel outChanel = outputStream.getChannel();
        ){
            //分配一个缓冲区存用来储输入的数据
            ByteBuffer byteBuffer = ByteBuffer.allocate(inputStream.available());
            //把输入流管道的数据填充到输出流管道
            while (inChannel.read(byteBuffer)>0){
                //为写入数据write()作准备
                byteBuffer.flip();
                //缓冲
                outChanel.write(byteBuffer);
                //为下一次read()准备
                byteBuffer.clear();
            }
            //实际上,NIO提供了管道之间直接相连的方式,上述循环可以直接以下边一行代码代替
//            inChannel.transferFrom(outChanel,0, inChannel.size());

        }

接下来我们对上边代码示例里ByteBuffer的flip()和clear()方法的作用再做下解读。
在此之前,我们首先了解下ByteBuffer中非常重要的三个属性:
capacity: 容量,缓存区最大存储量(每次read到缓冲区的最大长度)
position: 指针所在位置,即下一个要读或者写的位置(数组的索引位)
limit: 缓冲区的数据总量(写入时limit的值即为容量,读取时为当前缓冲区已存在数据长度)

在上边的demo的copy方法中,我们通过inChannel.read(bytebuffer)将数据写入缓冲区,
然后通过outChanel.write(byteBuffer)将缓冲区的数据写到输出流。

那么问题来了,怎么做到使用同一个数组(缓冲区使用数组存储数据)边写入边输出呢,自然是通过我们上边的三个属性,下边我们通过打印三个属性各个阶段的值来进行观察。

我们改写下copy方法(定义缓冲区的容量为5,在read,flip,write,clear后打印capacity、position、limit的值):

    protected void copy(String srcPath, String desPath) throws IOException {
        try(FileInputStream inputStream = new FileInputStream(srcPath);
            FileOutputStream outputStream = new FileOutputStream(desPath);
            //分别获取输入输出对应的管道
            FileChannel inChannel = inputStream.getChannel();
            FileChannel outChanel = outputStream.getChannel();
        ){
            System.out.println("输入数据字节长度:"+inputStream.available());
            //分配一个‘非直接缓冲区’存用来储输入的数据
            ByteBuffer byteBuffer = ByteBuffer.allocate(5);
            int i=0;
            //把输入流管道的数据填充到输出流管道
            while (inChannel.read(byteBuffer)>0){
                i++;
                System.out.println("第"+i+"次遍历开始---------------------------");
                System.out.println("read后-------------");
                print(byteBuffer);
                //准备缓冲器数据以便写入管道
                byteBuffer.flip();
                System.out.println("flip后-------------");
                print(byteBuffer);
                //缓冲
                outChanel.write(byteBuffer);
                System.out.println("write后------------");
                print(byteBuffer);
                //为下一次read()准备
                byteBuffer.clear();
                System.out.println("clear后------------");
                print(byteBuffer);
                System.out.println("第"+i+"次遍历结束---------------------------");
            }

        }
    }

    private void print(ByteBuffer buffer){
        System.out.println("capacity:" + buffer.capacity()
                + ", position:" + buffer.position()
                + ", limit:" + buffer.limit());
    }
    

打印结果:

输入数据字节长度:121次遍历开始---------------------------
read-------------
capacity:5, position:5, limit:5
flip后-------------
capacity:5, position:0, limit:5
write------------
capacity:5, position:5, limit:5
clear后------------
capacity:5, position:0, limit:51次遍历结束---------------------------2次遍历开始---------------------------
read-------------
capacity:5, position:5, limit:5
flip后-------------
capacity:5, position:0, limit:5
write------------
capacity:5, position:5, limit:5
clear后------------
capacity:5, position:0, limit:52次遍历结束---------------------------3次遍历开始---------------------------
read-------------
capacity:5, position:2, limit:5
flip后-------------
capacity:5, position:0, limit:2
write------------
capacity:5, position:2, limit:2
clear后------------
capacity:5, position:0, limit:53次遍历结束---------------------------

综上,输入字节长度12,定义缓冲区容量5
read后填充数据到缓冲区,此时position为下一个要写入的位置,若position==capacity即缓冲区已满。
然后若将缓冲区数据输出到输出流,执行flip()方法将postion置为数据起始位置即0,limit为当前缓冲区的数据长度5。
然后执行write将缓冲区数据写入输出流,此时position又到了5(数据从起始位置遍历到此)
最后clear(),清掉缓冲区数据,position重置为0
第一次循环结束,输入流共12个字节,第一次遍历结束取走了5个字节。

第二次循环同第一次遍历。。。
第二次循环结束,再次取走5个字节,剩余2个字节

第三次循环开始
read后,缓冲区中存在两个字节,position=2, limit=5, capacity=5;
flip()后,指针回到数据的起始位置position=0,limit=2, capacity=5;
write()后,指针再次回到结束位置position=2,limit=2, capacity=5;
clear()后,position=0,limit=5, capacity=5;

最后再总结下
flip()方法的作用:
官方解释:翻转此缓冲区。将限制设置为当前位置,然后将该位置设置为零。如果定义了标记,则将其丢弃。
即将limit置为当前指针位置(等于缓冲区数据长度),同时position置为数据的初始位置0,为读取缓冲区数据做准备(如示例中,write()方法为从缓冲区中读取数据写入输出流)
clear()方法的作用:
官方解释:清除此缓冲区。位置设置为零,限制设置为容量,并且标记被丢弃。
官方解释很通俗流,在此不再赘述。(在上边的示例中,每次循环结束执行clear方法,清空缓冲区,重置position,为再次执行read方法向缓冲区中写入数据做准备)

直接缓冲区和非直接缓冲区

非直接缓冲区:
缓冲区的数据会在jvm堆栈空间中开辟空间存储。
而对于文件IO,物理磁盘的存取是操作系统进行管理的,与物理磁盘的数据操作需要经过内核地址空间;而我们的Java应用程序是通过JVM分配的缓冲空间。有点雷同于一个属于核心态,一个属于应用态的意思,而数据需要在内核地址空间和用户地址空间,在操作系统和JVM之间进行数据的来回拷贝。

直接缓冲区:
直接缓冲区不再需要将数据在内核和用户空间之间互相拷贝,而是在物理内存中申请了一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间的数据存取之间通过这块直接申请的物理内存进行。(内存映射文件)

直接缓冲区适合与数据长时间存在于内存,或者大数据量的操作时更加适合。

内存映射文件 大文件处理

通过内存映射文件,我们可以创建和修改哪些因太大而不能全部放入内存的文件,下方依次给出通过内存映射文件进行读写的代码示例

读取文件

    /**
     * 内存映射文件读
     * @param path 文件路径
     */
    private static void mmfRead(String path) throws IOException {
        //每次读取长度为7个字节
        int size = 7;
        try(FileInputStream inputStream = new FileInputStream(path);
            FileChannel fileChannel = inputStream.getChannel();
            ) {
            //输入流字节长度:12
            int fileLength = inputStream.available();
            int position = 0;
            //每次读取7个字节,共12个字节,需要循环两次
            while (position<fileLength){
                System.out.println("--------开始-------");
                MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, Math.min(fileLength - position, size));
                CharBuffer charBuffer = StandardCharsets.UTF_8.decode(mappedByteBuffer);
                position+=size;
                System.out.println(charBuffer.toString());
                System.out.println("--------结束-------");

            }
        }
    }

打印结果

--------开始-------
nio 测
--------结束-------
--------开始-------
试!!
--------结束-------

写入文件

/**
     * 内存映射文件写
     * @param descPath 待写入文件路径
     * @param data 待写入的数据
     */
    private static void mmfWrite(String descPath, String data) throws IOException {
        try(//需要注意的是此处需要使用RandomAccessFile来构建一个可读可写的Channel
            RandomAccessFile randomAccessFile = new RandomAccessFile(descPath, "rw");
            FileChannel fileChannel = randomAccessFile.getChannel();
        ) {
            byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, bytes.length);
            mappedByteBuffer.put(bytes);
        }
    }

IO与NIO的区别

IO是面向流的处理,NIO是面向块(缓冲区)的处理:
面向流的I/O 系统一次一个字节地处理数据,一个面向块(缓冲区)的I/O系统以块的形式处理数据。
IO为单向流,NIO通过Channel管道+ByteBuffer缓冲期实现了流的双向处理
IO流是阻塞的,NIO流是不阻塞的;
NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
NIO基于内存映射文件可以实现对大文件的处理,每次映射大文件的一小部分。