事情的开始

  1.4版本开始,java提供了另一套IO系统,称为NIO,(New I/O的意思),NIO支持面向缓冲区的、基于通道的IO操作。

  1.7版本的时候,java对NIO系统进行了极大的扩展,增强了对文件处理和文件系统特性的支持。

  在不断的进化迭代之中,IO的很多应用场景应该推荐使用NIO来取代。

  NIO系统构建于两个基础术语之上:缓冲区和通道。

缓冲区

Buffer类

  缓冲区是一个固定数据量的指定基本类型的数据容器,可以将它理解成一块内存,java将它封装成了Buffer类。

  每个非布尔基本数据类型都有各自对应的缓冲区操作类,所有缓冲区操作类都是Buffer类的子类。

  除了存储的内容之外,所有的缓冲区都具有通用的核心功能:当前位置、界限、容量。

  当前位置是要读写的下一个元素的索引

  界限是缓冲区中最后一个有效位置之后下一个位置的索引值

  容量是缓冲区能够容纳的元素的数量,一般来说界限等于容量。

  对于标记、位置、限制和容量值遵守以下不变式:0 <= 标记 <= 位置 <= 限制 <= 容量

方法列表:

 
清除、反转、和重绕

  这三个词是在查阅JDK文档看到的,对应Buffer类的三个方法,个人觉得非常有助于理解。

  clear()使缓冲区为一系列新的通道读取或相对放置 操作做好准备:它将限制设置为容量大小,将位置设置为 0。

  flip()使缓冲区为一系列新的通道写入或相对获取 操作做好准备:它将限制设置为当前位置,然后将位置设置为 0。

  rewind()使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。

数据传输

  下面这些特定的缓冲区类派生字Buffer,这些类的名称暗含了他们所能容纳的数据类型:

  ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、MappedByteBuffer、ShortBuffer

  其中 MappedByteBuffer是ByteBuffer的子类,用于将文件映射到缓冲区。

  所有的缓冲区类都定义的有get()和put()方法,用于存取数据。(当然,如果缓冲区是只读的,就不能使用put操作)

通道

通道的用处

  通道,表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接,用于 I/O 操作的连接。

  通过通道,可以读取和写入数据。拿 NIO与原来的I/O 做个比较,通道就像是流,但它是面向缓冲区的。

  正如前面提到的,所有数据都通过 Buffer 对象来处理。你永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

  通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。

  通道实现了Channel接口并且扩展了Closeable接口和AutoCloseable接口,通过实现AutoCloseable接口,就可以使用带资源的try语句管理通道,那么当通道不再需要时会自动关闭。

获取通道

  获取通道的一种方式是对支持通道的对象调用getChannel()方法。

  例如,以下IO类支持getChannel()方法:

DatagramSocket、FileInputStream、FileOutputStream、RandomAccessFile、ServerSocket、Socket

  根据调用getChannel()方法的对象类型返回特定类型的通道,比如对FileInputStream、FileOutputStream或RandomAccessFile对象调用getChannel()方法时,会返回FileChannel类型的通道,对Socket对象调用getChannel()方法时,会返回SocketChannel类型的通道。

  通道都支持各种read()和write()方法,使用这些方法可以通过通道执行IO操作。

方法如下:

 
字符集和选择器

  NIO使用的另外两个实体是字符集和选择器。

  字符集定义了将字节映射为字符的方法,可以使用编码器将一系列字符编码成字节,使用解码器将一系列字节解码成字符。

  字符集、编码器和解码器由java.nio.charset包中定义的类支持,因为提供了默认的编码器和解码器,所以通常不需要显式的使用字符集进行工作。

  选择器支持基于键的,非锁定的多通道IO,也就是说,它可以通过多个通道执行IO,当然,前提是通道需要调用register方法注册到选择器中,

  选择器的应用场景在基于套接字的通道。

Path接口

  Path是JDK1.7新增进来的接口,该接口封装了文件的路径。

  因为Path是接口,不是类,所以不能通过构造函数直接创建Path实例,通常会调用Paths.get()工厂方法来获取Path实例。

get()方法有两种形式:

  Path get(String pathname,String ...more)
  Path get(URI uri)

  创建链接到文件的Path对象不会导致打开或创建文件,理解这一点很重要,这仅仅只是创建了封装文件目录路径的对象而已。

以下代码示例常用用法(1.txt是一个不存在的文件):

        Path path = Paths.get("./nio/src/1.txt");
        System.out.println("自身路径:"+path.toString());//输出.\nio\src\1.txt
        System.out.println("文件或目录名称:"+path.getFileName());//输出1.txt
        System.out.println("路径元素数量:"+path.getNameCount());//输出4
        System.out.println("路径中第3截:"+path.getName(2));//输出src
        System.out.println("父目录的路径"+path.getParent());//输出.\nio\src
        System.out.println(path.getRoot());//输出null
        System.out.println("是否绝对路径:"+path.isAbsolute());//输出false

        Path p = path.toAbsolutePath();//返回与该路径等价的绝对路径
        System.out.println("看看我这个是不是绝对路径:"+p.toString());//输出E:\JAVA\java_learning\.\nio\src\1.txt

        File file = path.toFile();//从该路径创建一个File对象
        System.out.println("文件是否存在:"+file.exists());//false

        Path path1 = file.toPath();//再把File对象转成Path对象
        System.out.println("是不是同一个对象:"+path1.equals(path));//输出true

为基于通道的IO使用NIO

通过通道读取文件

手动分配缓冲区

  这是最常用的方式,手动分配一个缓冲区,然后执行显式的读取操作,读取操作使用来自文件的数据加载缓冲区。

  

        try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){
            ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小
            int count = seekableByteChannel.read(buffer);//将文件中的数据读取到缓冲区
            buffer.rewind();
            while (count > 0){
                System.out.println((char)buffer.get());//读取缓冲区中的数据
                count --;
            }
        }catch (Exception e){
            e.printStackTrace();
        }

  该示例使用了SeekableByteChannel对象,该对象封装了文件操作的通道,可以转成FileChannel(不是默认的文件系统不能转)。这里注意,分配缓冲区大小就代表了最多读取的数据字节大小,比如我的示例文件中字节数是8个,但是我只分配了5个字节的缓冲区,因此只能读出前5个字节的数据。

  为什么会有buffer.rewind()这行代码呢?因为调用了read()方法将文件内容读取到缓冲区后,当前位置处于缓冲区的末尾,所以要重绕缓冲区,将指针重置到缓冲区的起始位置。

将文件映射到缓冲区

  这种方式的优点是缓冲区自动包含文件的内容,不需要显式的读操作。同样的要先获取Path对象,再获取文件通道。

  用newByteChannel()方法得到的SeekableByteChannel对象转成FileChannel类型的对象,因为FileChannel对象有map()方法,将通道映射到缓冲区。

map()方法如下所示:

  MappedByteBuffer map(FileChannel.MapMode how,long begin,long size) throws IOException

参数how的值为:MapMode.READ_ONLY、MapMode.READ_WRITE、MapMode.PRIVATE 之一。

  映射的开始位置由begin指定,映射的字节数由size指定,作为MappedByteBuffer返回指向缓冲区的引用,MappedByteBuffer是ByteBuffer的子类,一旦将文件映射到缓冲区,就可以从缓冲区读取文件了。

        try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){
            long size = fileChannel.size();//获取文件字节数量
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,0,size);
            for(int i=0;i < size; i ++){
                System.out.println((char)mappedByteBuffer.get());
            }
        }catch (Exception e){
            e.printStackTrace();
        }

通过通道写入文件

手动分配缓冲区
        try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.APPEND)){
            ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小
            for(int i=0;i<5;i++){
                buffer.put((byte)('A'+i));
            }
            buffer.rewind();
            seekableByteChannel.write(buffer);
        }catch (Exception e){
            e.printStackTrace();
        }

因为是针对写操作而打开文件,所以参数必须指定为StandardOpenOption.WRITE,如果希望文件不存在就创建文件,可以指定StandardOpenOption.CREATE,但是我还希望是以追加的形式写入内容,所以又指定了StandardOpenOption.APPEND。

  需要注意的是buffer.put()方法每次调用都会向前推进当前位置,所以在调用write()方法之前,需要将当前位置重置到缓冲区的开头,如果没有这么做,write()方法会认为缓冲区中没有数据。

将文件映射到缓冲区
        Path path = Paths.get("./nio/src/4.txt");
        try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(path,StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE)){
            MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
            for(int i=0;i < 5; i ++){
                buffer.put((byte) ('A'+i));
            }
        }catch (Exception e){
            e.printStackTrace();
        }

可以看出,对于通道自身并没有显式的写操作,因为缓冲区被映射到文件,所以对缓冲区的修改会自动反映到底层文件中。

  映射缓冲区要么是只读,要么是读/写,所以这里必须是READ和WRITE两个选项都得要。一旦将文件映射到缓冲区,就可以向缓冲区中写入数据,并且这些数据会被自动写入文件,所以不需要对通道执行显式的写入操作。

  另外,写入的文件大小不能超过缓冲区的大小,如果超过了之后会抛出异常,但是已经写入的数据仍然会成功。比如缓冲区5个字节,我写入10个字节,程序会抛出异常,但是前5个字节仍然会写入文件中。

使用NIO复制和移动文件

        Path path = Paths.get("./nio/src/4.txt");
        Path path2 = Paths.get("./nio/src/40.txt");
        try{
            Files.copy(path2,path, StandardCopyOption.REPLACE_EXISTING);
            //Files.move(path,path2, StandardCopyOption.REPLACE_EXISTING);
        }catch (Exception e){
            e.printStackTrace();
        }

StandardCopyOption.REPLACE_EXISTING选项的意思是如果目标文件存在则替换。

为基于流的IO使用NIO

  如果拥有Path对象,那么可以通过调用Files类的静态方法newInputStream()或newOutputStream()来得到连接到指定文件的流。

方法原型如下:

  static InputStream newInputStream(Path path,OpenOption... how) throws IOException

how的参数值必须是一个或多个由StandardOpenOption定义的值,如果没有指定选项,默认打开方式为StandardOpenOption.READ。

  一旦打开文件,就可以使用InputStream定义的任何方法。

  因为newInputStream()方法返回的是常规流,所以也可以在缓冲流中封装流。

        try(BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get("./nio/src/2.txt")))){
            int s = inputStream.available();
            for(int i=0;i<s;i++){
                int c = inputStream.read();
                System.out.print((char) c);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

OutputStream和前面的InputStream类似:

        try(BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get("./nio/src/2.txt")))){
            for(int i=0;i<26;i++){
                outputStream.write((byte)('A'+i));
            }
        }catch (Exception e){
            e.printStackTrace();
        }

为基于文件系统使用NIO

Files类

  要进行操作的文件是由Path指定的,但是对文件执行的许多操作都是由Files类中的静态方法提供的。

  java.nio.file.Files类就是为了替代java.io.File类而生。

以下列出部分常用方法:

参数列表中出现的有类型为OpenOption的参数,它是一个接口,真实传入的参数是StandardOpenOption类中的枚举,这个枚举参数与newBufferedWriter/newInputStream/newOutputStream/write方法一起使用。

下面演示追加写入文件操作:

        try{
            Path path = Paths.get("./nio/src/8.txt");
            String str = "今天天气不错哦\n";
            Files.write(path,str.getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        }catch (Exception e){
            e.printStackTrace();
        }

目录流

遍历目录

  如果Path中的路径是目录,那么可以使用Files类的静态方法newDirectoryStream()来获取目录流。

方法原型如下:

  static DirectoryStream<Path> newDirectoryStream(Path dir) throw IOException

  调用此方法的前提是目标必须是目录,并且可读,否则会抛异常。

        try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"))){
            for(Path path : paths){
                System.out.println(path.getFileName());
            }
        }catch (Exception e){
            e.printStackTrace();
        }

  DirectoryStream<Path>实现了Iterable<Path>,所以可以用foreach循环对其进行遍历,但是它实现的迭代器针对每个实例只能获取一次,所以只能遍历一次。

匹配内容

  Files.newDirectoryStream方法还有一种形式,可以传入匹配规则:

  static DirectoryStream<Path> newDirectoryStream(Path dir,String glob) throws IOException

  第二个参数就是匹配规则,但是它不支持强大的正则,只支持简单的匹配,如"?"代表任意1个字符,"*"代表任意个任意字符。

使用示例 匹配所有.java结尾的文件:

        try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"),"*.java")){
            for(Path path : paths){
                System.out.println(path.getFileName());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
复杂匹配

这种方式的原型为:

  static DirectoryStream<Path> newDirectoryStream(Path dir,DirectoryStream.Filter<? super Path> filter) throws IOException

其中的DirectoryStream.Filter是定义了以下方法的接口:

  boolean accept(T entry) throws IOException

这个方法中如果希望匹配entry就返回true,否则就返回false,这种形式的优点是可以基于文件名之外的其他内容过滤,比如说,可以只匹配目录、只匹配文件、匹配文件大小、创建日期、修改日期等各种属性。

下面是匹配文件大小的示例:

        String dirname = "./nio/src";
        DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
            @Override
            public boolean accept(Path entry) throws IOException {
                if(Files.size(entry) > 25){
                    return true;
                }
                return false;
            }
        };
        try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get(dirname),filter)){
            for(Path path : paths){
                System.out.println(path.getFileName());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
目录树

  遍历目录下的所有资源以往的做法都是用递归来实现,但是在NIO.2的时候提供了walkFileTree方法,使得遍历目录变得优雅而简单,其中涉及4个方法,根据需求选择重写。

示例如下:

        String dir = "./nio";
        try{
            Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>(){
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问文件:"+file);
                    return super.visitFile(file, attrs);
                }

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问目录:"+dir);
                    return super.preVisitDirectory(dir, attrs);
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    System.out.println("访问失败的文件:"+file);
                    return super.visitFileFailed(file, exc);
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    System.out.println("这个目录访问结束了:"+dir);
                    return super.postVisitDirectory(dir, exc);
                }
            });
        }catch (Exception e){
            e.printStackTrace();
        }

打印结果如图:

JAVA 探究NIO-LMLPHP

文件加锁机制

  JDK1.4引入的文件加锁机制,要锁定一个文件,可以调用FileChannel类的lock或tryLock方法

  FileChannel channel = FileChannel.open(path);
  FileLock lock = channel.lock() 或者 FileLock lock1 = channel.tryLock()

第一个调用会阻塞直到获得锁,第二个调用立刻就会返回 要么返回锁 要么返回Null。

  获得锁后这个文件将保持锁定状态,直到这个通道关闭,或者释放锁:lock.release(); 点进源码可以轻易发现,FileChannel实现了AutoCloseable接口,也就是说,可以通过try语句来自动管理资源,不需要手动释放锁。

  还可以锁定文件内容的一部分:

  FileLock lock(long start,long size,boolean shared)
  FileLock lock(long start,long size,boolean shared)

  锁定区域为(从start到start+size),那么在start+size之外的部分不会被锁定。shared参数为布尔值,代表是否是读锁,读锁就是共享锁,写锁就是排他锁。

源码分享

https://gitee.com/zhao-baolin/java_learning/tree/master/nio

11-30 19:27