NIO还不完全是为了并发而开发的, 而是一套全新的IO体系. 这里也就一并看看1.4之后新增的各种IO

  1. NIO包
  2. Path对象与工具类Paths – 操作路径
  3. Files类 – 读写文件
  4. Path与Files类搭配使用 – 操作文件系统
  5. Path与Files类搭配使用 – 访问目录中的项
  6. nio的目录流

NIO包

NIO包是Java 1.4开始引入的, 后来每一版都对其中进行了更新. Java SE 8的NIO包文档在这里.

根据官方文档的nio包组成, 有如下部分:

  1. java.nio, Java SE 4新增, 直接位于nio包内的是缓冲区相关的类
  2. java.nio.channels, Java SE 4新增, 定义channel和selector, 所谓channel, 就是连接到一个能够进行I/O操作的实体的数据通道,
    比如文件或者socket. selector则类似多路复用的选择器.
  3. java.nio.charset, Java SE 4新增, 定义字符集, 编码解码器, 用于在字节和字符之间转换
  4. java.nio.file, Java 7 新增, 用于方便快捷的操作路径和文件

NIO最大的特点是可以实现不阻塞,不像老IO必定阻塞. 此外IO的概念变了, 老IO是流的概念, 而NIO定义了缓冲区buffer和管道channel, 另外还一个核心是selector. 新IO相当于从管道中获取数据,
放到缓冲区进行处理, 再重复这个过程.

java.nio.file是后来加的, 为了方便操作.

Path对象与工具类Paths – 操作路径

先不看NIO, 来看看新增的java.nio.file包, 这个包里有Path接口, Paths和Files等常用工具类.

在做B站视频的时候, 使用了Python的Path包中的方法, 当时还在想如果Java也有就好了. 实际上Java SE 7 开始就提供了Path对象, 位于java.nio.file中.

Path对象的使用很简单, 构造器接受字符串参数, 如果拼接起来能够解析成一个符合当前操作系统要求的路径(不一定要实际存在), 就算解析成功. 解析成功之后, 就得到了一个Path对象.

public static void main(String[] args) throws IOException {

    Path path = Paths.get("D:", "anewfile.file");

    System.out.println(path.getRoot());
    System.out.println(path.getParent());
}

创建Path对象的时候, "anewfile.file"不存在也没有关系. 如果在windows系统下, 将上边改成:

Path path = Paths.get("D:", "test2||");

就会报异常, 因为||不能用于windows的文件名.

还可以方便的解析或者说拼接路径, 比如当前路径是 d:\downloads, 想在其中找到存放暗杀神桌游信息的目录 d:\downloads\ascension, 可以不用写完整路径, 而是使用.resolve方法:

Path path = Paths.get("D:", "downloads");
//将path拼接上相对路径ascension
path = path.resolve("ascension");
System.out.println(path.toAbsolutePath());

如果resolve的参数是一个绝对路径, 那么就得到绝对路径, 所以很方便. 与之类似的还有一个resolveSibling, 用于直接替代一串路径的最后一部分, 也比较方便.

Path在旧的路径系统中对应的是File对象, 所以Path都有一个toFile()转换成File对象, 而File对象有toPath()转换成Path对象.

以后如果想便捷的操作路径名, 就可以考虑使用Path对象.

Files类 – 读写文件

相比旧IO的FileInputStream+套壳的操作, nio中使用Files类提供了更简便的方法. 写到这里, 发现如果我前几天编写转换Foobar的cue文件编码的程序如果使用NIO会简便不少. 看几个操作.

1 读字节

读取一个文件的所有字节然后按照编码转换成字符串, Files需要操作Path对象:

byte[] bytes = Files.readAllBytes(Paths.get("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版.cue"));

String content = new String(bytes, "GB2312");

System.out.println(content);

2 读字符

直接按照行和编码读出字符串, 这样就无需经过字节转换:

Path path = Paths.get("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版.cue");
List<String> content = Files.readAllLines(path, Charset.forName("GB2312"));

for (String s : content) {
    System.out.println(s);
}

红字部分也是NIO包中的方法, 可以用名称来查找对应的字符集. 注意使用这个方法读出行的时候, 换行符会被忽略. 还可以直接读全部字符串:

Files.readString(path, Charset.forName("GB2312"));

3 写入

最常见的是写字符串, 直接将字符串按照指定的字符集转换后写入即可:

//写字节
Files.write(path, content.getBytes(StandardCharsets.UTF_8));

//直接写字符串的简易方法
Files.write(path, content.getBytes(Charset.forName("GB2312")));

我前几天的那个程序, 如果采用nio, 实际上短短几行就能完成, 如下:

Path path = Paths.get("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版.cue");
Path newPath = Paths.get("D:\\downloads\\music\\王菲\\2000《寓言》内地引进版\\寓言 内地引进版UTF-8.cue");
String content = Files.readString(path, StandardCharsets.UTF_8);
Files.writeString(newPath, content, Charset.forName("GB2312"));

write方法还有重载, 第三个参数是nio.file中的OpenOption接口的实现类, 可以选择写策略, 是追加还是完全覆盖, 默认是完全覆盖, 追加如下:

Files.writeString(newPath, content, Charset.forName("GB2312"), StandardOpenOption.APPEND);

write还有不指定字符集的重载, 默认会使用UTF-8. write还有重载可以一次写一个集合, 比如write(path, lines), 万变不离其宗.

4 局限性

由于Files是一次性读出, 所以对于中等大小的文件操作比较方便, 但是大型文件, 还是要使用原有的内部带有缓冲区的老IO. Files也提供了从Path对象生成老IO对象的功能:

  1. Files.newBufferedReader(path, charset)
  2. Files.newBufferedWriter(path, charset)
  3. Files.newInputStream(path)
  4. Files.newOutputStream(path)

可见Java的包设计者还是非常用心的, 新老包可转换, 互相融合, 非常好.

Path与Files类搭配使用 – 增删改文件和目录

操作文件系统不外乎增删改查, 也就是新建, 删除, 移动等方法.

1 新建目录和文件

Files中提供了简便的方法, 可以创建一个文件和目录, 并同时返回对应的Path对象, 可以用于后续操作.

//创建文件

//仅仅只是一个path对象, 没有任何实际用途
Path path = Paths.get("d:\\test.txt");
//创建新文件
Files.createFile(path);

执行了Files.createFile(path);就已经创建了新文件, 如果文件已经存在, 就会报FileAlreadyExistsException异常. 调用这个方法是原子的, 要么创建成功,
要么抛异常.

创建目录如下:

Path path = Paths.get("d:\\test.txt");
//创建新目录
Files.createDirectory(path);

这个操作就不是创建一个test.txt文件, 而是创建了test.txt目录. 这个方法中的path除了最后一部分可解析内容之外, 都必须是实际存在的文件系统路径, 否则会报异常.

如果要一次递归创建所有不存在的目录, 就使用:

Path path = Paths.get("d:\\a\\b\\c\\test.txt");

//创建中间目录
Files.createDirectories(path);

这个目录会在D盘下依次创建a,b,c,test.txt四个目录.

2 复制与剪切

复制和剪切的命令比较简单, 如下:

Files.copy(fromPath,toPath)

//如果目标已存在, 会失败
Files.move(fromPath,toPath)

//可添加多个Options的重载方法, 比如ATOMIC_MOVE, 即原子移动, 还有很多
Files.move(fromPath,toPath, Options...)

Options的详细使用方法, 可以查看Core Java 中文卷2的88页, 之前的write()方法中的option也可以接受不只一个.

还可以在流和Path对象之间复制, 相当于将流存储到硬盘或者将文件发送到流中.

3 删除文件

直接删除的话, 如果不存在会报错, 所以可以用另外一个方法:

//如果不存在, 会报NoSuchFileException
Files.delete(path);

//文件不存在, 删除也不会报错的方法
boolean deleted = Files.deleteIfExists(path);

if (deleted) {
    System.out.println("删除成功");
} else {
    System.out.println("删除失败");
}

4 其他方法

常用的就是静态方法exists(path), 查看这个path对应的文件或者目录是否实际存在.

boolean deleted = Files.exists(path);

还有size(path)返回文件字节数, 其他的都是一些与文件属性相关的内容了, 操作要比原来简单一些.

还有isDirectory(path)超经典的判断是不是一个目录.

Path与Files类搭配使用 – 访问目录中的项

除了操作单个path, 最常用的就是列出一个目录中所有的项目以便继续操作了.Files提供了静态方法, 针对一个path对象, 给出其内部各个项的stream<Path>对象, 这样就很方便了.

Path path = Paths.get("d:\\downloads");

    Files.list(path).forEach(s->{
        if (Files.isDirectory(s)) {
            System.out.println("目录: " + s);
        } else {
            System.out.println("文件: " + s);
        }
    });

上边这个程序就可以区分目录还是文件, 然后列出D盘downloads目录下的所有文件和目录项目.

Files.list(path)仅仅针对一个目录, 当然还会有经典的walk也就是遍历所有目录了:

Path path = Paths.get("d:\\downloads");

Files.walk(path).forEach(s->{
    if (Files.isDirectory(s)) {
        System.out.println("目录: " + s);
    } else {
        System.out.println("文件: " + s);
    }
});

walk方法的参数是一个起始目录, 以这个目录为根遍历其中所有文件. walk还可以重载, 第二个参数用于控制深度. 从结果来看, walk()的遍历实际上是深度优先.

nio的目录流

细粒度的控制分为两种, 一种是不递归, 仅仅访问某个目录下边一层, 可以直接使用过滤条件. 还有一种是访问者模式, 在递归访问的时候可以进行工作.

1 不递归的过滤显示

Stream<Path>本质上还是流. nio中还提供了一个精细粒度控制的对象, 叫做DirectoryStream<Path>, 看名字好像是Stream的子类,
但其实不是.

这是一个继承了Iterable接口的类, 所以可以用来迭代. 这个不是walk, 只能返回某一个目录的下一层. 这个目录流对象的好处在于可以直接传递过滤对象:

Path path = Paths.get("d:\\downloads");

Files.newDirectoryStream(path, "*.pdf").forEach(System.out::println);

后边的红色部分就是GLOB模式, 这里使用的是windows的GLOB模式, 也就是文件通配符. 这个直接返回了过滤后的, 由所有.pdf组成的一个Iterable<Path>对象.

2 递归的访问者模式

想要在递归访问也就是walk()过程中精细控制, Files.walkFileTree()方法就登场了. 这个方法有两个重载:

public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
    
public static Path walkFileTree(Path start,
                                    Set<FileVisitOption> options,
                                    int maxDepth,
                                    FileVisitor<? super Path> visitor)

其中一个参数是一个FileVisitor接口类型, 这个接口也位于java.nio.file中, 根据JDK8官网文档, 目前已知的实现类就只有SimpleFileVisitor一个.

这个接口中的各种方法会在不同的时候被调用, 比如遇到一个文件或者目录, 被处理前, 处理后, 访问发生错误, 等等.

然后还可以指定maxDepth深度.

还有一个参数是FileVisitOption, 一想就知道肯定来自于nio中的Options接口体系. 这个参数用于控制出现了上述几种情况的时候, 继续访问还是跳过等方法.

SimpleFileVisitor默认除了访问失败以外的其他情况, 不会做任何处理, 因此真想做精细粒度控制, 就需要自己继承接口来写了.