JAVA 探究NIO

时间:2024-01-22 07:53:12

事情的开始

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

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

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

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

缓冲区

Buffer类

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

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

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

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

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

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

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

方法列表:

方法 描述
Object array() 返回此缓冲区的底层实现数组
int arrayOffset() 返回此缓冲区的底层实现数组中第一个元素的索引
int capacity() 返回此缓冲区的容量
Buffer clear() 清除此缓冲区并返回缓冲区的引用
Buffer flip() 将缓冲区的界限设置为当前位置,并将当前位置重置为0,即反转缓冲区
boolean hasArray() 返回缓冲区是否具有可访问的底层实现数组。
boolean hasRemaining() 返回缓冲区中是否还有剩余元素
boolean isDirect() 返回此缓冲区是否是直接缓冲区(直接缓冲区可以直接对缓冲区进行IO)
boolean isReadOnly() 该缓冲区是否只读
int limit() 返回缓冲区的界限
Buffer limit(int n) 将缓冲区的界限设置为n
Buffer mark() 设置标记
int position() 返回此缓冲区的位置
Buffer position(int n) 将缓冲区的当前位置设置为n
int remaining() 返回当前位置与界限之间的元素数量(即界限减去当前位置的结果值)
Buffer reset() 将缓冲区的位置重置为之前设置标记的位置
Buffer rewind() 将缓冲区的位置设置为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操作。

方法如下:

方法 描述
int read(ByteBuffer b) 将字节读取到缓冲区,返回实际读取的字节数
int read(ByteBuffer b,long start) 从start指定的文件位置开始,从通道读取字节,并写入缓冲区
int write(ByteBuffer b) 将字节从缓冲区写入通道
int write(ByteBuffer b,long start) 从start指定的文件位置开始,将字节从缓冲区写入通道

 

 

 

 

 

 
字符集和选择器

  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类而生。

以下列出部分常用方法:

方法 描述
static Path copy(Path from,Path to,CopyOption... opts) 将from复制到to,返回to
static Path move(Path from,Path to,CopyOption... opts) 将from移动到to,返回to
static Path createDirectory(Path path,FileAttribute<?> attribs) 创建一个目录,目录属性是由attribs指定的。
static Path createFile(Path path,FileAttribute<?> attrbs) 创建一个文件,文件属性是由attribs指定的。
static void delete(Path path) 删除一个文件
static boolean exists(Path path) path代表的路径是否存在(无论文件还是目录)
static boolean notExists(Path path) path代表的路径是否不存在(无论文件还是目录)
static boolean isRegularFile(Path path) 是否是文件
static boolean isDirectory(Path path) 是否是目录
static boolean isExecutable(Path path) 是否是可执行文件
static boolean isHidden(Path path) 是否是隐藏文件
static boolean isReadable(Path path) 是否可读
static boolean isWritable(Path path) 是否可写
static long size(Path path) 返回文件大小
static SeekableByteChannel newByteChannel(Path path,OpenOption... opts) 打开文件,opts指定打开方式,返回一个通道对象
static DirectoryStream<Path> newDirectoryStream(Path path) 打开目录,返回一个目录流
static InputStream newInputStream(Path path,OpenOption... opts) 打开文件,返回一个输入流
static OutputStream newOutputStream(Path path,OpenOption... opts) 打开文件,返回一个输出流

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

StandardOpenOption类中的枚举 描述
READ 用于读取打开文件
WRITE 用于写入打开文件
APPEND 如果是写入,则内容追加到末尾
CREATE 自动在文件不存在的情况下创建新文件
CREATE_NEW 创建新文件,如果文件已存在则抛出异常
DELETE_ON_CLOSE 当文件被关闭时删除文件
DSYNC 对文件内容的修改被立即写入物理设备
SYNC 对文件内容或元数据的修改被立即写入物理设备
TRUNCATE_EXISTING 如果用于写入而打开,那么移除已有内容

 

 

 

 

 

 

 

 

 

 

 

 

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

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

            FileChannel channel = FileChannel.open(path);
            FileLock lock = channel.lock();
            FileLock lock1 = channel.tryLock();
            lock.release();
        }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();
        }

打印结果如图:

文件加锁机制

  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