Java NIO 的 Files 类提供了一套强大的文件操作工具,它基于 Path 对象进行工作。该类包含检查文件是否存在 (exists)、创建目录 (createDirectory)、复制与移动文件 (copy, move)、以及删除文件或目录 (delete) 等核心方法。特别强大的是 walkFileTree 方法,它能递归遍历目录树,配合 FileVisitor 接口,可轻松实现文件搜索、递归删除等复杂任务。Files 类方法设计周全,通常支持多种选项以控制行为(如覆盖文件),是替代传统 java.io.File 进行现代化文件操作的优选。

博主博客

java.nio.file.Files 类提供了多种操作文件系统中文件的方法。本教程将涵盖该类中最常用的方法。由于 Files 类包含的方法众多,如果您需要的方法未在此处描述,建议查阅 JavaDoc 文档,因为 Files 类很可能已经提供了相关方法。

java.nio.file.Files 类与 java.nio.file.Path 实例协同工作,因此在学习 Files 类之前,您需要先理解 Path 类。

Files.exists()

Files.exists() 方法用于检查指定的 Path 在文件系统中是否存在。

可以创建指向文件系统中不存在的路径的 Path 实例。例如,如果您计划创建一个新目录,通常会先创建对应的 Path 实例,然后再创建目录。

由于 Path 实例可能指向文件系统中存在或不存在的路径,您可以使用 Files.exists() 方法来确认它们是否存在(如果需要检查的话)。

以下是一个 Files.exists() 的 Java 示例:

Path path = Paths.get("data/logging.properties");

boolean pathExists =
        Files.exists(path,
            new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});

此示例首先创建一个指向要检查路径的 Path 实例。然后,示例调用 Files.exists() 方法,并将 Path 实例作为第一个参数传递。

注意 Files.exists() 方法的第二个参数。此参数是一个选项数组,会影响 Files.exists() 判断路径是否存在的方式。在上面的示例中,数组包含 LinkOption.NOFOLLOW_LINKS,这意味着 Files.exists() 方法在确定路径是否存在时不应遵循文件系统中的符号链接。

Files.createDirectory()

Files.createDirectory() 方法根据 Path 实例创建一个新目录。以下是一个 Files.createDirectory() 的 Java 示例:

Path path = Paths.get("data/subdir");

try {
    Path newDir = Files.createDirectory(path);
} catch(FileAlreadyExistsException e){
    // 目录已存在
} catch (IOException e) {
    // 发生其他错误
    e.printStackTrace();
}

第一行创建了代表要创建目录的 Path 实例。在 try-catch 块内,调用 Files.createDirectory() 方法并传入路径作为参数。如果目录创建成功,将返回一个指向新创建路径的 Path 实例。

如果目录已存在,将抛出 java.nio.file.FileAlreadyExistsException。如果发生其他问题,可能会抛出 IOException。例如,如果要创建的新目录的父目录不存在,则可能会抛出 IOException。父目录是指要在其中创建新目录的目录。

Files.copy()

Files.copy() 方法将文件从一个路径复制到另一个路径。以下是一个 Java NIO Files.copy() 示例:

Path sourcePath      = Paths.get("data/logging.properties");
Path destinationPath = Paths.get("data/logging-copy.properties");

try {
    Files.copy(sourcePath, destinationPath);
} catch(FileAlreadyExistsException e) {
    // 目标文件已存在
} catch (IOException e) {
    // 发生其他错误
    e.printStackTrace();
}

首先,示例创建源路径和目标路径的 Path 实例。然后,示例调用 Files.copy(),并传入这两个 Path 实例作为参数。这将导致源路径引用的文件被复制到目标路径引用的文件。

如果目标文件已存在,将抛出 java.nio.file.FileAlreadyExistsException。如果发生其他问题,将抛出 IOException。例如,如果要复制到的目录不存在,将抛出 IOException

覆盖现有文件

可以强制 Files.copy() 覆盖现有文件。以下示例展示了如何使用 Files.copy() 覆盖现有文件:

Path sourcePath      = Paths.get("data/logging.properties");
Path destinationPath = Paths.get("data/logging-copy.properties");

try {
    Files.copy(sourcePath, destinationPath,
            StandardCopyOption.REPLACE_EXISTING);
} catch(FileAlreadyExistsException e) {
    // 目标文件已存在
} catch (IOException e) {
    // 发生其他错误
    e.printStackTrace();
}

注意 Files.copy() 方法的第三个参数。此参数指示 copy() 方法在目标文件已存在时覆盖它。

Files.move()

Java NIO Files 类还包含一个将文件从一个路径移动到另一个路径的函数。移动文件与重命名文件相同,但移动文件既可以将其移动到不同的目录,也可以在同一个操作中更改其名称。是的,java.io.File 类也可以通过其 renameTo() 方法实现此功能,但现在您也可以在 java.nio.file.Files 类中使用文件移动功能。

以下是一个 Files.move() 的 Java 示例:

Path sourcePath      = Paths.get("data/logging-copy.properties");
Path destinationPath = Paths.get("data/subdir/logging-moved.properties");

try {
    Files.move(sourcePath, destinationPath,
            StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    // 移动文件失败
    e.printStackTrace();
}

首先创建源路径和目标路径。源路径指向要移动的文件,目标路径指向文件应移动到的位置。然后调用 Files.move() 方法。这将导致文件被移动。

注意传递给 Files.move() 的第三个参数。此参数告诉 Files.move() 方法覆盖目标路径上的任何现有文件。此参数实际上是可选的。

如果移动文件失败,Files.move() 方法可能会抛出 IOException。例如,如果目标路径已存在文件,而您省略了 StandardCopyOption.REPLACE_EXISTING 选项,或者要移动的文件不存在等。

Files.delete()

Files.delete() 方法可以删除文件或目录。以下是一个 Files.delete() 的 Java 示例:

Path path = Paths.get("data/subdir/logging-moved.properties");

try {
    Files.delete(path);
} catch (IOException e) {
    // 删除文件失败
    e.printStackTrace();
}

首先创建指向要删除文件的 Path。然后调用 Files.delete() 方法。如果 Files.delete() 因某些原因未能删除文件(例如文件或目录不存在),将抛出 IOException

Files.walkFileTree()

Files.walkFileTree() 方法包含递归遍历目录树的功能。walkFileTree() 方法接受一个 Path 实例和一个 FileVisitor 作为参数。Path 实例指向要遍历的目录。在遍历过程中会调用 FileVisitor

在解释遍历工作原理之前,首先看一下 FileVisitor 接口:

public interface FileVisitor {

    public FileVisitResult preVisitDirectory(
        Path dir, BasicFileAttributes attrs) throws IOException;

    public FileVisitResult visitFile(
        Path file, BasicFileAttributes attrs) throws IOException;

    public FileVisitResult visitFileFailed(
        Path file, IOException exc) throws IOException;

    public FileVisitResult postVisitDirectory(
        Path dir, IOException exc) throws IOException;

}

您需要自己实现 FileVisitor 接口,并将实现的实例传递给 walkFileTree() 方法。在目录遍历期间,您的 FileVisitor 实现的每个方法将在不同时间被调用。如果您不需要挂钩所有这些方法,可以扩展 SimpleFileVisitor 类,该类包含 FileVisitor 接口中所有方法的默认实现。

以下是一个 walkFileTree() 示例:

Files.walkFileTree(path, new FileVisitor<Path>() {
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    System.out.println("pre visit dir:" + dir);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    System.out.println("visit file: " + file);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    System.out.println("visit file failed: " + file);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    System.out.println("post visit directory: " + dir);
    return FileVisitResult.CONTINUE;
  }
});

在遍历过程中,FileVisitor 实现的每个方法在不同时间被调用:

  • preVisitDirectory() 方法在访问任何目录之前被调用。
  • postVisitDirectory() 方法在访问目录之后被调用。
  • visitFile() 方法在文件遍历期间为每个访问的文件调用。它不对目录调用,仅对文件调用。
  • visitFileFailed() 方法在访问文件失败时调用。例如,如果您没有正确的权限,或其他问题发生。

这四个方法中的每一个都返回一个 FileVisitResult 枚举实例。FileVisitResult 枚举包含以下四个选项:

  • CONTINUE
  • TERMINATE
  • SKIP_SIBLINGS
  • SKIP_SUBTREE

通过返回这些值之一,被调用的方法可以决定文件遍历应如何继续。

  • CONTINUE 意味着文件遍历应正常继续。
  • TERMINATE 意味着文件遍历应立即终止。
  • SKIP_SIBLINGS 意味着文件遍历应继续,但不再访问此文件或目录的任何同级项。
  • SKIP_SUBTREE 意味着文件遍历应继续,但不再访问此目录中的条目。此值仅在从 preVisitDirectory() 返回时有效。如果从任何其他方法返回,它将被解释为 CONTINUE

搜索文件

以下是一个扩展 SimpleFileVisitor 来查找名为 README.txt 的文件的 walkFileTree() 示例:

Path rootPath = Paths.get("data");
String fileToFind = File.separator + "README.txt";

try {
  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      String fileString = file.toAbsolutePath().toString();
      //System.out.println("pathString = " + fileString);

      if(fileString.endsWith(fileToFind)){
        System.out.println("file found at path: " + file.toAbsolutePath());
        return FileVisitResult.TERMINATE;
      }
      return FileVisitResult.CONTINUE;
    }
  });
} catch(IOException e){
    e.printStackTrace();
}

递归删除目录

Files.walkFileTree() 也可用于删除目录及其内部的所有文件和子目录。Files.delete() 方法仅当目录为空时才会删除目录。通过遍历所有目录,删除每个目录中的所有文件(在 visitFile() 内部),然后在之后删除目录本身(在 postVisitDirectory() 内部),您可以删除包含所有子目录和文件的目录。以下是一个递归目录删除示例:

Path rootPath = Paths.get("data/to-delete");

try {
  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      System.out.println("delete file: " + file.toString());
      Files.delete(file);
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
      Files.delete(dir);
      System.out.println("delete dir: " + dir.toString());
      return FileVisitResult.CONTINUE;
    }
  });
} catch(IOException e){
  e.printStackTrace();
}

Files 类中的其他方法

java.nio.file.Files 类包含许多其他有用的函数,例如创建符号链接、确定文件大小、设置文件权限等。如需了解更多关于这些方法的信息,请查阅 java.nio.file.Files 类的 JavaDoc 文档。

参考文献