如何在Java中使用文件操作API: java.nio.file.Path?- marcobehler


本文有关学习如何在Java中使用文件:从读取和写入文件到观察目录和使用内存文件系统。
Java有两个文件API。

  • 原始java.io.File API,自Java 1.0(1996)起可用。
  • java.nio.file.Path从Java 1.7(2011)开始可用的较新API。

File和Path API有什么区别?
旧文件API用于大量旧项目,框架和库。尽管它已经很久了,但它并没有被弃用(并且可能永远不会被弃用),您仍然可以将其与任何最新的Java版本一起使用。
但是,java.nio.file.Path做一切java.io.File可以做的,但总的来说,它可以做得更好。一些例子:
  • 文件功能:新类支持符号链接,适当的文件属性和元数据支持(认为:PosixFileAttributes),ACL等。
  • 更好的用法:例如,删除文件时,您会收到一个异常提示,并带有有意义的错误消息(没有此类文件,文件被锁定等),而不是简单的布尔型说法false。
  • 解耦:启用对内存中文件系统的支持,我们将在后面介绍。

有关这两种API之间差异的完整列表,请查看本文:https : //www.oracle.com/technical-resources/articles/javase/nio.html

由于上述原因,如果您要启动一个新的Java项目,强烈建议您在Paths API而不是File API。(即使文件比path读起来好多了,不是吗?)因此,Paths在本文中,我们将仅专注于API。

Path.of:如何引用文件
要使用Java处理文件,您首先需要引用文件(真是令人惊讶!)。如前所述,从Java 7开始,您将使用Paths API来引用文件,因此,一切都始于构造Path对象。
让我们看一些代码。

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

    // Java11+  : Path.of()

    Path path = Path.of(
"c:\\dev\\licenses\\windows\\readme.txt");
    System.out.println(path);

    path = Path.of(
"c:/dev/licenses/windows/readme.txt");
    System.out.println(path);

    path = Path.of(
"c:" , "dev", "licenses", "windows", "readme.txt");
    System.out.println(path);

    path = Path.of(
"c:" , "dev", "licenses", "windows").resolve("readme.txt"); // resolve == getChild()
    System.out.println(path);

    path = Path.of(new URI(
"file:///c:/dev/licenses/windows/readme.txt"));
    System.out.println(path);

   
// Java < 11 equivalent: Paths.get()
    path = Paths.get(
"c:/dev/licenses/windows/readme.txt");
    System.out.println(path);

   
// etc...
}

从Java 11开始,您应该使用static Path.of方法构造路径。
如果在Windows上使用正斜杠都没有关系,因为Path API足够聪明,可以独立于操作系统和任何正反斜杠问题构造正确的路径。
构造路径时,还有更多选择:您不必将完整路径指定为一个长字符串:

path = Path.of("c:" , "dev", "licenses", "windows", "readme.txt");
System.out.println(path);

path = Path.of(
"c:" , "dev", "licenses", "windows").resolve("readme.txt"); // resolve == getChild()
System.out.println(path);

相反,您可以将字符串序列传递给该Path.of方法,或者构造父目录并使用它来获取子文件(.resolve(child))。
最后但并非最不重要的一点是,您还可以将URI传递给Path.of调用。

path = Path.of(new URI("file:///c:/dev/licenses/windows/readme.txt"));
System.out.println(path);

因此,构造Path对象有多种选择。
但是,有两个要点:

  1. 构造路径对象或解析子对象并不意味着文件或目录实际存在。该路径仅是对潜在文件的引用。因此,您必须单独验证其存在。
  2. Java-11 Path.of之前的版本称为Paths.get,如果您使用较旧的Java版本或构建需要向后兼容的库,则需要使用它。从Java 11开始,Paths.get内部重定向到Path.of。
    // Java < 11 equivalent: Paths.get()path = Paths.get("c:/dev/licenses/windows/readme.txt");System.out.println(path);

文件:常用操作
1. 检查文件是否存在

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
boolean exists = Files.exists(path);
System.out.println(
"exists = " + exists);

检查文件或目录是否存在。还允许您指定其他参数,以定义如何处理符号链接,即是否遵循(默认)。
运行此代码段时,您将获得一个简单的布尔标志:exists = true

2.如何获取文件的最后修改日期

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
FileTime lastModifiedTime = Files.getLastModifiedTime(path);
System.out.println(
"lastModifiedTime = " + lastModifiedTime);

3.如何比较文件(Java12 +)

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
long mismatchIndex = Files.mismatch(path, Paths.get(
"c:\\dev\\whatever.txt"));
System.out.println(
"mismatch = " + mismatchIndex);

这是Java的相对较新的功能,自Java 12起可用。它比较两个文件的大小和字节,并返回第一个(字节)不匹配的位置。或者,如果没有不匹配,则为-1L。

4.如何获取文件的所有者

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
UserPrincipal owner = Files.getOwner(path);
System.out.println(
"owner = " + owner);

将文件或目录的所有者返回为UserPrincipal(从扩展Principal)。在Windows上,这将是WindowsUserPrincipal,其中包含用户的帐户名(如下所示)以及sidWindows用户在Windows计算机上的唯一安全标识符。

owner = DESKTOP-168M0IF\marco_local (User)

5.如何创建临时文件

Path tempFile1 = Files.createTempFile("somePrefixOrNull", ".jpg");
System.out.println(
"tempFile1 = " + tempFile1);

Path tempFile2 = Files.createTempFile(path.getParent(),
"somePrefixOrNull", ".jpg");
System.out.println(
"tempFile2 = " + tempFile2);

Path tmpDirectory = Files.createTempDirectory(
"prefix");
System.out.println(
"tmpDirectory = " + tmpDirectory);

创建临时文件时,可以指定前缀(第一参数)和后缀(第二参数)。两者都可以为null。
该前缀将以temp文件名作为前缀(duh!),后缀本质上是文件扩展名,并且如果您省略它,则将使用默认扩展名“ .tmp”。文件将在默认的临时文件目录中创建
除了默认的临时目录,您还可以指定自己的目录:

Path tmpDirectory = Files.createTempDirectory("prefix");
System.out.println(
"tmpDirectory = " + tmpDirectory);

除了文件,您还可以创建临时目录。由于在创建目录时不需要后缀参数,因此只需选择指定前缀参数即可。
注意:与流行的看法相反,临时文件不会删除自己。在单元测试中创建它们或在生产中运行时,必须确保明确删除它们。

6.如何创建文件和目录
您已经了解了如何创建临时文件,对于普通文件和目录也是如此。您将调用不同的方法:

Path newDirectory = Files.createDirectories(path.getParent().resolve("some/new/dir"));
System.out.println(
"newDirectory = " + newDirectory);

Path newFile = Files.createFile(newDirectory.resolve(
"emptyFile.txt"));
System.out.println(
"newFile = " + newFile);

有人对.resolve感到困惑:.resolve调用未创建文件,它仅返回对您要创建的(子)文件的引用。


7.如何获得文件的Posix权限
如果在类似Unix的系统(包括Linux和MacOS)上运行Java程序,则可以获得文件的Posix权限。认为:“-rw-rw-rw-”或“ -rwxrwxrwx”等。

Path path = Path.of("c:\\dev\\licenses\\windows\\readme.txt");
try {
    Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
    System.out.println(
"permissions = " + permissions);
} catch (UnsupportedOperationException e) {
    System.err.println(
"Looks like you're not running on a posix file system");
}

在Linux或MacOS上运行此命令,您将获得以下输出:
OWNER_WRITE
OWNER_READ
GROUP_WRITE
OTHERS_READ
...


8.读写文件
如何将字符串写入文件:

Path utfFile = Files.createTempFile("some", ".txt");
Files.writeString(utfFile,
"this is my string ää öö üü"); // UTF 8
System.out.println(
"utfFile = " + utfFile);

Path iso88591File = Files.createTempFile(
"some", ".txt");
Files.writeString(iso88591File,
"this is my string ää öö üü", StandardCharsets.ISO_8859_1); // otherwise == utf8
System.out.println(
"iso88591File = " + iso88591File);

从Java 11(更具体地说是11.0.2 / 12.0,因为以前的版本中存在一个错误)开始,您应该使用该Files.writeString方法将字符串内容写入文件。默认情况下,它将写入UTF-8文件,但是您可以通过指定其他编码来覆盖它。
9.如何将字节写入文件

Path anotherIso88591File = Files.createTempFile("some", ".txt");
Files.write(anotherIso88591File,
"this is my string ää öö üü".getBytes(StandardCharsets.ISO_8859_1));
System.out.println(
"anotherIso88591File = " + anotherIso88591File);

如果要向文件中写入字节(在Java版本低于11的旧版本中,必须使用相同的API来编写字符串),则需要调用Files.write。

10.写入文件时的选项

Path anotherUtf8File = Files.createTempFile("some", ".txt");
Files.writeString(anotherUtf8File,
"this is my string ää öö üü", StandardCharsets.UTF_8,
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
System.out.println(
"anotherUtf8File = " + anotherUtf8File);

Path oneMoreUtf8File = Files.createTempFile(
"some", ".txt");
Files.write(oneMoreUtf8File,
"this is my string ää öö üü".getBytes(StandardCharsets.UTF_8),
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
System.out.println(
"oneMoreUtf8File = " + oneMoreUtf8File);

当调用这两种write方法时,将自动创建文件(如果文件已经存在,则将其截断)。这意味着,我们将不必像上面那样创建显式的临时文件。
如果您不希望这种行为(即,如果文件已经存在则失败)并获得相应的异常,则需要传递另一个OpenOption

11.使用Writer和OutputStreams

try (BufferedWriter bufferedWriter = Files.newBufferedWriter(utfFile)) {
    // handle reader
}

try (OutputStream os = Files.newOutputStream(utfFile)) {
   
// handle outputstream
}

最后但并非最不重要的一点是,如果要直接使用编写器或输出流,请确保调用相应的Files方法,而不要手动构造编写器或流。

 12.如何从文件中读取字符串

String s = Files.readString(utfFile);// UTF 8
System.out.println(
"s = " + s);

s = Files.readString(utfFile, StandardCharsets.ISO_8859_1);
// otherwise == utf8
System.out.println(
"s = " + s);

在Java11 +上,您应该使用该Files.readString方法从文件中读取字符串。确保传递适当的文件编码;默认情况下,Java将使用UTF-8编码来读入文件。
 13.如何从文件读取字节
s = new String(Files.readAllBytes(utfFile), StandardCharsets.UTF_8);
System.out.println("s = " + s);

14.使用reader和InputStreams

try (BufferedReader bufferedReader = Files.newBufferedReader(utfFile)) {
    // handle reader
}

try (InputStream is = Files.newInputStream(utfFile)) {
   
// handle inputstream
}

无论何时创建,写入或读取文件,您绝对应该使用显式编码,尽管新的Java 11方法默认使用UTF-8而不是特定于平台的编码有很大帮助。

15.移动,删除和列出文件

Path utfFile = Files.createTempFile("some", ".txt");

try {
    Files.move(utfFile, Path.of(
"c:\\dev"));  // this is wrong!
} catch (FileAlreadyExistsException e) {
   
// welp, that din't work!
}

 Files.move方法不会将文件移动到指定目录(您可能会期望)。
  • test.jpg→c:\temp不起作用。
  • test.jpg→c:\temp\test.jpg有效。

Files.move(utfFile, Path.of("c:\\dev").resolve(utfFile.getFileName().toString()));

 不要将文件移动到文件夹,而是将它们“移动”到它们的全新路径,包括文件名和扩展名。

 

Path utfFile2 = Files.createTempFile("some", ".txt");
Files.move(utfFile2, Path.of(
"c:\\dev").resolve(utfFile.getFileName().toString()), StandardCopyOption.REPLACE_EXISTING);

Path utfFile3 = Files.createTempFile(
"some", ".txt");
Files.move(utfFile3, Path.of(
"c:\\dev").resolve(utfFile.getFileName().toString()), StandardCopyOption.ATOMIC_MOVE);
移动文件时,还可以根据基础文件系统的功能指定移动方式。
  • 默认情况下,如果目标文件已存在,FileAlreadyExistsException将抛出。
  • 如果指定该StandardCopyOption.REPLACE_EXISTING选项,则目标文件将被覆盖。
  • 如果指定此StandardCopyOption.ATOMIC_MOVE选项,则可以将文件移动到目录中,并确保监视目录的所有进程都可以访问完整文件,而不仅仅是部分文件。

删除文件和目录:

try {
    Files.delete(tmpDir);
} catch (DirectoryNotEmptyException e) {
    e.printStackTrace();
}

仅在目录为空时删除它们。不幸的是,没有清除非空目录的标志,您只会得到一个DirectoryNotEmptyException。
如果要使用纯Java版本删除非空目录树,则需要执行以下操作:
try (Stream<Path> walk = Files.walk(tmpDir)) {
    walk.sorted(Comparator.reverseOrder()).forEach(path -> {
        try {
            Files.delete(path);
        } catch (IOException e) {
            // something could not be deleted..
            e.printStackTrace();
        }
    });
}

Files.walk从您指定的目录开始,将深度优先遍历文件树。该reverseOrder比较器将确保您删除所有儿童,删除实际目录之前。不幸的是,Files.delete在forEach使用者内部使用时,您还需要捕获IOException 。删除非空目录的大量代码,不是吗?
罗列文件:
try (var files = Files.list(tmpDirectory)) {
    files.forEach(System.out::println);
}

try (var files = Files.newDirectoryStream(tmpDirectory, "*.txt")) {
    files.forEach(System.out::println);
}

递归列出文件:
try (var files = Files.walk(tmpDirectory)) {
    files.forEach(System.out::println);
}

 16.内存中文件系统
一些开发人员认为使用文件总是意味着您实际上必须将它们写入磁盘。在测试过程中,这导致创建许多临时文件和目录,然后必须确保再次将其删除。但是,使用Java的Path-API,有一种更好的方法:内存文件系统。它们使您可以完全在内存中写入和读取文件,而无需打磁盘。超快速且非常适合测试(只要您不用完内存,erm…)。
有两个值得关注的Java内存文件系统。一种选择是“ 内存文件系统”
  

import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder; 
  try (FileSystem fileSystem = MemoryFileSystemBuilder.newMacOs().build()) {

            Path inMemoryFile = fileSystem.getPath("/somefile.txt");
            Files.writeString(inMemoryFile,
"Hello World");

            System.out.println(Files.readString(inMemoryFile));
        }

通过调用newLinux()或newWindows(),newMacOs()您可以控制创建的文件系统的语义。
 另一个选择是JimFS。让我们看看如何使用它创建一个内存文件系统。

 

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());) {

            Path inMemoryFile = fileSystem.getPath("/tmp/somefile.txt");
            Files.writeString(inMemoryFile,
"Hello World");

            System.out.println(Files.readString(inMemoryFile));
        }

如何使您的应用程序与内存文件系统一起使用:不要使用 Path.of 和 Paths.get, 使用 FileSystem 或 Path