介绍 Java NIO2 File API

介绍 Java NIO2 File API

本文我们聚焦java new io api —— NIO的基础文件操作。Java NIO2 File API 是java 7 引入的主要新特性之一,尤其和Path api一起组成了新的文件系统API.

环境及约定

如果需要使用nio,需要导入包:

import java.nio.file.*;

因为本文示例代码可能运行在不同环境,所以我们针对用户目录进行操作,确保在所有操作系统中都可运行且结果一致:

private static String HOME = System.getProperty("user.home");

File类是java.nio.file包中的主要入口点之一,其提供了丰富的API用于读、写以及操作文件和目录。Files类方法处理Path对象的实例。

检查文件或目录

Path实例可以表示文件系统中的文件或目录,文件操作不能确定文件或目录是否存在,是否可访问。为了简化,文本我们使用术语————文件,即表示文件也表示目录,除非特别说明。

检查文件是否存在,使用exists 方法:

@Test
public void givenExistentPath_whenConfirmsFileExists_thenCorrect() {
    Path p = Paths.get(HOME);
 
    assertTrue(Files.exists(p));
}

检查文件不存在,使用notExists 方法:

@Test
public void givenNonexistentPath_whenConfirmsFileNotExists_thenCorrect() {
    Path p = Paths.get(HOME + "/inexistent_file.txt");
 
    assertTrue(Files.notExists(p));
}

检查文件是否为真正的文件,如myfile.txt,还是一个目录。使用isRegularFile方法:

@Test
public void givenDirPath_whenConfirmsNotRegularFile_thenCorrect() {
    Path p = Paths.get(HOME);
 
    assertFalse(Files.isRegularFile(p));
}

可以通过static方法检查文件权限,通过isReadable方法检查文件是否可读:

@Test
public void givenExistentDirPath_whenConfirmsReadable_thenCorrect() {
    Path p = Paths.get(HOME);
 
    assertTrue(Files.isReadable(p));
}

使用isWritable方法检查文件是否可写:

@Test
public void givenExistentDirPath_whenConfirmsWritable_thenCorrect() {
    Path p = Paths.get(HOME);
 
    assertTrue(Files.isWritable(p));
}

同样我们也可以检查文件是否可执行:

@Test
public void givenExistentDirPath_whenConfirmsExecutable_thenCorrect() {
    Path p = Paths.get(HOME);
    assertTrue(Files.isExecutable(p));
}

当有两个path对象,可以检查两者是否指向底层文件相同中相同的文件:

@Test
public void givenSameFilePaths_whenConfirmsIsSame_thenCorrect() {
    Path p1 = Paths.get(HOME);
    Path p2 = Paths.get(HOME);
 
    assertTrue(Files.isSameFile(p1, p2));
}

创建文件

文件系统API提供了一行代码创建文件,为了创建真正的文件,通过传递Path对象使用createFile方法。处理文件名称,所有path对象中表示文件名称元素必须存在,否则会抛IOException异常:

@Test
public void givenFilePath_whenCreatesNewFile_thenCorrect() {
    String fileName = "myfile_" + UUID.randomUUID().toString() + ".txt";
    Path p = Paths.get(HOME + "/" + fileName);
    assertFalse(Files.exists(p));
 
    Files.createFile(p);
 
    assertTrue(Files.exists(p));
}

上面示例中,首先检查文件不存在,然后创建文件,最后确定其存在。

为了创建目录,可以使用createDirectory方法:

@Test
public void givenDirPath_whenCreatesNewDir_thenCorrect() {
    String dirName = "myDir_" + UUID.randomUUID().toString();
    Path p = Paths.get(HOME + "/" + dirName);
    assertFalse(Files.exists(p));
 
    Files.createDirectory(p);
 
    assertTrue(Files.exists(p));
    assertFalse(Files.isRegularFile(p));
    assertTrue(Files.isDirectory(p));
}

该操作也需要path中所有名称元素都存在,否则会抛IOException异常:

@Test(expected = NoSuchFileException.class)
public void givenDirPath_whenFailsToCreateRecursively_thenCorrect() {
    String dirName = "myDir_" + UUID.randomUUID().toString() + "/subdir";
    Path p = Paths.get(HOME + "/" + dirName);
    assertFalse(Files.exists(p));
 
    Files.createDirectory(p);
}

然而,如果我们想通过单次调用创建多层目录,可以使用createDirectories方法。与前面方法不同,当遇到不存在名称元素时,其不会抛IOException异常,而是从前到后递归创建所有目录:

@Test
public void givenDirPath_whenCreatesRecursively_thenCorrect() {
    Path dir = Paths.get(
      HOME + "/myDir_" + UUID.randomUUID().toString());
    Path subdir = dir.resolve("subdir");
    assertFalse(Files.exists(dir));
    assertFalse(Files.exists(subdir));
 
    Files.createDirectories(subdir);
 
    assertTrue(Files.exists(dir));
    assertTrue(Files.exists(subdir));
}

创建临时文件

许多应用程序运行时会在文件系统中创建临时文件的情况。因此大多数文件系统都有一个专门的目录来存储这些生成的临时文件。新的API提供了实现这种特殊操作的方法,createTempFile API执行该操作,其参数包括path对象,文件前缀和后缀:

@Test
public void givenFilePath_whenCreatesTempFile_thenCorrect() {
    String prefix = "log_";
    String suffix = ".txt";
    Path p = Paths.get(HOME + "/");
 
    Files.createTempFile(p, prefix, suffix);
         
    assertTrue(Files.exists(p));
}

对于这种操作这些参数足够了,然而,如果你需要指定文件的特殊属性,需要第四个参数。

上面示例在home目录中创建了一个临时文件,分别指定了前缀和后缀。最终会产生类似这样的文件:log_8821081429012075286.txt,中间长的字符串是系统生成的。

如果不提供前缀和后缀,那么文件名称仅包括长数字串,并带确省.tmp扩展名:

@Test
public void givenPath_whenCreatesTempFileWithDefaults_thenCorrect() {
    Path p = Paths.get(HOME + "/");
 
    Files.createTempFile(p, null, null);
         
    assertTrue(Files.exists(p));
}

上面操作创建的文件名称类似为8600179353689423985.tmp。最后我们如果不提供任何参数,完全使用缺省参数,则缺省会在系统的临时文件目录创建文件:

@Test
public void givenNoFilePath_whenCreatesTempFileInTempDir_thenCorrect() {
    Path p = Files.createTempFile(null, null);
 
    assertTrue(Files.exists(p));
}

对于windows,缺省位置类似为C:\Users\user\AppData\Local\Temp\6100927974988978748.tmp。

所有上面操作也适用于创建目录,仅需要使用createTempDirectory 方法代替createTempFile方法。

删除文件

删除文件需要使用删除API。为了清晰说明,下面测试首先确保文件不存在,然后创建并确实文件存在,最后删除并确认文件已经不存在:

@Test
public void givenPath_whenDeletes_thenCorrect() {
    Path p = Paths.get(HOME + "/fileToDelete.txt");
    assertFalse(Files.exists(p));
    Files.createFile(p);
    assertTrue(Files.exists(p));
 
    Files.delete(p);
 
    assertFalse(Files.exists(p));
}

如果被删除文件不存在,则删除操作会抛IOException异常:

@Test(expected = NoSuchFileException.class)
public void givenInexistentFile_whenDeleteFails_thenCorrect() {
    Path p = Paths.get(HOME + "/inexistentFile.txt");
    assertFalse(Files.exists(p));
 
    Files.delete(p);
}

为了避免抛异常,可以使用deleteIfExists方法。当多个线程执行此操作时,这一点很重要,我们不希望出现一条失败消息,因为一个线程执行该操作的时间比当前线程早,而当前线程已经失败:

@Test
public void givenInexistentFile_whenDeleteIfExistsWorks_thenCorrect() {
    Path p = Paths.get(HOME + "/inexistentFile.txt");
    assertFalse(Files.exists(p));
 
    Files.deleteIfExists(p);
}

当处理目录而不是文件时,应该记住删除操作默认不支持递归,所以目录不空则抛IOException异常:

@Test(expected = DirectoryNotEmptyException.class)
public void givenPath_whenFailsToDeleteNonEmptyDir_thenCorrect() {
    Path dir = Paths.get(
      HOME + "/emptyDir" + UUID.randomUUID().toString());
    Files.createDirectory(dir);
    assertTrue(Files.exists(dir));
 
    Path file = dir.resolve("file.txt");
    Files.createFile(file);
 
    Files.delete(dir);
 
    assertTrue(Files.exists(dir));
}

拷贝文件

使用copy 方法可以拷贝文件或目录:

@Test
public void givenFilePath_whenCopiesToNewLocation_thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir_" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir_" + UUID.randomUUID().toString());
 
    Files.createDirectory(dir1);
    Files.createDirectory(dir2);
 
    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");
 
    Files.createFile(file1);
 
    assertTrue(Files.exists(file1));
    assertFalse(Files.exists(file2));
 
    Files.copy(file1, file2);
 
    assertTrue(Files.exists(file2));
}

不过不指定REPLACE_EXISTING选项,目标文件以及存在会失败:

@Test(expected = FileAlreadyExistsException.class)
public void givenPath_whenCopyFailsDueToExistingFile_thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir_" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir_" + UUID.randomUUID().toString());
 
    Files.createDirectory(dir1);
    Files.createDirectory(dir2);
 
    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");
 
    Files.createFile(file1);
    Files.createFile(file2);
 
    assertTrue(Files.exists(file1));
    assertTrue(Files.exists(file2));
 
    Files.copy(file1, file2);
 
    Files.copy(file1, file2, StandardCopyOption.REPLACE_EXISTING);
}

当拷贝目录时,其内容不会被递归拷贝。如果 /yining 包括 /articles.db 和 /authors.db文件,拷贝 /yining 至新的位置,仅拷贝空目录,文件不会随着被拷贝。

移动文件

通过move 方法可以移动目录或文件,于copy方法类似。如果拷贝类似于gui中的拷贝和粘贴,那么move 就是剪切和粘贴操作:

@Test
public void givenFilePath_whenMovesToNewLocation_thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir_" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir_" + UUID.randomUUID().toString());
 
    Files.createDirectory(dir1);
    Files.createDirectory(dir2);
 
    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");
    Files.createFile(file1);
 
    assertTrue(Files.exists(file1));
    assertFalse(Files.exists(file2));
 
    Files.move(file1, file2);
 
    assertTrue(Files.exists(file2));
    assertFalse(Files.exists(file1));
}

如果目标文件以及存在,move操作会失败。如果指定REPLACE_EXISTING 选项,与copy方法类似:

@Test(expected = FileAlreadyExistsException.class)
public void givenFilePath_whenMoveFailsDueToExistingFile_thenCorrect() {
    Path dir1 = Paths.get(
      HOME + "/firstdir_" + UUID.randomUUID().toString());
    Path dir2 = Paths.get(
      HOME + "/otherdir_" + UUID.randomUUID().toString());
 
    Files.createDirectory(dir1);
    Files.createDirectory(dir2);
 
    Path file1 = dir1.resolve("filetocopy.txt");
    Path file2 = dir2.resolve("filetocopy.txt");
 
    Files.createFile(file1);
    Files.createFile(file2);
 
    assertTrue(Files.exists(file1));
    assertTrue(Files.exists(file2));
 
    Files.move(file1, file2);
 
    Files.move(file1, file2, StandardCopyOption.REPLACE_EXISTING);
 
    assertTrue(Files.exists(file2));
    assertFalse(Files.exists(file1));
}

总结

本文我们学习了java 7 中的重要新功能————新的文件系统API(NIO2),并通过示例验证了每个方法的具体用法。

你可能感兴趣的:(介绍 Java NIO2 File API)