10.2 Java处理I/O的现代方式

Java 7 引入了全新的 I/O API(一般称为 NIO.2),几乎可以完全取代以前使用 File 类处理 I/O 的方式。新添加的各个类都在 java.nio.file 包中。

很多情况下,使用 Java 7 引入的新 API 处理 I/O 更简单。新 API 分为两大部分:第一部分是一个新抽象,Path 接口(这个接口的作用可以理解为表示文件的位置,这个位置可以有内容,也可以没有);第二部分是很多处理文件和文件系统的新方法,方便且实用。这些新方法都是 Files 类的静态方法。

10.2.1 文件

例如,使用 Files 类的新功能执行基本的复制操作非常简单,如下所示:

  1. File inputFile = new File("input.txt");
  2. try (InputStream in = new FileInputStream(inputFile)) {
  3. Files.copy(in, Paths.get("output.txt"));
  4. } catch(IOException ex) {
  5. ex.printStackTrace();
  6. }

下面我们纵览一下 Files 类中的一些重要方法,多数方法执行的操作都不言自明。很多情况下,这些方法都有返回类型。不过,除了人为的个例,或者重复等效 C 代码的行为,很少使用返回类型。

  1. Path source, target;
  2. Attributes attr;
  3. Charset cs = StandardCharsets.UTF_8;
  4. // 创建文件
  5. //
  6. // 示例路径 --> /home/ben/.profile
  7. // 示例属性 --> rw-rw-rw-
  8. Files.createFile(target, attr);
  9. // 删除文件
  10. Files.delete(target);
  11. boolean deleted = Files.deleteIfExists(target);
  12. // 复制/移动文件
  13. Files.copy(source, target);
  14. Files.move(source, target);
  15. // 读取信息的实用方法
  16. long size = Files.size(target);
  17. FileTime fTime = Files.getLastModifiedTime(target);
  18. System.out.println(fTime.to(TimeUnit.SECONDS));
  19. Map<String, ?> attrs = Files.readAttributes(target, "*");
  20. System.out.println(attrs);
  21. // 处理文件类型的方法
  22. boolean isDir = Files.isDirectory(target);
  23. boolean isSym = Files.isSymbolicLink(target);
  24. // 处理读写操作的方法
  25. List<String> lines = Files.readAllLines(target, cs);
  26. byte[] b = Files.readAllBytes(target);
  27. BufferedReader br = Files.newBufferedReader(target, cs);
  28. BufferedWriter bwr = Files.newBufferedWriter(target, cs);
  29. InputStream is = Files.newInputStream(target);
  30. OutputStream os = Files.newOutputStream(target);

Files 类中的某些方法可以接受可选的参数,为方法执行的操作指定其他行为(可能是针对特定实现的行为)。

这个 API 的某些决策偶尔会导致让人烦恼的行为。例如,默认情况下,复制操作不会覆盖已经存在的文件,所以需要使用一个复制选项指定这种行为:

  1. Files.copy(Paths.get("input.txt"), Paths.get("output.txt"),
  2. StandardCopyOption.REPLACE_EXISTING);

StandardCopyOption 是一个枚举,实现了 CopyOption 接口。而且,LinkOption 枚举也实现了 CopyOption 接口。所以,Files.copy() 方法能接受任意个 LinkOptionStandardCopyOption 参数。LinkOption 用于指定如何处理符号链接(当然,前提是底层操作系统支持符号链接)。

10.2.2 路径

Path 接口可用于在文件系统中定位文件。这个接口表示的路径具有下述特性:

  • 系统相关

  • 有层次结构

  • 由一系列路径元素组成

  • 假设的(可能还不存在,或者已经删除)

因此,Path 对象和 File 对象完全不同。其中特别重要的一点是,Path 是接口,而不是类,这体现了系统相关性。因此,不同的文件系统提供方可以使用不同的方式实现 Path 接口,提供系统专用的特性,但同时还保有整体的抽象。

组成 Path 对象的元素中有一个可选的根组件,表示实例所属文件系统的层次结构。注意,有些 Path 对象可能没有根组件,例如表示相对路径的 Path 对象。除了根组件之外,每个 Path 实例都有零个或多个目录名和名称元素。

名称元素是离目录层次结构的根最远的元素,表示文件或目录的名称。Path 对象的内容可以理解为使用特殊的分隔符把各个路径元素联接在一起。

Path 对象是个抽象概念,和任何物理文件路径都没关联。因此,可以轻易表示还不存在的文件路径。Java 提供的 Paths 类中有创建 Path 实例的工厂方法。

Paths 类提供了两个 get() 方法,用于创建 Path 对象。普通的版本接受一个 String 对象,使用默认的文件系统提供方。另一个版本接受一个 URI 对象,利用了 NIO.2 能插入其他提供方定制文件系统的特性。这是高级用法,有兴趣的开发者可以参阅相关文档。

  1. Path p = Paths.get("/Users/ben/cluster.txt");
  2. Path p = Paths.get(new URI("file:///Users/ben/cluster.txt"));
  3. System.out.println(p2.equals(p));
  4. File f = p.toFile();
  5. System.out.println(f.isDirectory());
  6. Path p3 = f.toPath();
  7. System.out.println(p3.equals(p));

这个示例还展示了 Path 对象和 File 对象之间可以轻易地相互转换。有了 Path 类中的 toFile() 方法和 File 类中的 toPath() 方法,开发者可以毫不费力地在两个 API 之间切换,而且可以使用一种直观的方式重构使用 File 类的代码,换用 Path 接口。

除此之外,还可以使用 Files 类中一些有用的“桥接”方法。通过这些方法可以轻易使用旧的 I/O API,例如,有些便利方法可以创建 Writer 对象,把内容写入 Path 对象指定的位置:

  1. Path logFile = Paths.get("/tmp/app.log");
  2. try (BufferedWriter writer =
  3. Files.newBufferedWriter(logFile, StandardCharsets.UTF_8,
  4. StandardOpenOption.WRITE)) {
  5. writer.write("Hello World!");
  6. // ...
  7. } catch (IOException e) {
  8. // ...
  9. }

这里使用了 StandardOpenOption 枚举,其作用和复制选项类似,不过用于指定打开新文件的行为。

在这个示例中,我们使用 Path API 完成了下述操作:

  • 创建一个 Path 对象,对应于一个新文件;

  • 使用 Files 类创建那个新文件;

  • 创建一个 Writer 对象,打开那个文件;

  • 把内容写入那个文件;

  • 写入完毕后自动关闭那个文件。

下面再举个例子。这个示例基于前面的代码,把一个 .jar 文件本身当成 FileSystem 对象处理,直接把一个新文件添加到这个 JAR 文件中。JAR 文件其实就是 ZIP 文件,所以这种技术也适用于 .zip 压缩文件。

  1. Path tempJar = Paths.get("sample.jar");
  2. try (FileSystem workingFS =
  3. FileSystems.newFileSystem(tempJar, null)) {
  4. Path pathForFile = workingFS.getPath("/hello.txt");
  5. List<String> ls = new ArrayList<>();
  6. ls.add("Hello World!");
  7. Files.write(pathForFile, ls, Charset.defaultCharset(),
  8. StandardOpenOption.WRITE, StandardOpenOption.CREATE);
  9. }

这个示例展示了如何使用 getPath() 方法在 FileSystem 对象中创建 Path 对象。使用这种技术,开发者其实可以把 FileSystem 对象当成黑盒。

Java 最初提供的 I/O API 受到的批评之一是,不支持本地 I/O 和高性能 I/O。Java 1.4 首次对此提出了解决方案——New I/O (NIO) API,而且在后续版本中一直在改善。