Java7-NIO2-高级教程-一-

134 阅读1小时+

Java7 NIO2 高级教程(一)

协议:CC BY-NC-SA 4.0

零、前言

这本书涵盖了开发基于 NIO.2 的应用所涉及的所有重要方面。它为充分利用 NIO.2 提供了清晰的说明,并提供了许多练习和案例研究,用新的 I/O 功能来丰富 Java 7 应用。您将学习开发 NIO.2 应用,从简单但基本的东西开始,逐渐过渡到复杂的特性,如套接字和异步通道。

这本书是给谁的

这本书既适用于不熟悉 Java 7 的有经验的 Java 程序员,也适用于对 Java 7 有一些经验的人。对于开篇章节(章节 1 - 5 ),熟悉 Java 语法,知道如何打开和运行 NetBeans 项目就足够了。对于第六章-10 章),了解一些基本的编程概念是必不可少的,比如递归、多线程和并发、互联网协议和网络应用。

这本书涵盖了什么

本节包含每章内容的简要总结。

进行Path类操作

第一章:在这里,你会看到新的用于操作文件路径的 API 您现在可以使用java.nio.file.Path类来操作任何文件系统中的文件。在这一章中,我将介绍一些重要的主题,比如声明路径实例和语法操作。

通过新的 java.nio.file.attribute API(包括 POSIX)获取/设置文件元数据

第二章:使用 NIO.2,你可以管理比以前更多的关于文件元数据的细节。属性被分成不同的类别,现在它们也包含了 POSIX 系统。第二章深入探讨了每一个类别。

管理符号链接和硬链接

第三章:nio . 2 展示了 Java 的一个未开发领域。本章向您展示了如何创建、跟踪和操作符号链接和硬链接。

通过新的 java.nio.file.Files API 处理文件和目录

第四章:在这里,您将学习涉及文件/目录的最常见任务,例如创建、读取、写入、更新等等。您将学习如何检查文件状态和循环文件存储,如何处理临时文件,以及如何删除、复制和移动文件和目录。

使用 FileVisitor API 开发递归文件操作

第五章:需要复制、移动或删除整个目录吗?你来对地方了。第五章向您展示了如何通过全新的 FileVisitor API 完成所有这些工作。您还将了解如何开发一个搜索文件工具。

探索监视服务 API 和文件更改通知

第六章:想监控文件/目录的变化,比如条目的创建、删除或修改?这是监视服务做得最好的。在本章中,我还将介绍如何观察打印托盘和摄像机。在这里,您会发现新的监视服务 API 是多么灵活和通用。

使用新的 SeekableByteChannel API 处理随机访问文件

第七章:随机访问文件(RAF)在合适的人手里是一个强大的工具。本章介绍了新的SeekableByteChannel API,并提供了大量利用其方法的例子。实践,实践,实践,并超越成为皇家空军学徒!

开发基于阻塞/非阻塞套接字的应用

第八章:学习如何以阻塞和非阻塞方式开发基于 Java 网络的应用。我将详细介绍 TCP 和 UDP,并在本章中介绍套接字编程的重要方面。

使用 NIO.2 皇冠上的宝石:异步通道 API

第九章:这是我自己个人最喜欢的一章。写作是一种乐趣,我希望你会发现它很有用,就像我发现它很有趣一样。使用异步通道 API,您可以使用一套类和选项开发基于异步网络的 Java 应用。异步通道 API 棒极了!

使用 Zip 文件系统提供程序并编写一个定制的文件系统提供程序

第十章:这最后一章以一个使用新的 Zip 文件系统提供者的例子结束了这本书。我还提出了一些关于编写定制文件系统提供者的考虑事项。第十章还包含了一个表格,详细说明了java.io.Filejava.nio.file.PathAPI 之间的转换。

一、使用Path

开始探索 NIO.2 API(也称为“JSR 203:Java 平台的更多新 I/O API”(nio . 2))的推荐切入点是新的抽象类java.nio.file.Path。这个类是 NIO.2 的一个里程碑,每个涉及 I/O 操作的应用都将利用这个类的强大功能。实际上,它是 NIO.2 中最常用的类,因为许多 I/O 操作都基于一个Path资源。

Path类支持两种类型的操作:语法操作(几乎任何涉及操纵路径而不访问文件系统的操作;这些是在内存中完成的逻辑操作)和对路径引用的文件的操作。本章涵盖了第一种类型的操作,并向您介绍了Path API。在第四章的中,我将重点探讨第二种类型的操作。本章介绍的概念将在本书的其余部分非常有用。

引入Path

路径驻留在文件系统中,该文件系统“将文件存储和组织在某种形式的介质上,通常是一个或多个硬盘驱动器上,以便于检索。” 1 可以通过java.nio.file.FileSystems final 类访问文件系统,该类用于获取我们想要处理的java.nio.file.FileSystem的实例。FileSystems包含以下两个重要的方法,以及一组newFileSystem()方法,用于构建新的文件系统:

  • getDefault():这是一个静态方法,将默认的FileSystem返回给 JVM——通常是操作系统的默认文件系统。
  • getFileSystem(URI uri):这是一个静态方法,它从与给定的 URI 模式相匹配的一组可用文件系统提供者中返回一个文件系统。Path类操纵任何文件系统(FileSystem)中的文件,该文件系统可以使用任何存储位置(java.nio.file.FileStore);这个类代表底层存储)。默认情况下(通常情况下),Path指的是默认文件系统(计算机的文件系统)中的文件,但 NIO.2 是完全模块化的——针对内存、网络或虚拟文件系统中的数据实现FileSystem完全符合 NIO.2 的要求。NIO.2 为我们提供了可能需要通过文件、目录或链接执行的所有文件系统功能。

1 甲骨文,*Java 教程,*什么是路径?(以及其他文件系统事实),download . Oracle . com/javase/tutorial/essential/io/path . html

Path类是众所周知的java.io.File类的升级版本,但File类保留了一些特定的操作,因此它没有被弃用,也不能被认为已经过时。此外,从 Java 7 开始,这两个类都是可用的,这意味着程序员可以混合他们的能力来获得最好的 I/O API。Java 7 为它们之间的转换提供了一个简单的 API。还记得你必须做以下事情的日子吗?

import java.io.File;
…
File file = new File("index.html");

好了,那些日子已经一去不复返了,因为有了 Java 7,你可以这样做:

import java.nio.file.Path;
import java.nio.file.Paths;
…
Path path = Paths.get("index.html");

仔细看,Path是文件系统中路径的编程表示。路径字符串包含文件名、目录列表和操作系统相关的文件分隔符(例如,Microsoft Windows 上的反斜杠“\”和 Solaris 和 Linux 上的正斜杠“/”),这意味着Path不是独立于系统的,因为它基于系统相关的字符串路径。因为Path基本上是一个字符串,引用的资源可能不存在。

定义路径

一旦确定了文件系统以及文件或目录的位置,就可以为它创建一个Path对象。绝对路径、相对路径、用符号“.”(表示当前目录)或“..”(表示父目录)定义的路径,以及仅包含文件/目录名的路径包含在Path类中。定义Path最简单的解决方案是调用Paths助手类的get()方法之一。下面的小节介绍了几种不同的方法来定义同一个文件的路径(在 Windows 上)——C:\rafaelnadal\tournaments\2009\BNP.txt

定义一个绝对路径

绝对路径(也称为完整路径或文件路径)是包含根目录和所有其他包含文件或文件夹的子目录的路径。在 NIO.2 中定义绝对路径是一个只有一行代码的任务,正如您在下面的例子中所看到的,它指向了在C:\rafaelnadal\tournaments\2009目录中名为BNP.txt的文件(该文件可能不存在以测试这段代码):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

get()也允许你将一个路径分割成一组块。NIO 将为您重建路径,不管有多少块。请注意,如果您为路径的每个组件定义了一个块,则可以省略文件分隔符。前面的绝对路径可以被分块为“如下”:

Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");
Path path = Paths.get("C:", "rafaelnadal", "tournaments", "2009", "BNP.txt");
定义相对于文件存储根目录的路径

相对路径(也称为非绝对路径或部分路径)只是完整路径的一部分。创建网页时经常使用相对路径。相对路径比绝对路径使用得更频繁。定义相对于当前文件存储根目录的路径应以文件分隔符开始。在以下示例中,如果当前文件存储根目录是C:,则绝对路径是C:\rafaelnadal\tournaments\2009\BNP.txt:

Path path = Paths.get("/rafaelnadal/tournaments/2009/BNP.txt");
Path path = Paths.get("/rafaelnadal","tournaments/2009/BNP.txt");
定义相对于工作文件夹的路径

当你定义一个相对于当前工作文件夹的路径时,该路径应该而不是以文件分隔符开始。如果当前文件夹是C:根目录下的/ATP,那么下面这段代码返回的绝对路径是C:\ATP\rafaelnadal\tournaments\2009\BNP.txt:

Path path = Paths.get("rafaelnadal/tournaments/2009/BNP.txt");
Path path = Paths.get("rafaelnadal","tournaments/2009/BNP.txt");
使用快捷键定义路径

使用符号“.”(表示当前目录)或“..”(表示父目录)来定义路径是一种常见的做法。NIO.2 可以处理这些类型的路径,以消除可能的冗余情况,如果您调用Path.normalize()方法(该方法删除任何冗余元素,包括任何".或"目录 /.."出现):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009/dummy/../BNP.txt").normalize();
Path path = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt").normalize();

如果你想看看normalize()方法的效果,试着定义相同的Path和不定义normalize(),如下,并将结果打印到控制台:

Path noNormalize = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt");
Path normalize = Paths.get("C:/rafaelnadal/tournaments/./2009/dummy/../BNP.txt").normalize();

如果您使用System.out.println()打印前面的路径,您将会看到下面的结果,其中normalize()已经删除了多余的元素:


C:\rafaelnadal\tournaments\.\2009\dummy\..\BNP.txt


C:\rafaelnadal\tournaments\2009\BNP.txt

从 URI 定义一条路径

在某些情况下,您可能需要从统一资源标识符(URI)创建一个Path。您可以通过使用URI.create()方法从给定的字符串创建一个 URI,并通过使用将 URI 对象作为参数的Paths.get()方法来实现。如果您需要封装可以输入到 web 浏览器地址栏中的路径字符串,这将非常有用:

import java.net.URI;
…
Path path = Paths.get(URI.create("file:///rafaelnadal/tournaments/2009/BNP.txt"));
Path path = Paths.get(URI.create("file:///C:/rafaelnadal/tournaments/2009/BNP.txt"));
使用 FileSystems.getDefault()定义路径。getPath()方法

创建Path的另一个常见解决方案是使用FileSystems类。首先,调用getDefault()方法获得默认的FileSystem——nio . 2 将提供一个通用对象,它能够访问默认的文件系统。然后,您可以如下调用getPath()方法(前面例子中的Paths.get()方法只是这个解决方案的简写):

import java.nio.file.FileSystems;
…
Path path = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009", "BNP.txt");
Path path = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009/BNP.txt");
Path path = FileSystems.getDefault().getPath("rafaelnadal/tournaments/2009", "BNP.txt");
Path path = FileSystems.getDefault().
                        getPath("/rafaelnadal/tournaments/./2009","BNP.txt").normalize();
获取主目录的路径

当您需要指向主目录的路径时,您可以按照下面的示例进行操作(返回的主目录取决于每台计算机和每个操作系统):

Path path = Paths.get(System.getProperty("user.home"), "downloads", "game.exe");

在我的 Windows 7 机器上,这个返回C:\Users\Leo\downloads\game.exe,而在我朋友的 CentOS 系统(Linux)上,这个返回/home/simpa/downloads/game.exe

获取关于路径的信息

在定义了一个Path对象之后,您可以访问一组方法,这些方法提供了关于路径元素的有用信息。这些方法基于以下事实:NIO.2 将路径字符串拆分成一组元素(一个元素是表示目录或文件的子路径),并将索引 0 分配给最高的元素,将索引n–1 分配给最低的元素,其中 n 是路径元素的数量;通常,最高的元素是根文件夹,最低的元素是文件。本节提供了将这些信息获取方法应用于路径C:\rafaelnadal\tournaments\2009\BNP.txt的示例:

Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");
获取路径文件/目录名

由路径指示的文件/目录由getFileName()方法返回,它是目录层次结构中离根最远的元素:

//output: BNP.txt
System.out.println("The file/directory indicated by path: " + path.getFileName());
获取路径根

路径的根可以用getRoot()方法获得(如果Path没有根,则返回null):

//output: C:\
System.out.println("Root of this path: " + path.getRoot());
获取路径父项

该路径的父路径(路径的根组件)由getParent()方法返回(如果Path没有父路径,则返回null):

//output: C:\rafaelnadal\tournaments\2009
System.out.println("Parent: " + path.getParent());
获取路径名元素

您可以使用getNameCount()方法获得路径中元素的数量,使用getName()方法获得每个元素的名称:

//output: 4
System.out.println("Number of name elements in path: " + path.getNameCount());

//output: rafaelnadal  tournaments  2009  BNP.txt
for (int i = 0; i < path.getNameCount(); i++) {
  System.out.println("Name element " + i + " is: " + path.getName(i));
}
获取路径子路径

您可以使用subpath()方法提取一个相对路径,该方法获得两个参数,开始索引和结束索引,表示元素的子序列:

//output: rafaelnadal\tournaments\2009
System.out.println("Subpath (0,3): " + path.subpath(0, 3));

转换路径

在本节中,您将看到如何将一个Path对象转换成一个字符串、一个 URI、一个绝对路径、一个真实路径和一个File对象。Path类包含了每一个转换的专用方法,如下面的小节所示。以下是我们将要使用的路径:

Path path = Paths.get("/rafaelnadal/tournaments/2009", "BNP.txt");
将路径转换成字符串

路径的字符串转换可以通过toString()方法实现:

//output: \rafaelnadal\tournaments\2009\BNP.txt
String path_to_string = path.toString();
System.out.println("Path to String: " + path_to_string);
将路径转换为 URI

您可以通过应用toURI()方法将Path转换为 web 浏览器格式字符串,如下例所示。结果是一个 URI 对象,它封装了一个可以输入到 web 浏览器地址栏中的路径字符串。

//output: file:///C:/rafaelnadal/tournaments/2009/BNP.txt
URI path_to_uri = path.toUri();
System.out.println("Path to URI: " + path_to_uri);
将相对路径转换为绝对路径

从相对路径获取绝对路径是一项非常常见的任务。NIO.2 可以用toAbsolutePath()方法做到这一点(注意,如果您将这个方法应用到一个已经是绝对的路径,那么将返回相同的路径):

//output: C:\rafaelnadal\tournaments\2009\BNP.txt
Path path_to_absolute_path = path.toAbsolutePath();
System.out.println("Path to absolute path: " + path_to_absolute_path.toString());
将路径转换为真实路径

toRealPath()方法返回现有文件的真实路径——这意味着该文件必须存在,如果使用toAbsolutePath()方法,这是不必要的。如果没有参数传递给此方法,并且文件系统支持符号链接,此方法将解析路径中的任何符号链接。如果你想忽略符号链接,那么给方法传递LinkOption.NOFOLLOW_LINKS枚举常量。此外,如果Path是相对的,它返回一个绝对路径,如果Path包含任何冗余元素,它返回一个删除了这些元素的路径。如果文件不存在或者不能被访问,这个方法抛出一个IOException

以下代码片段通过不跟随符号链接来返回文件的真实路径:

import java.io.IOException;
…
//output: C:\rafaelnadal\tournaments\2009\BNP.txt
try {
    Path real_path = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
    System.out.println("Path to real path: " + real_path);
} catch (NoSuchFileException e) {
    System.err.println(e);
} catch (IOException e) {
    System.err.println(e);
}
将路径转换成文件

也可以使用toFile()方法将Path转换成File对象,如下所示。这是PathFile之间的一座巨大桥梁,因为File类也包含一个名为toPath()的方法用于恢复。

//output: BNP.txt
File path_to_file = path.toFile();

//output: \rafaelnadal\tournaments\2009\BNP.txt
Path file_to_path = path_to_file.toPath();
System.out.println("Path to file name: " + path_to_file.getName());
System.out.println("File to path: " + file_to_path.toString());

结合两条路径

组合两个路径是一种技术,它允许您定义一个固定的根路径,并向它附加一个部分路径。这对于基于公共零件定义路径非常有用。NIO.2 通过resolve()方法提供了这种操作。以下是其工作原理的一个示例:

//define the fixed path
Path base = Paths.get("C:/rafaelnadal/tournaments/2009");

//resolve BNP.txt file
Path path_1 = base.resolve("BNP.txt");
//output: C:\rafaelnadal\tournaments\2009\BNP.txt
System.out.println(path_1.toString());

//resolve AEGON.txt file
Path path_2 = base.resolve("AEGON.txt");
//output: C:\rafaelnadal\tournaments\2009\AEGON.txt
System.out.println(path_2.toString());

还有一个方法专用于兄弟路径,名为resolveSibling()。它根据当前路径的父路径解析传递的路径。实际上,这个方法用给定路径的文件名替换当前路径的文件名。

下面的例子阐明了这个观点:

//define the fixed path
Path base = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

//resolve sibling AEGON.txt file
Path path = base.resolveSibling("AEGON.txt");
//output: C:\rafaelnadal\tournaments\2009\AEGON.txt
System.out.println(path.toString());

在两个地点之间建造一条道路

当需要构造一个从一个位置到另一个位置的路径时,可以调用relativize()方法,该方法在这个路径和给定路径之间构造一个相对路径。此方法构造一个路径,该路径从原始路径开始,在传入路径指定的位置结束。新路径相对于原始路径。为了更好地理解这个强大的工具,考虑一个简单的例子。假设您有以下两条相对路径:

Path path01 = Paths.get("BNP.txt");
Path path02 = Paths.get("AEGON.txt");

在这种情况下,假设BNP.txtAEGON.txt是兄弟,这意味着您可以通过向上一级然后向下一级从一个导航到另一个。应用relativize()方法输出..\AEGON.txt..\BNP.txt:

//output:  ..\AEGON.txt
Path path01_to_path02 = path01.relativize(path02);
System.out.println(path01_to_path02);

//output:  ..\BNP.txt
Path path02_to_path01 = path02.relativize(path01);
System.out.println(path02_to_path01);

另一种典型的情况是包含根元素的两条路径。考虑以下路径:

Path path01 = Paths.get("/tournaments/2009/BNP.txt");
Path path02 = Paths.get("/tournaments/2011");

在这种情况下,两个路径包含相同的根元素/tournaments。要从path01导航到path02,您将向上两级,向下一级(..\..\2011)。要从path02导航到path01,您将上升一级并下降两级(..\2009\BNP.txt)。这正是relativize()方法的工作原理:

//output:  ..\..\2011
Path path01_to_path02 = path01.relativize(path02);
System.out.println(path01_to_path02);

//output:  ..\2009\BNP.txt
Path path02_to_path01 = path02.relativize(path01);
System.out.println(path02_to_path01);

注意如果只有一条路径包含根元素,那么就不能构造相对路径。两条路径都必须包含一个根元素。即使这样,相对路径的构造也是依赖于系统的。

比较两条路径

出于不同的目的,可以用不同的方式来检验两个Paths是否相等。您可以通过调用Path.equals()方法来测试两条路径是否相等。这种方法符合Object.equals()规范。它不访问文件系统,因此不需要存在比较的路径,并且它不检查路径是否是同一个文件。在某些操作系统实施中,路径通过忽略大小写进行比较,而在其他实施中,比较区分大小写,实施将指定是否考虑大小写。这里我显示了一个相对于当前文件存储的路径和一个绝对路径,两者代表相同的文件,但不相等:

Path path01 = Paths.get("/rafaelnadal/tournaments/2009/BNP.txt");
Path path02 = Paths.get("C:/rafaelnadal/tournaments/2009/BNP.txt");

if(path01.equals(path02)){
    System.out.println("The paths are equal!");
} else {
    System.out.println("The paths are not equal!"); //true
}

有时你会想检查两个路径是否是同一个文件/文件夹。您可以通过调用java.nio.File.Files.isSameFile()方法(如下例所示)轻松实现这一点,该方法返回一个布尔值。在幕后,这个方法使用了Path.equals()方法。如果Path.equals()返回true,则路径相等,因此无需进一步比较。如果返回false,则isSameFile()方法进入动作以再次检查。请注意,此方法要求比较的文件存在于文件系统中;否则,它抛出一个IOException

try {
    boolean check = Files.isSameFile(path01, path02);
    if(check){
        System.out.println("The paths locate the same file!"); //true
    } else {
        System.out.println("The paths does not locate the same file!");
    }
} catch (IOException e) {
    System.out.println(e.getMessage());
}

由于Path类实现了Comparable接口,您可以通过使用compareTo()方法来比较路径,该方法按字典顺序比较两个抽象路径。这对于排序很有用。如果参数等于此路径,则方法返回零;如果此路径在字典序上小于参数,则返回小于零的值;如果此路径在字典序上大于参数,则返回大于零的值。下面是使用compareTo()方法的一个例子:

//output: 24
int compare = path01.compareTo(path02);
System.out.println(compare);

使用startsWith()endsWith()方法可以完成部分比较,如下例所示。使用这些方法,您可以分别测试当前路径是以给定路径开始还是结束。这两种方法都返回布尔值。

boolean sw = path01.startsWith("/rafaelnadal/tournaments");
boolean ew = path01.endsWith("BNP.txt");
System.out.println(sw);  //output:  true
System.out.println(ew);  //output:  true

遍历路径的名称元素

由于Path类实现了Iterable接口,您可以获得一个对象,使您能够迭代路径中的元素。您可以通过使用显式迭代器或者使用每次迭代返回一个Path对象的foreach循环进行迭代。下面是一个例子:

Path path = Paths.get("C:", "rafaelnadal/tournaments/2009", "BNP.txt");

for (Path name : path) {
    System.out.println(name);
}

这将从最接近根的元素开始输出元素,如下所示:


rafaelnadal

tournaments

2009

BNP.txt

总结

在本章中,您已经迈出了进入 NIO.2 API 的第一步。除了学习基本的 NIO.2 概念,比如文件系统和文件存储,您还了解了对Path类的概述,这些知识对于每个想要学习如何使用 NIO.2 API 的开发人员来说都是必不可少的。知道如何获得默认文件系统以及如何定义和操作文件路径是很重要的,因为Path类将贯穿全书,并且通常是应用的入口点。

二、元数据文件属性

如果您对文件或目录有疑问,例如它是否隐藏、它是否是目录、它的大小以及它的所有者是谁,您可以从元数据中获得这些问题(以及许多其他问题)的答案,元数据是关于其他数据的数据。

NIO.2 将元数据的概念与属性相关联,并通过java.nio.file.attribute包提供对它们的访问。由于不同的文件系统对于应该跟踪哪些属性有不同的概念,NIO.2 将属性分组到视图中,每个视图映射到一个特定的文件系统实现。通常,视图通过一个通用的方法readAttributes()批量提供属性。此外,您可以分别使用getAttribute()setAttribute()方法提取和设置单个属性,这些方法在java.nio.file.Files类中可用。根据视图,其他方法可用于其他任务。

在本章中,您将学习如何使用 NIO.2 提供的视图。您将看到如何确定文件是只读的还是隐藏的,最后一次访问或修改它的时间,谁拥有它,以及如何获得它的所有权。您还将了解如何查看文件的访问控制列表(ACL ),以及如何设置文件的 Unix 权限。此外,您将探索文件存储属性,并学习如何定义自己的属性。

nio . 2 中支持的视图

NIO.2 附带了一组六个视图,概述如下:

  • BasicFileAttributeView:这是所有文件系统实现必须支持的基本属性的视图。属性视图名称为basic
  • DosFileAttributeView:这个视图在支持 DOS 属性的文件系统上提供了标准的四个受支持的属性。属性视图名称为dos
  • PosixFileAttributeView:这个视图用支持 POSIX(Unix 可移植操作系统接口)系列标准的文件系统上支持的属性扩展了基本属性视图,比如 Unix。属性视图名称为posix
  • 任何支持文件所有者概念的文件系统实现都支持这个视图。属性视图名称为owner
  • AclFileAttributeView:该视图支持读取或更新文件的 ACL。支持 NFSv4 ACL 模型。属性视图名称为acl
  • UserDefinedFileAttributeView:该视图支持用户定义的元数据。

确定特定文件系统支持的视图

在尝试访问视图的属性之前,请确保您的文件系统支持相应的视图。NIO.2 允许您按名称查看受支持视图的完整列表,或者检查文件存储——由映射任何类型的存储(如分区、设备、卷等)的FileStore类表示——是否支持特定的视图。

一旦获得了对默认文件系统的访问权——通过调用FileSystems.getDefault()方法——就可以轻松地遍历由FileSystem.supportedFileAttributeViews()方法返回的受支持的视图。以下代码片段显示了如何做到这一点:

import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.util.Set;
…
FileSystem fs = FileSystems.getDefault();        
Set<String> views = fs.supportedFileAttributeViews();

for (String view : views) {
      System.out.println(view);
}

例如,对于 Windows 7,上述代码返回以下结果:


acl

basic

owner

user

dos

Image 注意所有文件系统都支持基本视图,所以在输出中至少应该得到basic名称。

您可以通过调用FileStore.supportsFileAttributeView()方法来测试文件存储上的特定视图。您可以将所需的视图作为一个String或一个类名来传递。以下代码检查所有可用的文件存储是否都支持基本视图:

import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.attribute.BasicFileAttributeView;
…
FileSystem fs = FileSystems.getDefault();
for (FileStore store : fs.getFileStores()) {
   boolean supported = store.supportsFileAttributeView(BasicFileAttributeView.class);
   System.out.println(store.name() + " ---" + supported);
}

此外,您可以检查特定文件所在的文件存储是否支持单一视图,如下例所示:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    FileStore store = Files.getFileStore(path);
    boolean supported = store.supportsFileAttributeView("basic");
    System.out.println(store.name() + " ---" + supported);
} catch (IOException e) {
    System.err.println(e);
}

既然您已经确定了文件系统支持哪些视图,那么是时候深入研究每个视图的属性了,从基本视图开始。

基本观点

大多数文件系统实现支持一组公共属性(大小、创建时间、上次访问时间、上次修改时间等。).这些属性被分组到一个名为BasicFileAttributeView的视图中,可以按照下面的小节进行提取和设置。

用 readAttributes()获取批量属性

您可以使用readAttributes()方法批量提取属性,如下所示(varargs参数目前支持LinkOption.NOFOLLOW_LINKS枚举——不要使用符号链接):

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; `import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; … BasicFileAttributes attr = null; Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {     attr = Files.readAttributes(path, BasicFileAttributes.class); } catch (IOException e) {     System.err.println(e); }

System.out.println("File size: " + attr.size()); System.out.println("File creation time: " + attr.creationTime()); System.out.println("File was last accessed at: " + attr.lastAccessTime()); System.out.println("File was last modified at: " + attr.lastModifiedTime());

System.out.println("Is directory? " + attr.isDirectory()); System.out.println("Is regular file? " + attr.isRegularFile()); System.out.println("Is symbolic link? " + attr.isSymbolicLink()); System.out.println("Is other? " + attr.isOther());`

用 getAttribute()获取单个属性

如果您需要提取单个属性,而不是批量提取所有属性,请使用getAttribute()方法。您需要传递文件路径和属性名,并指定是否需要跟随符号链接。下面的代码片段展示了如何提取size属性值。请记住,getAttribute()方法返回一个Object,因此您需要根据属性的值类型进行显式转换。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {
    long size = (Long)Files.getAttribute(path, "basic:size", NOFOLLOW_LINKS);                    
    System.out.println("Size: " + size);
} catch (IOException e) {
    System.err.println(e);
}

基本属性名称如下所示:

  • lastModifiedTime
  • lastAccessTime
  • creationTime
  • size
  • isRegularFile
  • isDirectory
  • isSymbolicLink
  • isOther
  • fileKey

普遍接受的检索单个属性的形式是[view-name:]attribute-name。这个view-name就是basic

更新基本属性

更新文件的最后修改时间、最后访问时间和创建时间属性中的任何一个或全部可以使用setTimes()方法来完成,该方法将表示最后修改时间、最后访问时间和创建时间的三个参数作为FileTime的实例,这是 Java 7 中的一个新类,表示文件的时间戳属性的值。如果lastModifiedTimelastAccessTimecreationTime中的任何一个具有值null,则相应的时间戳不改变。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
long time = System.currentTimeMillis();
FileTime fileTime = FileTime.fromMillis(time);
try {
    Files.getFileAttributeView(path,
    BasicFileAttributeView.class).setTimes(fileTime, fileTime, fileTime);
} catch (IOException e) {
    System.err.println(e);
}

更新文件的最后修改时间也可以通过Files.setLastModifiedTime()方法完成:

long time = System.currentTimeMillis();
FileTime fileTime = FileTime.fromMillis(time);
try {
    Files.setLastModifiedTime(path, fileTime);
} catch (IOException e) {
    System.err.println(e);
}

更新文件的最后修改时间也可以用setAttribute()方法来完成。实际上,这个方法可以用来更新文件的最后修改时间、最后访问时间,或者创建时间属性,就像调用setTimes()方法一样:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
try {
    Files.setAttribute(path, "basic:lastModifiedTime", fileTime, NOFOLLOW_LINKS);
    Files.setAttribute(path, "basic:creationTime", fileTime, NOFOLLOW_LINKS);
    Files.setAttribute(path, "basic:lastAccessTime", fileTime, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

显然,现在您必须提取这三个属性的值才能看到变化。您可以通过使用getAttribute()方法来实现:

try {
    FileTime lastModifiedTime = (FileTime)Files.getAttribute(path,
                                 "basic:lastModifiedTime", NOFOLLOW_LINKS);
    FileTime creationTime = (FileTime)Files.getAttribute(path,
                                 "basic:creationTime", NOFOLLOW_LINKS);
    FileTime lastAccessTime = (FileTime)Files.getAttribute(path,
                                 "basic:lastAccessTime", NOFOLLOW_LINKS);

    System.out.println("New last modified time: " + lastModifiedTime);
    System.out.println("New creation time: " + creationTime);
    System.out.println("New last access time: " + lastAccessTime);

} catch (IOException e) {
      System.err.println(e);
}

DOS 视图

具体到 DOS 文件系统(或 Samba),DosFileAttributeView视图用 DOS 属性扩展了基本视图(这意味着可以直接从 DOS 视图访问基本视图)。共有四种属性,通过以下方法进行映射:

  • isReadOnly():返回readonly属性的值(如果true,文件不能被删除或更新)
  • isHidden():返回hidden属性的值(如果true,文件对用户不可见)
  • isArchive():返回archive属性的值(特定于备份程序)
  • isSystem():返回system属性的值(如果true,则该文件属于操作系统)

下面的清单大量提取了给定路径的前四个属性:

import java.io.IOException; `import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.DosFileAttributes; ... DosFileAttributes attr = null; Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {     attr = Files.readAttributes(path, DosFileAttributes.class); } catch (IOException e) {     System.err.println(e); }

System.out.println("Is read only ? " + attr.isReadOnly()); System.out.println("Is Hidden ? " + attr.isHidden()); System.out.println("Is archive ? " + attr.isArchive()); System.out.println("Is system ? " + attr.isSystem());`

设置属性值和通过名称提取单个属性可以分别通过setAttribute()getAttribute()方法来完成,如下所示(我随机选择了hidden属性):

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
//setting the hidden attribute to true
try {
    Files.setAttribute(path, "dos:hidden", true, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

//getting the hidden attribute
try {
    boolean hidden = (Boolean) Files.getAttribute(path, "dos:hidden", NOFOLLOW_LINKS);
    System.out.println("Is hidden ? " + hidden);
} catch (IOException e) {
     System.err.println(e);
}

可以使用以下名称获取 DOS 属性:

  • hidden
  • readonly
  • system
  • archive

普遍接受的形式是[view-name:]attribute-name。这个view-name就是dos

文件所有者视图

大多数文件系统接受文件所有者的概念,将其作为一种身份来确定对文件系统中对象的访问权限。NIO.2 将这一概念映射到一个名为UserPrincipal的接口中,并允许您通过文件所有者视图(由FileOwnerAttributeView接口表示)来获取或设置文件的所有者。实际上,正如您将在下面的代码示例中看到的,NIO.2 有多种方式来设置和获取文件所有者。

Image 注意在本节的示例中使用了一个名为“进程的主体,但是这个主体在您的机器上不可用。为了在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试代码,您需要添加您的主体名称(您的机器的管理员用户或具有适当操作系统特权的用户)。

使用 Files.setOwner()设置文件所有者

您可以通过调用Files.setOwner()方法来设置文件所有者。除了文件路径之外,这个方法还获得了一个UserPrincipal实例,该实例映射了一个表示文件所有者的字符串。默认文件系统的用户主体查找服务可以通过调用FileSystem.getUserPrincipalLookupService()方法获得。下面是一个设置文件所有者的简单示例:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserPrincipal; ... UserPrincipal owner = null; Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); try {     owner = path.getFileSystem().getUserPrincipalLookupService().                                        lookupPrincipalByName("apress");     Files.setOwner(path, owner); } catch (IOException e) {     System.err.println(e); }

使用 FileOwnerAttributeView.setOwner()设置文件所有者

FileOwnerAttributeView映射了一个支持读取或更新文件所有者的文件属性视图。owner属性由名称owner标识,属性值是一个UserPrincipal对象。以下代码片段向您展示了如何使用此接口设置所有者:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserPrincipal;
...
UserPrincipal owner = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
FileOwnerAttributeView foav = Files.getFileAttributeView(path,
                                           FileOwnerAttributeView.class);
try {
    owner = path.getFileSystem().getUserPrincipalLookupService().
                                 lookupPrincipalByName("apress");
    foav.setOwner(owner);
} catch (IOException e) {
    System.err.println(e);
}
使用 Files.setAttribute()设置文件所有者

与大多数视图一样,文件所有者视图可以访问setAttribute()方法。属性的完整名称是owner:owner,正如您在这里看到的:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserPrincipal;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
UserPrincipal owner = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {
    owner = path.getFileSystem().getUserPrincipalLookupService().
                                 lookupPrincipalByName("apress");
    Files.setAttribute(path, "owner:owner", owner, NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}
使用 FileOwnerAttributeView.getOwner()获取文件所有者

在确定文件系统中对象的访问权限时,读取文件所有者是一项常见任务。getOwner()方法以UserPrincipal方法的形式返回文件的所有者,代表文件所有者的String可以通过调用UserPrincipal.getName()方法获得:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileOwnerAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); FileOwnerAttributeView foav = Files.getFileAttributeView(path,                                         FileOwnerAttributeView.class); try {     String owner = foav.getOwner().getName();     System.out.println(owner); } catch (IOException e) {     System.err.println(e); }

使用 Files.getAttribute()获得文件所有者

本节的最后一个例子涉及到了Files.getAttribute()方法。我相信从上面的章节中你已经非常熟悉这个方法了,所以下面是代码片段:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserPrincipal;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
try {            
    UserPrincipal owner = (UserPrincipal) Files.getAttribute(path,
                                               "owner:owner", NOFOLLOW_LINKS);
    System.out.println(owner.getName());
    } catch (IOException e) {
        System.err.println(e);
    }

Image 警告如果无法获得默认文件系统的用户主体查找服务或者指定了无效的用户名,则会抛出java.nio.file.attribute.UserPrincipalNotFoundException

文件所有者属性可能需要以下名称:

  • owner

普遍接受的形式是[view-name:]attribute-name。这个view-name就是owner

POSIX 视图

Unix 爱好者的好消息!POSIX 用 Unix 及其风格支持的属性扩展了基本视图——文件所有者、组所有者和九个相关的访问权限(读、写、同一组的成员等)。).

基于PosixFileAttributes类,您可以如下提取 POSIX 属性:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributes;
…
PosixFileAttributes attr = null;
Path path = Paths.get("/home/rafaelnadal/tournaments/2009/BNP.txt");
try {
    attr = Files.readAttributes(path, PosixFileAttributes.class);
} catch (IOException e) {
    System.err.println(e);
}

System.out.println("File owner: " + attr.owner().getName());
System.out.println("File group: " + attr.group().getName());
System.out.println("File permissions: " + attr.permissions().toString());

或者您可以通过调用Files.getFileAttributeView()方法来使用“长方法”:

import java.nio.file.attribute.PosixFileAttributeView;
…
try {
    attr = Files.getFileAttributeView(path,
                  PosixFileAttributeView.class).readAttributes();
} catch (IOException e) {
    System.err.println(e);
}

POSIX 属性可能需要以下名称:

  • group
  • permissions

普遍接受的形式是[view-name:]attribute-name。这个view-name就是posix

POSIX 权限

permissions()方法返回一组PosixFilePermissions对象。PosixFilePermissions是一个权限助手类。这个类最有用的方法之一是asFileAttribute(),它接受文件权限的Set,并构造一个可以传递给Path.createFile()方法或Path.createDirectory()方法的文件属性。例如,您可以提取一个文件的 POSIX 权限,并创建另一个具有相同属性的文件,如下所示(本例使用前面例子中的attr对象):

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
…
Path new_path = Paths.get("/home/rafaelnadal/tournaments/2009/new_BNP.txt");
FileAttribute<Set<PosixFilePermission>> posixattrs =  
                       PosixFilePermissions.asFileAttribute(attr.permissions());
try {
    Files.createFile(new_path, posixattrs);
} catch (IOException e) {
    System.err.println(e);
}

此外,您可以通过调用fromString()方法将文件的权限设置为硬编码的字符串:

Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
try {
    Files.setPosixFilePermissions(new_path, permissions);
} catch (IOException e) {
    System.err.println(e);
}
POSIX 组所有者

可以用名为group的 POSIX 属性设置文件组所有者。setGroup()方法获取文件路径和一个GroupPrincipal实例,该实例映射一个表示组所有者的字符串——该类扩展了UserPrincipal接口:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; … Path path = Paths.get("/home/rafaelnadal/tournaments/2009/BNP.txt"); try {     GroupPrincipal group = path.getFileSystem().                getUserPrincipalLookupService().lookupPrincipalByGroupName("apressteam");     Files.getFileAttributeView(path, PosixFileAttributeView.class).setGroup(group); } catch (IOException e) {     System.err.println(e); }

Image 注意在前面的例子中使用了一个名为“*apress team”*的组主体,但是这个组在您的机器上不可用。要在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试前面的代码,您需要添加您的组主体名称(您的机器的管理组或具有适当操作系统特权的组)。

您可以通过调用Files.getAttribute()方法轻松找到该组:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
try {
    GroupPrincipal group = (GroupPrincipal) Files.getAttribute(path, "posix:group",
                                                                       NOFOLLOW_LINKS);
    System.out.println(group.getName());
} catch (IOException e) {
    System.err.println(e);
}

Image 注意你可以通过调用FileOwnerAttributeView.getOwner()FileOwnerAttributeView.setOwner()来获得对所有者的访问,这在 POSIX 视图中是继承的。

前交叉韧带视图

访问控制列表(ACL)是权限的集合,旨在对文件系统对象的访问实施严格的规则。在 ACL 中,控制每个对象的所有者、权限和不同种类的标志。NIO.2 通过由AclFileAttributeView接口表示的 ACL 视图提供对 ACL 的控制,这是一个文件属性视图,支持读取或更新文件的 ACL 或文件所有者属性。

使用 Files.getFileAttributeView()读取 ACL

如果您从未见过 ACL 的内容,那么尝试下面的代码,它使用Files.getFileAttributeView()提取 ACL 作为List<AclEntry>:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclFileAttributeView;
import java.util.List;
…
List<AclEntry> acllist = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

AclFileAttributeView aclview = Files.getFileAttributeView(path, AclFileAttributeView.class);
try {
    acllist = aclview.getAcl();
} catch (IOException e) {
    System.err.println(e);
}
使用 Files.getAttribute()读取 ACL

您还可以使用getAttribute()方法来读取 ACL:

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
…
List<AclEntry> acllist = null;
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    acllist = (List<AclEntry>) Files.getAttribute(path, "acl:acl", NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println(e);
}

ACL 属性可能需要以下名称:

  • acl
  • owner

普遍接受的形式是[view-name:]attribute-name。这个view-name就是acl

读取 ACL 条目

前面的两个例子向您展示了如何提取指定路径的 ACL。结果是一个列表AclEntry——一个从 ACL 映射条目的类。每个条目有四个组成部分:

  • 类型:确定条目是允许还是拒绝访问。可以是ALARMALLOWAUDITDENY
  • 主体:条目授予或拒绝访问的身份。这被映射为一个UserPrincipal
  • 权限:一组权限。映射为Set<AclEntryPermission>
  • 标志:一组标志,指示条目如何被继承和传播。映射为Set<AclEntryFlag>

您可以遍历列表并提取每个条目的组件,如下所示——这是在前面几节中提取的列表:

for (AclEntry aclentry : acllist) {
       System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++++++");
       System.out.println("Principal: " + aclentry.principal().getName());
       System.out.println("Type: " + aclentry.type().toString());
       System.out.println("Permissions: " + aclentry.permissions().toString());
       System.out.println("Flags: " + aclentry.flags().toString());
}

以下是此代码的输出示例(在 Windows 7 上测试):


++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: BUILTIN\Administrators

Type: ALLOW

Permissions: [WRITE_OWNER, READ_ACL, EXECUTE, WRITE_NAMED_ATTRS, READ_ATTRIBUTES,
READ_NAMED_ATTRS, WRITE_DATA, WRITE_ACL, READ_DATA, WRITE_ATTRIBUTES, SYNCHRONIZE, DELETE,
DELETE_CHILD, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: NT AUTHORITY\SYSTEM

Type: ALLOW

Permissions: [WRITE_OWNER, READ_ACL, EXECUTE, WRITE_NAMED_ATTRS, READ_ATTRIBUTES,
READ_NAMED_ATTRS, WRITE_DATA, WRITE_ACL, READ_DATA, WRITE_ATTRIBUTES, SYNCHRONIZE, DELETE,
DELETE_CHILD, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: NT AUTHORITY\Authenticated Users

Type: ALLOW


Permissions: [READ_ACL, EXECUTE, READ_DATA, WRITE_ATTRIBUTES, WRITE_NAMED_ATTRS,
SYNCHRONIZE, DELETE, READ_ATTRIBUTES, READ_NAMED_ATTRS, WRITE_DATA, APPEND_DATA]

Flags: []

++++++++++++++++++++++++++++++++++++++++++++++++++++

Principal: BUILTIN\Users

Type: ALLOW

Permissions: [READ_ACL, EXECUTE, READ_DATA, SYNCHRONIZE, READ_ATTRIBUTES, READ_NAMED_ATTRS]

Flags: []

在 ACL 中授予新的访问权限

ACL 条目是通过调用相关联的AclEntry.Builder对象的build()方法来创建的。例如,如果您要授予承担者新的访问权限,则必须遵循以下流程:

  1. 通过调用FileSystem.getUserPrincipalLookupService()方法来查找主体。
  2. 获取 ACL 视图(如前所述)。
  3. 使用AclEntry.Builder对象创建一个新条目。
  4. 读取 ACL(如前所述)。
  5. 插入新条目(建议放在任何DENY条目之前)。
  6. 使用setAcl()setAttribute()重写 ACL。

按照这些步骤,您可以编写一个代码片段,用于向名为apress的主体授予读数据访问权和追加数据访问权:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.AclEntryPermission; import java.nio.file.attribute.AclEntryType; import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.UserPrincipal; import java.util.List; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; … try {     //Lookup for the principal     UserPrincipal user = path.getFileSystem().getUserPrincipalLookupService() `                                                   .lookupPrincipalByName("apress");

    //Get the ACL view     AclFileAttributeView view = Files.getFileAttributeView(path,                                                AclFileAttributeView.class);

    //Create a new entry     AclEntry entry = AclEntry.newBuilder().setType(AclEntryType.ALLOW).                setPrincipal(user).setPermissions(AclEntryPermission.READ_DATA,                  AclEntryPermission.APPEND_DATA).build();

    //read ACL     List acl = view.getAcl();

    //Insert the new entry     acl.add(0, entry);

    //rewrite ACL     view.setAcl(acl);                 //or, like this     //Files.setAttribute(path, "acl:acl", acl, NOFOLLOW_LINKS);

} catch (IOException e) {     System.err.println(e); }`

Image 注意上例中使用的名为“*apress”*的主体在您的机器上不可用。要在没有获得java.nio.file.attribute.UserPrincipalNotFoundException的情况下测试代码,添加您的主体名称(您机器的管理员用户或具有适当操作系统权限的用户)。

上述代码在现有文件的 ACL 中添加了一个新条目。在一般情况下,您可能会在创建新文件时这样做。

Image 注意你可以通过调用FileOwnerAttributeView.getOwner()FileOwnerAttributeView.setOwner()来获得对所有者的访问,这两个值在 ACL 视图中是继承的。

文件存储属性

如果您将计算机视为一个文件存储容器,那么您可以轻松地识别更多类型的存储,如分区、设备、卷等等。NIO.2 可以通过FileStore抽象类获得每种存储类型的信息。对于一个特定的商店,您可以获得它的名称、类型、总空间、已用空间和可用空间。在下面的小节中,您将看到如何为默认文件系统中的所有存储以及包含指定文件的存储获取该信息。

获取所有文件存储的属性

一旦获得了对默认文件系统的访问权——通过调用FileSystems.getDefault()方法——就可以轻松地遍历由FileSystem.getFileStores()方法提供的文件存储列表。因为每个实例(名称、类型、总空间、已用空间和可用空间)都是一个FileStore对象,所以可以调用相应的专用方法,如name()type()getTotalSpace()等等。以下代码片段打印了关于您的商店的信息:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
…
FileSystem fs = FileSystems.getDefault();
for (FileStore store : fs.getFileStores()) {
  try {
        long total_space = store.getTotalSpace() / 1024;
        long used_space = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;
        long available_space = store.getUsableSpace() / 1024;
        boolean is_read_only = store.isReadOnly();

        System.out.println("--- " + store.name() + " --- " + store.type());
        System.out.println("Total space: " + total_space);
        System.out.println("Used space: " + used_space);
        System.out.println("Available space: " + available_space);
        System.out.println("Is read only? " + is_read_only);

  } catch (IOException e) {
      System.err.println(e);
  }
}

以下是此代码的输出示例:


---  --- NTFS

Total space: 39070048

Used space: 31775684

Available space: 7294364

---  --- NTFS


Total space: 39070048

Used space: 8530348

Available space: 30539700

--- SAMSUNG DVD RECORDER VOLUME --- UDF

Total space: 2936192

Used space: 2936192

Available space: 0

Image 注意正如您在前面的例子中所看到的,如果一个商店没有名称,则返回一个空字符串。此外,返回的磁盘空间量的值以字节表示,因此您可能希望将这些数字转换为千字节、兆字节或千兆字节,以便于人们阅读。

获取文件所在的文件存储的属性

基于FileStore类,您可以获得特定文件所在的文件存储的属性。这个任务可以通过调用Files.getFileStore()方法来完成,该方法获得一个表示文件的参数(一个Path对象)。NIO.2 为您确定文件存储并提供对信息的访问。以下代码显示了一种可能的方法:

`import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); try {     FileStore store = Files.getFileStore(path);

   long total_space = store.getTotalSpace() / 1024;    long used_space = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;    long available_space = store.getUsableSpace() / 1024;    boolean is_read_only = store.isReadOnly();

   System.out.println("--- " + store.name() + " --- " + store.type());    System.out.println("Total space: " + total_space);    System.out.println("Used space: " + used_space);    System.out.println("Available space: " + available_space);    System.out.println("Is read only? " + is_read_only); } catch (IOException e) {    System.err.println(e); }`

此代码的输出示例如下:


---  --- NTFS

Total space: 39070048

Used space: 8530348

Available space: 30539700

Is read only? false

文件存储可能支持一个或多个FileStoreAttributeView类,这些类提供一组文件存储属性的只读或可更新视图,如下所示:

FileStoreAttributeView fsav =
         store.getFileStoreAttributeView(FileStoreAttributeView.class);

Image 注意此外,您可以使用getAttribute()方法读取文件存储属性的值。

用户自定义文件属性视图

如果您发现没有足够的内置属性来满足您的需要,或者如果您有一些唯一的元数据(对文件系统有意义)要与文件关联,您可以定义自己的属性。NIO.2 通过UserDefinedFileAttributeView接口提供了用户定义的文件属性视图——扩展属性。这个工具允许您将您认为对您的用例有用的任何属性关联到一个文件。例如,如果您开发分布式文件系统,这可能很有用。例如,您可以添加一个布尔属性来验证文件是否被复制或分发到其他位置。

检查用户定义属性的可支持性

在您尝试创建自己的属性之前,请检查您的文件系统是否支持此功能。因为这是通过文件存储来检查的,而不是通过文件本身,所以首先您需要获得所需的文件存储。然后,您可以调用supportsFileAttributeView()方法,该方法使用一个String参数来表示文件属性视图的名称或视图名称为UserDefinedFileAttributeView.class。它返回一个布尔值,如下所示:

import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserDefinedFileAttributeView;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");

try {
    FileStore store = Files.getFileStore(path);
    if (!store.supportsFileAttributeView(UserDefinedFileAttributeView.class)) {
      System.out.println("The user defined attributes are not supported on: " + store);
    } else {
      System.out.println("The user defined attributes are supported on: " + store);
    }
} catch (IOException e) {
    System.err.println(e);
}

Image 注意你可以直接从默认的文件系统中获取所有的文件存储,或者一组文件存储。不需要从文件所在的位置获取文件存储。

对用户定义属性的操作

如果您的文件系统支持用户定义的属性,那么您就可以创建自己的属性了。接下来,您将看到如何定义属性,如何列出用户定义的属性,以及如何删除用户定义的属性。本节的重点应该是用户定义属性的生命周期。

定义一个用户属性

首先,您将定义一个名为file.description的属性,其值为"This file contains private information!"。通过调用Files.getFileAttributeView()获得视图后,您可以如下编写这个用户定义的属性:

import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserDefinedFileAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,                                          UserDefinedFileAttributeView.class); try {     int written = udfav.write("file.description", Charset.defaultCharset().                encode("This file contains private information!")); } catch (IOException e) {     System.err.println(e); }

write()方法将来自给定缓冲区的属性值作为一个字节序列写入。它接收两个参数:属性名和包含属性值的缓冲区。如果给定名称的属性已经存在,则替换其值。如您所见,该方法返回了一个int,它表示写入的字节数,可能为零。

Image 注意另外,你可以用setAttribute()的方法写一个属性。可以从缓冲区或者字节数组(byte[])写入。

列出用户定义的属性名称和值大小

在任何时候,您都可以通过调用UserDefinedFileAttributeView.list()方法来查看用户定义的属性名称和值大小的列表。返回的列表是表示属性名称的字符串集合。将它们的名称传递给UserDefinedFileAttributeView.size()方法将导致属性值的大小。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.UserDefinedFileAttributeView;
…
Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt");
UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,
                                         UserDefinedFileAttributeView.class);

try {
    for (String name : udfav.list()) {
        System.out.println(udfav.size(name) + "     " + name);
    }
} catch (IOException e) {
    System.err.println(e);
}
获取用户定义属性的值

读取用户定义属性的值是通过使用UserDefinedFileAttributeView.read()方法完成的。您向它传递属性名和目标缓冲区,它返回指定缓冲区中的值。以下代码片段向您展示了如何做到这一点:

import java.io.IOException; `import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserDefinedFileAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,                                          UserDefinedFileAttributeView.class);

try {     int size = udfav.size("file.description");     ByteBuffer bb = ByteBuffer.allocateDirect(size);     udfav.read("file.description", bb);     bb.flip();     System.out.println(Charset.defaultCharset().decode(bb).toString()); } catch (IOException e) {     System.err.println(e); }`

Image 注意使用UserDefinedFileAttributeView.size()方法,您可以很容易地设置表示用户定义属性的值的缓冲区的正确大小。

Image 注意你也可以使用getAttribute()方法读取一个属性。该值作为字节数组返回(byte[])。

删除文件的自定义属性

当用户定义的属性不再有用时,您可以通过调用UserDefinedFileAttributeView.delete()方法轻松删除它。您只需要向方法提供属性的名称,它就会为您完成剩下的工作。下面显示了如何删除前面定义的属性:

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.UserDefinedFileAttributeView; … Path path = Paths.get("C:/rafaelnadal/tournaments/2009", "BNP.txt"); UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,                                          UserDefinedFileAttributeView.class); try {     udfav.delete("file.description"); } catch (IOException e) {     System.err.println(e); }

总结

在本章中,您已经探索了 NIO.2 提供的视图。您看到了如何操作各种属性,如何针对不同的目的查询文件或文件存储,以及如何定义自己的元数据。

在介绍了 NIO.2 视图以及如何确定特定文件系统支持哪些视图之后,本章介绍了基本和 DOS 属性,这些属性应该适用于每个文件。这些属性提供了主要的元数据,比如大小、创建时间、最后修改时间、只读等等。本章接下来介绍了文件所有者属性,它为设置和获取文件所有者提供了支持,接着介绍了 Unix 用户的 POSIX 属性和 ACL 属性,它们提供了对权限集合的访问,这些权限控制着对文件系统对象的访问。本章最后讨论了文件存储属性和用户定义的属性。

三、管理符号链接和硬链接

Linux 和 Unix 用户(尤其是管理员)应该熟悉链接的概念。有两种类型的链接:符号链接和硬链接。链接通常通过几个名称到达一个文件,而不是从根目录通过一系列目录和子目录进行导航——可以将链接想象为一个映射文件/目录路径的实体,并通过一组名称进行标识。如果你是一个 Windows 的忠实用户,你可能不熟悉链接,尽管 Windows 本身完全知道它们,尤其是符号链接,它很像 Windows 的快捷方式。

NIO.2 同时支持硬链接和符号链接。Path类的每个方法都知道如何检测一个链接,如果没有指定行为的配置,它们将以默认的方式运行。在本章中,你将学习如何通过java.nio.file API 操作链接,包括如何创建一个链接以及如何找到一个链接的目标。大多数操作都是通过java.nio.file.Files类实现的,该类提供了createLink()createSymbolicLink()isSymbolicLink()readSymbolicLink()等方法。本章将详细介绍每一种方法。

介绍链接

当您通过一组名称(从命令行、应用或其他方式)访问一个文件时,您正在处理一个链接。链接可以设置为硬链接(有时拼写为 hardlink )或符号链接(也称为 symlinksoftlink )。当一个文件有两个相同权重的名称和 inode 表时(Linux 文件实际上不存在于目录中;它们被分配一个索引节点号,Linux 用它来定位它们)直接指向磁盘上包含数据的块,该链接是一个硬链接。把硬链接想象成指向文件的目录引用或指针。当一个文件有一个主名称,并且在文件名表中有一个额外的条目将任何访问引用回主名称时,该链接就是一个符号链接。符号链接比硬链接更灵活,使用频率也更高。以下是这两种类型的链接之间的主要差异/相似之处:

  • 只能为文件创建硬链接,不能为目录创建。符号链接可以链接到文件或目录。
  • 硬链接不能跨文件系统存在。符号链接可以跨文件系统存在。
  • 硬链接的目标必须存在。符号链接的目标可能不存在。
  • 移除你的硬链接所指向的原始文件并不会移除硬链接本身,硬链接仍然提供底层文件的内容。移除符号链接指向的原始文件不会移除附加的符号链接,但是如果没有原始文件,符号链接就没有用。
  • 如果删除硬链接或符号链接本身,原始文件将保持不变。
  • 硬链接是与原始文件相同的实体。所有属性都相同。符号链接没有这么大的限制。
  • 硬链接的外观和行为都像普通文件,因此很难找到硬链接。符号链接的目标甚至可能不存在,因此它非常灵活。

从命令行创建链接

Windows 用户可以使用mklink命令从命令行创建符号链接和硬链接。该命令根据您需要创建的链接类型获得一组选项。其中一些选项如下:

/D      Creates a directory symbolic link.  Default is a file symbolic link.
/H      Creates a hard link instead of a symbolic link.
/J      Creates a Directory Junction.
Link    specifies the new symbolic link name.
Target  specifies the path (relative or absolute) that the new link refers to.

例如,如果您想让文件夹C:\rafaelnadal\photos也可以从C:\rafaelnadal中获得,您可以使用以下命令:

mklink /D C:\rafaelnadal C:\rafaelnadal\photos

现在,如果您查看C:\rafaelnadal目录,您还会看到C:\rafaelnadal\photos目录中的所有文件。

Unix (Linux)用户可以使用名为ln的命令来实现与前面的 Windows 示例相同的效果(注意,在本例中,目标文件首先列出,链接名称其次列出):

ln –s /home/rafaelnadal/photos /home/rafaelnadal

此外,在 Unix (Linux)中,您可以使用rm命令删除链接:

rm /home/rafaelnadal

创建符号链接

在 NIO.2 中创建符号链接非常容易。您只需调用Files.createSymbolicLink()方法,该方法使用符号链接的路径来创建、符号链接的目标以及在创建符号链接时自动设置的属性数组。它返回符号链接的路径。

如果你的文件系统不支持符号链接,那么会抛出一个UnsupportedOperationException异常。此外,请记住,符号链接的目标可以是绝对的,也可以是相对的(如第一章中所述),可能存在也可能不存在。

下面的代码片段用默认属性创建了一个简单的符号链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.1的符号链接(建议该文件存在,并且文件系统必须有创建符号链接的权限)。

Path link = FileSystems.getDefault().getPath("rafael.nadal.1");
Path target= FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permission denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occurred!");
      }
System.err.println(e);
}

当你想修改链接的默认属性时,你可以使用createSymbolicLink()方法的第三个参数。这个参数是一个类型为FileAttribute的属性数组,这个类封装了一个文件属性的值,可以在创建新文件、目录或链接时自动设置这个值。下面的代码片段读取目标文件的属性并创建一个链接,将目标文件的属性分配给链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.2的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

Path link = FileSystems.getDefault().getPath("rafael.nadal.2");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    PosixFileAttributes attrs = Files.readAttributes(target, PosixFileAttributes.class);
    FileAttribute<Set<PosixFilePermission>> attr =
                               PosixFilePermissions.asFileAttribute(attrs.permissions());

    Files.createSymbolicLink(link, target, attr);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permission denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occured!");
      }
    System.err.println(e);
}

此外,您可以使用setAttribute()方法修改创建后的链接属性。例如,下面的代码片段读取目标的lastModifiedTimelastAccessTime属性,并将它们设置为链接。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.3的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

Path link = FileSystems.getDefault().getPath("rafael.nadal.3");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);

    FileTime lm = (FileTime) Files.getAttribute(target,
                                                 "basic:lastModifiedTime", NOFOLLOW_LINKS);
    FileTime la = (FileTime) Files.getAttribute(target,
                                                   "basic:lastAccessTime", NOFOLLOW_LINKS);
    Files.setAttribute(link, "basic:lastModifiedTime", lm, NOFOLLOW_LINKS);
    Files.setAttribute(link, "basic:lastAccessTime", la, NOFOLLOW_LINKS);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
      if (e instanceof SecurityException) {
          System.err.println("Permision denied!");
      }
      if (e instanceof UnsupportedOperationException) {
          System.err.println("An unsupported operation was detected!");
      }
      if (e instanceof IOException) {
          System.err.println("An I/O error occured!");
      }
    System.err.println(e);
}

Image 注意如果符号链接已经存在,那么会抛出一个FileAlreadyExistsException异常。

创建硬链接

您可以通过调用createLink()方法来创建一个硬链接,该方法使用链接来创建一个现有文件的路径。它返回链接的路径,表示新的目录条目。然后,您可以使用该链接作为路径来访问该文件。

如果你的文件系统不支持硬链接,那么会抛出一个UnsupportedOperationException异常。此外,请记住,只能为现有文件创建硬链接。

以下代码片段为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.4的硬链接(该文件必须存在,并且文件系统必须具有创建硬链接的权限):

import java.io.IOException; `import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path;

public class Main {

 public static void main(String[] args) {

  Path link = FileSystems.getDefault().getPath("rafael.nadal.4");   Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

  try {       Files.createLink(link, target);             System.out.println("The link was successfully created!");       } catch (IOException | UnsupportedOperationException | SecurityException e) {         if (e instanceof SecurityException) {             System.err.println("Permission denied!");         }         if (e instanceof UnsupportedOperationException) {             System.err.println("An unsupported operation was detected!");         }         if (e instanceof IOException) {             System.err.println("An I/O error occured!");         }         System.err.println(e);   }  } }`

Image 注意如果硬链接已经存在,那么会抛出FileAlreadyExistsException异常。

检查符号链接

不同的Path实例可以指向文件或链接,所以你可以通过调用Files.isSymbolicLink()方法来检测一个Path实例是否指向一个符号链接。它接收一个参数,代表要测试的Path,并返回一个布尔值。下面的代码片段是测试符号链接的Path的一个简单例子。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.5的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

`… Path link = FileSystems.getDefault().getPath("rafael.nadal.5"); Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {     Files.createSymbolicLink(link, target);     } catch (IOException | UnsupportedOperationException | SecurityException e) {     … }

//check if a path is a symbolic link - solution 1 boolean link_isSymbolicLink_1 = Files.isSymbolicLink(link); boolean target_isSymbolicLink_1 = Files.isSymbolicLink(target);

System.out.println(link.toString() + " is a symbolic link ? " + link_isSymbolicLink_1); System.out.println(target.toString() + " is a symbolic link ? " + target_isSymbolicLink_1); …`

该代码输出以下结果:


rafael.nadal.5 is a symbolic link ? true

C:\rafaelnadal\photos\rafa_winner.jpg is a symbolic link ? false

正如你在第二章中读到的,你可以通过使用属性视图来测试符号链接的Path。基本视图提供了一个名为isSymbolicLink的属性,如果指定的Path定位到一个符号链接的文件,它将返回true。您可以通过readAttributes()方法查看isSymbolicLink属性(在这种情况下不推荐,因为它会返回大量的属性列表),或者更容易地通过getAttribute()方法查看,方法如下:

try {
    boolean link_isSymbolicLink_2 = (boolean) Files.getAttribute(link,
                                                                "basic:isSymbolicLink");
    boolean target_isSymbolicLink_2 = (boolean) Files.getAttribute(target,
                                                                "basic:isSymbolicLink");

    System.out.println(link.toString() + " is a symbolic link ? " + link_isSymbolicLink_2);
    System.out.println(target.toString() + " is a symbolic link ? "+ target_isSymbolicLink_2);
    } catch (IOException | UnsupportedOperationException e) {
      System.err.println(e);
}
…

同样,输出是


rafael.nadal.5 is a symbolic link ? true

C:\rafaelnadal\photos\rafa_winner.jpg is a symbolic link ? false

定位链接的目标

从一个链接开始,您可以通过调用readSymbolicLink()方法来定位它的目标(可能不存在)。这个方法从用户那里接收链接,作为一个Path,并返回一个代表链接目标的Path对象。如果传递的路径不是链接,那么将抛出一个NotLinkException异常。下面的代码片段使用这个方法为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.6的符号链接(该文件必须存在,并且文件系统必须具有创建符号链接的权限):

Path link = FileSystems.getDefault().getPath("rafael.nadal.6");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");…
…
try {
    Path linkedpath = Files.readSymbolicLink(link);
    System.out.println(linkedpath.toString());
} catch (IOException e) {
    System.err.println(e);
}

检查链接和目标是否指向同一个文件

有时您可能需要检查链接和目标是否指向同一个文件(位置)。您可以通过不同的方式获得这些信息,但是一个简单的解决方案是使用Files.isSameFile()方法。该方法(从用户处)接收两个要比较的Path,并返回一个布尔值。下面的代码片段创建了一个目标和该目标的符号链接,然后应用了isSameFile()方法。它为文件C:\rafaelnadal\photos\rafa_winner.jpg创建一个名为rafael.nadal.7的符号链接(该文件必须存在,并且文件系统必须有创建符号链接的权限)。

Path link = FileSystems.getDefault().getPath("rafael.nadal.7");
Path target = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_winner.jpg");

try {
    Files.createSymbolicLink(link, target);
    } catch (IOException | UnsupportedOperationException | SecurityException e) {
     …
}

try {
    Path linkedpath = Files.readSymbolicLink(link);
    System.out.println(linkedpath.toString());
    } catch (IOException e) {
      System.err.println(e);
}

输出如下:


rafael.nadal.7 and C:\rafaelnadal\photos\rafa_winner.jpg point to the same location

总结

在本章中,您看到了 NIO.2 如何处理符号链接和硬链接。在简要概述了这两个概念以及如何在 Windows 和 Unix (Linux)中创建它们的一些简短示例之后,您看到了 NIO.2 方法。您学习了如何直接从 Java 创建符号链接和硬链接,如何检查路径是否是链接,如何检测链接的目标,以及如何检查链接和目标是否指向同一个文件。

四、文件和目录

现在您已经知道了如何使用Path类指向文件或目录,您已经准备好学习如何完成管理文件和目录的最常见任务,比如创建、读取、写入、移动、删除等等。NIO.2 附带了一组全新的方法来完成这些任务,其中大部分都可以在java.nio.file.Files类中找到。

本章首先探索一些专门检查Path是否可读、可写、可执行、常规或隐藏的方法。这些检查使您能够在应用写或读之类的操作之前,确定正在处理的文件或目录的类型。然后这一章将重点放在目录操作上,向您展示如何列出、创建和读取目录。您将看到如何列出文件系统的根目录,使用诸如createDirectory()createTempDirectory()的方法创建目录,编写目录过滤器,以及使用newDirectoryStream()方法列出目录的内容。熟悉目录操作后,您将探索文件操作,如读、写、创建和打开文件。正如您将看到的,有许多文件 I/O 方法可供选择。在本章中,您将看到缓冲和非缓冲流的工作方法,将通道方法的讨论留到下一章,在下一章中,您将看到 NIO 的真正威力。本章以众所周知的删除、复制和移动操作结束。

这些任务中的每一个都有详细的介绍,正如您将看到的,许多方面都是从以前的 Java 6“重新设计”的,但是您也将认识到许多来自java.io.File类的介绍方法。

文件和目录的检查方法

Files类提供了一组is方法,您可以使用这些方法在实际操作文件或目录之前执行各种检查。其中一些方法在前面的章节中已经介绍过了,其余的在这里介绍。建议利用这些方法,因为它们非常有助于帮助您避免应用中的异常或其他奇怪行为。例如,在尝试将文件移动到另一个位置之前,最好先检查该文件是否存在。同样,在尝试读取文件之前,检查文件是否可以读取也是一个好主意。这些检查中的一些也可以通过元数据属性来执行,正如你在第二章中看到的。

*#### 检查文件或目录是否存在

正如您在前面章节中所知道的,即使映射的文件或目录实际上并不存在,Path实例也是完全有效的。此外,语法Path方法在这种情况下可以成功应用,因为它们不操作文件或目录本身。但是在某些时候,知道一个文件或目录是否存在是非常重要的,这就是为什么Files类为这种类型的检查提供了以下两种方法:

  • exists():检查文件是否存在
  • notExists():检查文件是否不存在

这两种方法都接收两个参数,表示要测试的文件的路径和指示如何处理符号链接的选项。如果文件存在,exists()方法返回true,否则返回false(文件不存在或无法执行检查)。

以下代码片段检查文件AEGON.txt是否存在于C:\rafaelnadal\tournaments\2009目录中(在我们假设的目录结构中,该文件存在):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009","AEGON.txt");
…
boolean path_exists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});

如果您只需要在文件不存在的情况下采取行动,那么调用notExists()方法,如果文件不存在,该方法返回true,否则返回false(文件存在或者无法执行检查):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
"AEGON.txt");
…
boolean path_notexists = Files.notExists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});

Image 注意如果两种方法都应用于同一个Path,并且都返回false,则无法进行检查。例如,如果应用不能访问文件,那么状态是未知的,两种方法都返回false。从这里,很容易得出结论,文件/目录的存在状态可以是:存在、不存在或未知。在检查该状态后,结果立即过期,因为存在的文件可以在检查后立即删除,因此结果必须立即“过期”。如果此方法指示文件存在,则不能保证后续访问会成功。此外,如果这些方法之一没有读取文件的权限,可能会抛出一个SecurityException

Image 注意 !Files.exists(…)不等同于Files.notExists(…)notExists()方法不是exists()方法的补充。

检查文件可访问性

在访问文件之前,另一个好的做法是使用isReadable()isWritable()isExecutable()方法检查其可访问性级别。在您传递了要验证的Path之后,这些方法将分别检查它是否是可读的Path(文件存在,JVM 有权限打开它进行读取)、可写的Path(文件存在,JVM 有权限打开它进行写入)、可执行的Path(文件存在,JVM 有权限执行它)。

此外,您可以通过调用isRegularFile()方法来检查Path是否指向常规文件。常规文件是没有特殊特征的文件(它们不是符号链接、目录等。)并包含真实数据,如文本或二进制文件。isReadable()isWritable()isExecutable()isRegularFile()都返回布尔值:true如果文件存在并且可读、可写、可执行和正常,或者false如果文件不存在,读取、写入、执行和正常访问将被拒绝,因为 JVM 没有足够的权限,或者无法确定访问。

将这些方法放入检查C:\rafaelnadal\tournaments\2009目录中的AEGON.txt文件的可访问性的代码片段中(该文件必须存在),如下所示:

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009","AEGON.txt");

boolean is_readable = Files.isReadable(path);
boolean is_writable = Files.isWritable(path);
boolean is_executable = Files.isExecutable(path);
boolean is_regular = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);

if ((is_readable) && (is_writable) && (is_executable) && (is_regular)) {
     System.out.println("The checked file is accessible!");
} else {
     System.out.println("The checked file is not accessible!");
}

或者,您可以使用这个较短的版本:

boolean is_accessible = Files.isRegularFile(path) & Files.isReadable(path) &  
                        Files.isExecutable(path) & Files.isWritable(path);
if (is_accessible) {
    System.out.println("The checked file is accessible!");
} else {
    System.out.println("The checked file is not accessible!");
}

Image 注意前面的例子通过对一个Path应用所有四种方法来检查可访问性,但是你可以根据你需要获得的可访问性级别以不同的方式组合这四种方法。例如,您可能不关心Path是否可写,在这种情况下,您可以排除这个检查。

Image 注意即使这些方法确认了可访问性,也不能保证文件可以被访问。原因在于一个众所周知的软件错误,名为“检查时间到使用时间”(TOCTTOU,发音为“TOCK too”),这意味着在检查和使用检查结果之间的时间内,系统可能会发生不同类型的变化。Unix 爱好者可能熟悉这个概念,但是它也适用于任何其他系统。

检查两条路径是否指向同一个文件

在前一章中,你看到了如何检查一个符号链接和一个目标是否指向同一个文件。使用isSameFile()方法可以执行的另一个常见测试是检查两个以不同方式表达的Path是否指向同一个文件。例如,一个相对的Path和一个绝对的Path可能指向同一个文件,即使这不是很明显。调用isSameFile()方法将在下面的代码片段中揭示这一点,它以三种不同的方式表达了到MutuaMadridOpen.txt文件的路径(该文件必须存在于C:\rafaelnadal\tournaments\2009 directory中):

Path path_1 = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
                                                                     "MutuaMadridOpen.txt");
Path path_2 = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/2009",
                                                                     "MutuaMadridOpen.txt");
Path path_3 = FileSystems.getDefault().getPath("/rafaelnadal/tournaments/dummy/../2009",
                                                                     "MutuaMadridOpen.txt");
try {
    boolean is_same_file_12 = Files.isSameFile(path_1, path_2);
    boolean is_same_file_13 = Files.isSameFile(path_1, path_3);
    boolean is_same_file_23 = Files.isSameFile(path_2, path_3);

    System.out.println("is same file 1&2 ? " + is_same_file_12);
    System.out.println("is same file 1&3 ? " + is_same_file_13);
    System.out.println("is same file 2&3 ? " + is_same_file_23);
} catch (IOException e) {
    System.err.println(e);
}

输出如下所示:


is same file 1&2 ? true

is same file 1&3 ? true

is same file 2&3 ? true

检查文件可见性

如果你需要找出一个文件是否被隐藏,你可以调用Files.isHidden()方法。记住“隐藏”的概念是依赖于平台/提供商的,你只需要通过Path检查并得到truefalse响应。下面的代码片段检查MutuaMadridOpen.txt文件是否是隐藏文件(该文件必须存在于C:\rafaelnadal\tournaments\2009目录中):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2009",
                                                                       "MutuaMadridOpen.txt");
…
try {
    boolean is_hidden = Files.isHidden(path);
    System.out.println("Is hidden ? " + is_hidden);
} catch (IOException e) {
    System.err.println(e);
}

创建和读取目录

谈到创建和读取目录,NIO.2 在Files类中提供了一组专用方法。在本节中,您将了解如何列出文件系统根目录、创建目录(包括临时目录)、列出目录的内容,以及编写和使用目录过滤器。

列出文件系统根目录

在 Java 6 中,文件系统根目录被提取为一组File对象。从 Java 7 开始,NIO.2 将文件系统根目录作为Path对象的Iterable来获取。这个IterablegetRootDirectories()方法返回,如下所示:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name : dirs) {
     System.out.println(name);
}

可能的输出如下:


C:\

D:\

E:\

你可以很容易地从Iterable进入一个数组,如下所示:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories(); ArrayList<Path> list = new ArrayList<Path>(); for (Path name : dirs) {      // System.out.println(name); `     list.add(name); } Path[] arr = new Path[list.size()]; list.toArray(arr);

for(Path path : arr) {     System.out.println(path); }`

如果您需要将文件系统根目录提取为一个数组File,请使用 Java 6 解决方案:

File[] roots = File.listRoots();
for (File root : roots) {
     System.out.println(root);
}
创建新目录

创建一个新目录是一个常见的任务,可以通过调用Files.createDirectory()方法来完成。这个方法获取要创建的目录(Path)和一个可选的文件属性列表(FileAttribute<?>),以便在创建时自动设置。它返回创建的目录。下面的代码片段使用默认属性在C:\rafaelnadal\tournaments目录下创建一个名为\2010的新目录(该目录必须不存在):

Path newdir = FileSystems.getDefault().getPath("C:/rafaelnadal/tournaments/2010/");
…
try {
    Files.createDirectory(newdir);
} catch (IOException e) {
    System.err.println(e);
}

您可以在创建时添加一组属性,如下面的示例代码片段所示,它在具有特定权限的 POSIX 文件系统上创建一个新目录:

Path newdir = FileSystems.getDefault().getPath("/home/rafaelnadal/tournaments/2010/");
…
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
try {
    Files.createDirectory(newdir, attr);
} catch (IOException e) {
       System.err.println(e);
}

Image 注意如果目录存在,那么createDirectory()方法会抛出异常。

有时你需要创建不止一个目录。例如,您可能需要创建一系列分层目录,如\statistics\win\prizes。您可以调用一系列createDirectory()方法,或者更优雅地使用Files.createDirectories()方法,这将在一次调用中创建目录序列;根据需要,从上到下创建目录,将\statistics作为相对根,\prizes作为最后一片叶子。目录序列作为一个Path实例传递,在创建目录时可以自动设置文件属性列表,也可以不设置。以下代码片段显示了如何在C:\rafaelnadal目录下创建一系列分层目录:

Path newdir= FileSystems.getDefault().getPath("C:/rafaelnadal/", "statistics/win/prizes");
…
try {
    Files.createDirectories(newdir);
} catch (IOException e) {
    System.err.println(e);
}

Image 注意如果目录序列中已经存在一个或多个目录,那么createDirectories()方法不会抛出异常,而只是“跳转”该目录并进入下一个目录。这种方法在创建一些目录后可能会失败,但不是全部。

列出目录的内容

使用目录和文件通常涉及为了不同的目的循环目录的内容。NIO.2 通过一个名为DirectoryStream的可迭代流提供了这个工具,这是一个实现Iterable的接口。通过Files.newDirectoryStream()方法可以直接访问目录流,该方法获取目录的Path并返回一个新的打开的目录流。

列举全部内容

以下代码片段将以链接、文件、子目录和隐藏文件的形式返回目录的全部内容(列出的目录是C:\rafaelnadal\tournaments\2009):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");

//no filter applied
System.out.println("\nNo filter applied:");
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
     for (Path file : ds) {
          System.out.println(file.getFileName());
     }
}catch(IOException e) {
   System.err.println(e);
}

一个可能的输出如下(这是C:\rafaelnadal\tournaments\2009 directory的全部内容):


No filter applied:

AEGON.txt

BNP.txt

MutuaMadridOpen.txt

supershot.bmp

Tickets.zip

TournamentsCalendar.xls

Videos

…

通过应用 Glob 模式列出内容

有时,您可能需要仅列出符合特定标准的内容,这需要对目录内容应用过滤器。通常,您只需要提取名称与特定模式匹配的文件和子目录。NIO.2 将这种特殊模式定义为内置的 glob 过滤器。根据 NIO.2 文档,glob 模式只是一个与其他字符串匹配的字符串——在本例中是目录和文件名。因为这是一种模式,所以它必须遵守一些规则,如下所示:

  • *:表示(匹配)任意数量的字符,包括无。
  • **:类似于*,但是跨越了目录的界限。
  • ?:精确表示(匹配)一个字符。
  • {}:表示由逗号分隔的子模式的集合。例如,{A,B,C}匹配 A、B 或 c。
  • []:传达一组单个字符或一系列字符(如果有连字符的话)。一些常见的例子包括:
    • [0-9]:匹配任意数字
    • [A-Z]:匹配任何大写字母
    • [a-z,A-Z]:匹配任何大写或小写字母
    • [12345]:匹配 1、2、3、4 或 5 中的任意一个
  • 方括号内,*?\匹配自己。
  • 其他所有字符都匹配自己。
  • 为了匹配*?或其他特殊字符,您可以使用反斜杠字符\对它们进行转义。例如,\\匹配单个反斜杠,\?匹配问号。

既然您已经知道了如何构建 glob 模式,那么是时候引入newDirectoryStream()方法了,该方法将Path获取到目录并应用 glob 过滤器。以下示例将从C:\rafaelnadal\tournaments\2009目录中提取所有 PNG、JPG 和 BMP 类型的文件(不管它们的名称如何):

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");
…
//glob pattern applied
System.out.println("\nGlob pattern applied:");
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, "*.{png,jpg,bmp}")) {
     for (Path file : ds) {
          System.out.println(file.getFileName());
     }
} catch (IOException e) {
    System.err.println(e);
}

输出如下所示:


Glob pattern applied:

supershot.bmp

通过应用用户定义的过滤器来列出内容

如果 glob 模式不能满足您的需求,那么是时候编写您自己的过滤器了。这是一个简单的任务,需要实现DirectoryStream.Filter<T>接口,它只有一个名为accept()的方法。根据您的实现,接受或拒绝一个Path。例如,以下代码片段只接受最终结果中的目录:

Path path = Paths.get("C:/rafaelnadal/tournaments/2009");
…
//user-defined filter - only directories are accepted
DirectoryStream.Filter<Path> dir_filter = new DirectoryStream.Filter<Path>() {

public boolean accept(Path path) throws IOException {
      return (Files.isDirectory(path, NOFOLLOW_LINKS));
  }
};

接下来,创建的过滤器作为参数传递给newDirectoryStream()方法:

System.out.println("\nUser defined filter applied:"); try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, dir_filter)) { for (Path file : ds) {       System.out.println(file.getFileName());      } } catch (IOException e) {     System.err.println(e); }

输出如下所示:


User defined filter applied:

videos

下表列出了一组常用的过滤器:

  • 仅接受大于 200KB 的文件/目录的过滤器:`DirectoryStream.Filter size_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {      return (Files.size(path) > 204800L);   } };`

  • 仅接受当天修改的文件的过滤器:`DirectoryStream.Filter time_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {      long currentTime = FileTime.fromMillis(System.currentTimeMillis()).to(TimeUnit.DAYS);      long modifiedTime = ((FileTime) Files.getAttribute(path, "basic:lastModifiedTime",                                                        NOFOLLOW_LINKS)).to(TimeUnit.DAYS);      if (currentTime == modifiedTime) {              return true;         }

        return false;   } };`

  • 仅接受隐藏文件/目录的过滤器:`DirectoryStream.Filter hidden_filter = new DirectoryStream.Filter() {

    public boolean accept(Path path) throws IOException {      return (Files.isHidden(path));   } };`

创建、读取和写入文件

对文件最常见的操作可能包括创建、读取和/或写入操作。NIO.2 附带了许多专用方法,用于以不同的复杂性和性能级别执行这些操作,从常用的小文件方法(将所有字节读入字节数组很方便的情况)到高级功能的方法,如文件锁定和内存映射 I/O。本节从小文件的方法开始,以缓冲和非缓冲流的方法结束。

一个代表一个输入源或一个输出目的地(它可以是从磁盘文件到内存数组的任何东西)。流支持不同种类的数据,如字符串、字节、原始数据类型、本地化字符和对象。在无缓冲流中,每个读或写请求都由底层操作系统直接处理,而在缓冲流中,数据是从称为缓冲区的内存区域读取的;并且只有当缓冲区为空时才调用本地输入 API。类似地,缓冲的输出流将数据写入缓冲区,并且仅当缓冲区已满时才调用本机输出 API。当一个缓冲区没有等待它被填满就被写出来时,我们说这个缓冲区被刷新

使用标准开放选项

从 NIO.2 开始,专用于创建、读取和写入动作(或任何其他涉及打开文件的动作)的方法支持可选参数OpenOption,该参数配置如何打开或创建文件。实际上,OpenOption是来自java.nio.file包的一个接口,它有两个实现:LinkOption类(还记得众所周知的NOFOLLOW_LINKS枚举常量)和StandardOpenOption类,后者定义了以下枚举:

Image

Image

在您看了创建一个新文件之后,这些常量中的一些将在接下来的部分中显示。

创建新文件

创建新文件是一个常见的任务,可以通过调用Files.createFile()方法来完成。这个方法获取要创建的文件(Path)和一个可选的文件属性列表(FileAttribute<?>),以便在创建时自动设置。它返回创建的文件。下面的代码片段在C:\rafaelnadal\tournaments\2010目录(该目录必须存在)中创建一个名为SonyEricssonOpen.txt的新文件,该文件具有默认属性(最初,该文件必须不存在;否则将抛出FileAlreadyExistsException异常):

Path newfile = FileSystems.getDefault().
                           getPath("C:/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");
…
try {
    Files.createFile(newfile);
} catch (IOException e) {
    System.err.println(e);
}

您可以在创建时添加一组属性,如下面的代码片段所示。这段代码在具有特定权限的 POSIX 文件系统上创建一个新文件。

Path newfile = FileSystems.getDefault().
               getPath("/home/rafaelnadal/tournaments/2010/SonyEricssonOpen.txt");

Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
try {
    Files.createFile(newfile, attr);
} catch (IOException e) {
    System.err.println(e);
}

您很快就会看到,这不是创建新文件的唯一方法。

编写小文件

NIO.2 为编写小型二进制/文本文件提供了一个优雅的解决方案。该功能通过两种Files.write()方法提供。这两种方法都打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。在所有字节或行都被写入后,该方法关闭文件(即使发生 I/O 错误或异常,它也会关闭文件)。简而言之,这个方法就像CREATETRUNCATE_EXISTINGWRITE选项存在一样——当然,当没有指定其他选项时,这是默认适用的。

使用 write()方法写入字节

将字节写入文件可以通过Files.write()方法完成。此方法获取文件的路径、包含要写入的字节的字节数组以及指定如何打开文件的选项。它返回写入文件的路径。

下面的代码片段用默认的打开选项(文件名为ball.png,将被写入C:\rafaelnadal\photos目录)写一个字节数组(代表一个小网球图片):

Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
…
byte[] ball_bytes = new byte[]{
(byte)0x89,(byte)0x50,(byte)0x4e,(byte)0x47,(byte)0x0d,(byte)0x0a,(byte)0x1a,(byte)0x0a,
(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x0d,(byte)0x49,(byte)0x48,(byte)0x44,(byte)0x52,
(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x10,
(byte)0x08,(byte)0x02,(byte)0x00,            
…
(byte)0x49,(byte)0x45,(byte)0x4e,(byte)0x44,(byte)0xae,(byte)0x42,(byte)0x60,(byte)0x82
};

try {
    Files.write(ball_path, ball_bytes);
} catch (IOException e) {
    System.err.println(e);
}

现在,如果你检查相应的路径,你会发现一个代表网球的小图片。

而且,如果你需要写文本(String)并且你想使用这个方法,那么把文本转换成一个字节数组如下(文件名是wiki.txt,创建于C:\rafaelnadal\wiki):

Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
String rf_wiki = "Rafael \"Rafa\" Nadal Parera (born 3 June 1986) is a Spanish professional
tennis " + "player and a former World No. 1\. As of 29 August 2011 (2011 -08-29)[update], he is
ranked No. 2 " + "by the Association of Tennis Professionals (ATP). He is widely regarded as
one of the greatest players " + "of all time; his success on clay has earned him the nickname
\"The King of Clay\", and has prompted " + "many experts to regard him as the greatest clay
court player of all time. Some of his best wins are:";

try {
    byte[] rf_wiki_byte = rf_wiki.getBytes("UTF-8");
    Files.write(rf_wiki_path, rf_wiki_byte);
} catch (IOException e) {
    System.err.println(e);
}

即使这可行,使用下面描述的write()方法将文本写入文件也容易得多。

用 write()方法书写线条

将行写入文件可以通过使用Files.write()方法来完成(一个“行”是一个字符序列)。在每一行之后,这个方法追加平台的行分隔符(line.separator系统属性)。该方法获取文件的路径、char 序列上的 iterable 对象、用于编码的字符集以及指定文件打开方式的选项。它返回写入文件的路径。

下面的代码片段将一些行写入一个文件(实际上,它将一些行附加到前面部分创建的文件wiki.txt的末尾):

Path rf_wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
ArrayList<String> lines = new ArrayList<>();
lines.add("\n");
lines.add("Rome Masters - 5 titles in 6 years");
lines.add("Monte Carlo Masters - 7 consecutive titles (2005-2011)");
lines.add("Australian Open - Winner 2009");
lines.add("Roland Garros - Winner 2005-2008, 2010, 2011");
lines.add("Wimbledon - Winner 2008, 2010");
lines.add("US Open - Winner 2010");

try {
    Files.write(rf_wiki_path, lines, charset, StandardOpenOption.APPEND);
} catch (IOException e) {
    System.err.println(e);
}
阅读小文件

NIO.2 提供了一种快速读取小字节/文本文件的方法。该工具通过Files.readAllBytes()Files.readAllLines()方法提供。这些方法将整个文件的字节或行分别读入一次读取,并在文件被读取或发生 I/O 错误或异常后为您打开和关闭流。

使用 readAllBytes()方法读取

Files.readAllBytes()方法将整个文件读入一个字节数组,而Files.readAllLines()方法将整个文件读入一个String的集合(如下一节所述)。关注readAllBytes()方法,下面的代码片段将先前创建的ball.png二进制文件(该文件必须存在)读入一个字节数组(文件路径作为参数传递):

Path ball_path = Paths.get("C:/rafaelnadal/photos", "ball.png");
…
try {
    byte[] ballArray = Files.readAllBytes(ball_path);            
} catch (IOException e) {
    System.out.println(e);
}

如果您想确保返回的字节数组包含图片,您可以运行(作为测试)下面的代码片段,它将字节写入同一目录中名为bytes_to_ball.png的文件中:

…
Files.write(ball_path.resolveSibling("bytes_to_ball.png"), ballArray);
…

或者你也可以如下使用ImageIO。行ImageIO.write()将把您的bufferedImage数据作为 PNG 类型的文件写入您的磁盘,并将它存储在C:\rafaelnadal\photos目录中。

BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(ballArray));
ImageIO.write(bufferedImage, "png", (ball_path.resolveSibling("bytes_to_ball.png")).toFile());

readAllBytes()方法也可以读取文本文件。这一次,字节数组应该被转换为String,如下例所示(您可以使用任何适合您的文本文件的字符集):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
try {
    byte[] wikiArray = Files.readAllBytes(wiki_path);
    String wikiString = new String(wikiArray, "ISO-8859-1");
    System.out.println(wikiString);
} catch (IOException e) {
    System.out.println(e);
}

Image 注意如果文件太大(大于 2GB),那么数组的大小无法分配,会抛出OutOfMemory错误。这取决于 JVM 上的Xmx参数:对于 32 位 JVM,它不能大于 2GB(但默认情况下通常会更小,256MB,这取决于平台)。对于 64 位 JVM 来说,它可以大得多——可能有几十亿字节。

使用 readAllLines()方法读取

在前面的例子中,您看到了如何通过readAllBytes()方法读取文本文件。一个更方便的解决方案是使用readAllLines()方法,因为该方法将读取整个文件并返回一个StringList,可以很容易地如下循环(将文件的Path和用于解码的字符集传递给该方法):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt"); … Charset charset = Charset.forName("ISO-8859-1"); try {     List<String> lines = Files.readAllLines(wiki_path, charset);     for (String line : lines) {          System.out.println(line);     } } catch (IOException e) {     System.out.println(e); }

根据官方文件,该方法将以下内容识别为行终止符:

  • \u000D后接\u000A:回车后接换行
  • \u000A:换行
  • \u000D:回车
使用缓冲流

在大多数操作系统中,读取或写入数据的系统调用是一项开销很大的操作。缓冲区可以通过在缓冲的方法和操作系统之间提供一个内存空间来解决这个问题。在调用本机 API 之前,这些方法从操作系统和应用之间的缓冲区获取数据或将数据放入缓冲区,这提高了应用的效率,因为它减少了系统调用的数量-仅当缓冲区满或空时才访问磁盘,这取决于它是写操作还是读操作。NIO.2 提供了两种通过缓冲区读写文件的方法:分别是Files.newBufferedReader()Files.newBufferedWriter()。这两种方法都获得一个Path实例,并返回一个旧的 JDK 1.1 BufferedReaderBufferedWriter实例。

使用 newBufferedWriter()方法

newBufferedWriter()方法获取文件的路径、用于编码的字符集和指定文件打开方式的选项。它返回一个新的默认缓冲写入器(这是一个特定于java.ioBufferedWriter)。该方法打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。简而言之,该方法就像CREATETRUNCATE_EXISTINGWRITE选项一样(当没有指定其他选项时,这是默认适用的)。

下面的代码片段使用一个缓冲区将数据追加到先前创建的wiki.txt文件中(该文件存在;您应该在C:\rafaelnadal\wiki目录中找到它):

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
String text = "\nVamos Rafa!";
try (BufferedWriter writer = Files.newBufferedWriter(wiki_path, charset,
                                                                 StandardOpenOption.APPEND)) {
     writer.write(text);
} catch (IOException e) {
     System.err.println(e);
}
使用 newBufferedReader()方法

newBufferedReader()方法可用于通过缓冲区读取文件。方法获取文件的路径和用于将字节解码为字符的字符集。它返回一个新的默认缓冲读取器(这是一个特定于java.ioBufferedReader)。

下面的代码片段使用 UTF-8 字符集读取wiki.txt文件:

Path wiki_path = Paths.get("C:/rafaelnadal/wiki", "wiki.txt");
…
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(wiki_path, charset)) {
     String line = null;
     while ((line = reader.readLine()) != null) {
             System.out.println(line);
     }
} catch (IOException e) {
     System.err.println(e);
}

如果您按照前面几节中的示例并创建了整个wiki.txt文件,那么前面的代码将输出以下内容:


Rafael "Rafa" Nadal Parera (born 3 June 1986) is a Spanish professional tennis player and a
former World No. 1\. As of 29 August 2011 (2011 -08-29)[update], he is ranked No. 2 by the
Association of Tennis Professionals (ATP). He is widely regarded as one of the greatest
players of all time; his success on clay has earned him the nickname "The King of Clay", and
has prompted many experts to regard him as the greatest clay court player of all time. Some
of his best wins are:

Rome Masters - 5 titles in 6 years

Monte Carlo Masters - 7 consecutive titles (2005-2011)

Australian Open - Winner 2009

Roland Garros - Winnner 2005-2008, 2010, 2011

Wimbledon - Winner 2008, 2010

US Open - Winner 2010

Vamos Rafa!

使用无缓冲流

非缓冲流可以通过新的 NIO.2 方法获得,既可以逐字使用,也可以使用java.io API 提供的包装习惯用法转换成缓冲流。无缓冲流方法有Files.newInputStream()(从文件中读取的输入流)和Files.newOutputStream()(写入文件的输出流)。

使用 newOutputStream()方法

newOutputStream()方法获取文件的路径和指定如何打开文件的选项。它返回一个新的默认线程安全的非缓冲流,可以用来向文件写入字节(这是一个特定于java.ioOutputStream)。该方法打开文件进行写入(如果文件不存在,这可能涉及到创建文件),或者最初将现有的常规文件截断为 0 字节大小。简而言之,该方法就像CREATETRUNCATE_EXISTINGWRITE选项一样(当没有指定其他选项时,这是默认适用的)。

以下代码片段将把文本行“球拍:Babolat AeroPro Drive GT”写入文件C:\rafaelnadal\equipment\racquet.txt(该文件最初并不存在,但由于没有指定选项,它将被自动创建):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
String racquet = "Racquet: Babolat AeroPro Drive GT";

byte data[] = racquet.getBytes();
try (OutputStream outputStream = Files.newOutputStream(rn_racquet)) {
     outputStream.write(data);
} catch (IOException e) {
     System.err.println(e);
}

此外,如果您决定使用缓冲流而不是前面的代码是一个更好的主意,建议使用基于java.io API 的转换,如下面的代码所示,它将文本“String: Babolat RPM Blast 16”附加到文件racquet.txt(该文件必须存在):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
String string = "\nString: Babolat RPM Blast 16";

try (OutputStream outputStream = Files.newOutputStream(rn_racquet, StandardOpenOption.APPEND);
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
      writer.write(string);
} catch (IOException e) {
     System.err.println(e);
}
使用 newInputStream()方法

newInputStream()方法获取要打开的文件的路径和指定如何打开文件的选项。它返回一个新的默认线程安全的非缓冲流,可以用来从文件中读取字节(这是一个特定于java.ioInputStream)。方法打开文件进行读取;如果没有选项,则相当于用READ选项打开文件。

以下代码片段读取文件racquet.txt的内容(该文件必须存在):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt"); … int n;     try (InputStream in = Files.newInputStream(rn_racquet)) {      while ((n = in.read()) != -1) {        System.out.print((char)n);                     } } catch (IOException e) {     System.err.println(e); }

正如您可能已经从java.io API 中了解到的那样,InputStream类还提供了一个read()方法,用于填充 byte 类型的缓冲数组。因此,您可以修改前面的代码,如下所示(请记住,您仍然在处理一个无缓冲的流):

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
…
int n;    
byte[] in_buffer = new byte[1024];
try (InputStream in = Files.newInputStream(rn_racquet)) {
     while ((n = in.read(in_buffer)) != -1) {
            System.out.println(new String(in_buffer));
     }
} catch (IOException e) {
     System.err.println(e);
}

Image 注意调用read(in_buffer)方法和调用read(in_buffer,0,in_buffer.length)方法是一回事。

此外,您可以通过与java.io API 交互操作,将非缓冲流转换为缓冲流。下面的示例与前面的示例效果相同,但效率更高:

Path rn_racquet = Paths.get("C:/rafaelnadal/equipment", "racquet.txt");
…
try (InputStream in = Files.newInputStream(rn_racquet);
     BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
     String line = null;
     while ((line = reader.readLine()) != null) {
             System.out.println(line);
     }
} catch (IOException e) {
     System.err.println(e);
}

过去的三个示例将具有相同的输出:


Racquet: Babolat AeroPro Drive GT

String: Babolat RPM Blast 16

创建临时目录和文件

临时目录是存储临时文件的目录。临时目录的位置取决于操作系统。在 Windows 中,临时目录是通过 TEMP 环境变量设置的,通常是C:\Temp%Windows%\Temp,或者是Local Settings\Temp中的每个用户一个临时目录。在 Linux/Unix 中,全局临时目录是/tmp/var/tmp

创建临时目录

在 NIO.2 中,可以用createTempDirectory()方法创建一个临时目录。在默认操作系统位置创建一个临时目录可以通过调用带有两个参数的createTempDirectory()方法来完成:一个用于生成目录名的前缀字符串(可以是null)和一个可选的文件属性列表,在创建目录时自动设置。以下代码片段创建了两个临时目录,一个带前缀,一个不带前缀:

String tmp_dir_prefix = "nio_";
try {
    //passing null prefix
    Path tmp_1 = Files.createTempDirectory(null);
    System.out.println("TMP: " + tmp_1.toString());

    //set a prefix
    Path tmp_2 = Files.createTempDirectory(tmp_dir_prefix);
    System.out.println("TMP: " + tmp_2.toString());
} catch (IOException e) {
    System.err.println(e);
}

以下是可能的输出:


TMP: C:\Users\Leo\AppData\Local\Temp\3238630399269555448

TMP: C:\Users\Leo\AppData\Local\Temp\nio_1097550355199661257

Image 注意如果不知道临时目录的默认位置是什么,可以使用下面的代码:

//output: C:\Users\Leo\AppData\Local\Temp\
String default_tmp = System.getProperty("java.io.tmpdir");
System.out.println(default_tmp);

更进一步,您可以通过调用另一个createTempDirectory()方法来指定创建临时目录的默认目录。除了临时目录前缀和可选的属性列表,这个方法还获得了一个代表临时目录的默认目录的Path。以下示例在C:\rafaelnadal\tmp目录中创建一个临时目录:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/");
String tmp_dir_prefix = "rafa_";
…
try {
    //create a tmp directory in the base dir
    Path tmp = Files.createTempDirectory(basedir, tmp_dir_prefix);
    System.out.println("TMP: " + tmp.toString());
} catch (IOException e) {
    System.err.println(e);
}

以下是可能的输出:


TMP: C:\rafaelnadal\tmp\rafa_1753327229539718259

使用关机挂钩删除临时目录

大多数操作系统会自动删除临时目录(如果没有,您可以使用几种清理软件中的一种)。但是,有时您可能需要以编程方式控制删除过程。方法只做了一半的工作,因为删除是你的责任。为此,您可以附加一个关闭挂钩机制,一个用于执行任何资源清理或保存的运行时机制,这些必须在 JVM 关闭之前发生。这个钩子可以作为 Java Thread来实现。当钩子在关机时被 JVM 执行时,Threadrun()方法将被执行。图 4-1 中的显示了一个漂亮而简单的关断钩流程设计。

Image

图 4-1 。关机挂钩的简单流程设计

将图 4-1 中所示的图放入代码行中,提供如下框架代码:

`Runtime.getRuntime().addShutdownHook(new Thread() {

@Override public void run() {   System.out.println("Shutdown-hook activated ...");

  //… here, cleanup/save resources

  System.out.println("Shutdown-hook successfully executed ...");   } });`

Image 注意注意添加一个关闭挂钩作为ThreadRuntime可以作为一个匿名的内部类来完成,就像前面的代码一样,或者作为一个单独的类来实现Runnable或者扩展Thread

当 JVM 关闭时,shutdown-hook 是删除临时目录的一个很好的解决方案,但是,您可能知道,如果目录不是空的,就不能删除它;因此,您需要遍历临时目录的内容,并在删除临时目录本身之前删除每个条目。至此,您已经知道如何向下遍历一个目录的内容,所以现在假设您的临时目录只包含临时文件(在许多实际情况下都是如此)和其他空的临时目录。在本书的后面,你将看到如何实现递归操作来浏览层次结构的所有级别。

以下示例将上一节中列出目录内容的代码与 shutdown-hook 结合在一起:

`final Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/"); final String tmp_dir_prefix = "rafa_";

try { //create a tmp directory in the base dir final Path tmp_dir = Files.createTempDirectory(basedir, tmp_dir_prefix);

Runtime.getRuntime().addShutdownHook(new Thread() {

@Override public void run() {   System.out.println("Deleting the temporary folder ...");

  try (DirectoryStream ds = Files.newDirectoryStream(tmp_dir)) {        for (Path file : ds) {                Files.delete(file);        }

   Files.delete(tmp_dir);

   } catch (IOException e) {        System.err.println(e);    }

   System.out.println("Shutdown-hook completed..."); } });

//simulate some I/O operations over the temporary file by sleeping 10 seconds //when the time expires, the temporary file is deleted             Thread.sleep(10000); //operations done

} catch (IOException | InterruptedException e) {     System.err.println(e); }`

Image 注意前面的例子使用了一个Thread.sleep()方法在临时目录的创建时间和 JVM 关闭时间之间添加一个延迟。显然,代替它的是,您将提供使用临时目录的业务逻辑,该临时目录是为作业创建的。

使用 deleteOnExit()方法删除临时目录

删除临时目录的另一个解决方案是调用deleteOnExit()方法。这个方法在java.io.File类中可用(不特定于 NIO.2 ),它将在 JVM 关闭时删除传递的文件或目录。因为必须为每个临时文件或目录调用此方法,所以它被认为是最没有吸引力的选择,因为它将为每个临时实体消耗内存。

Image 注意如果你的系统长时间处于活动状态或者在短时间内创建了许多临时文件或目录,那么使用deleteOnExit()是个坏主意!在选择使用deleteOnExit()之前,要考虑到它会使用大量的内存,直到 JVM 终止才会释放。

以下代码片段向您展示了如何使用deleteOnExit():

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp/"); String tmp_dir_prefix = "rafa_";

try {     //create a tmp directory in the base dir     Path tmp_dir = Files.createTempDirectory(basedir, tmp_dir_prefix);

    File asFile = tmp_dir.toFile();     asFile.deleteOnExit();

    //simulate some I/O operations over the temporary file by sleeping 10 seconds     //when the time expires, the temporary file is deleted                 //EACH CREATED TEMPORARY ENTRY SHOULD BE REGISTERED FOR DELETE ON EXIT     Thread.sleep(10000);     //operations done

} catch (IOException | InterruptedException e) {    System.err.println(e); }`

Image 注意因为deleteOnExit()适用于File实例,而不是Path,所以需要通过调用Path.toFile()方法将Path转换为File

创建临时文件

这一节将详细介绍临时文件以及 NIO.2 处理这些文件的方法。在实际应用中,临时文件通常会提供非常有用的帮助。当您需要在应用或应用执行之外使用的非缩进文件时,它们非常有用。在 Java 中称为“工作文件”,它们可以放在从应用中选择的任何目录中,或者放在由 Java 属性java.io.tmpdir返回的默认位置中。

在 NIO.2 中,可以用createTempFile()方法创建一个临时文件。在默认的操作系统位置创建一个临时文件可以通过调用带有三个参数的createTempFile()方法来完成:一个前缀字符串连接在文件名前面(可以是null),一个后缀字符串连接在文件名后面(可以是null);缺省值是.tmp,以及创建文件时自动设置的可选文件属性列表。以下代码片段创建了两个临时文件,一个没有前缀和后缀,另一个具有指定的前缀和后缀:

String tmp_file_prefix = "rafa_";
String tmp_file_sufix=".txt";

try {
    //passing null prefix/suffix
    Path tmp_1 = Files.createTempFile(null,null);
    System.out.println("TMP: " + tmp_1.toString());

    //set a prefix and a suffix
    Path tmp_2 = Files.createTempFile(tmp_file_prefix, tmp_file_sufix);
    System.out.println("TMP: " + tmp_2.toString());

} catch (IOException e) {
    System.err.println(e);
}

输出将是操作系统默认位置中的两个空临时文件:


TMP: C:\Users\Leo\AppData\Local\Temp\6873427319542945524.tmp

TMP: C:\Users\Leo\AppData\Local\Temp\rafa_6168226983257408796.txt

Image 注意如果不知道临时文件的默认位置是什么,可以使用下面的代码:

//output: C:\Users\Leo\AppData\Local\Temp\
String default_tmp = System.getProperty("java.io.tmpdir");

更进一步,您可以通过调用另一个createTempFile()方法来指定创建临时文件的默认目录。除了临时文件的前缀和后缀以及可选的属性列表,这个方法还获得了一个代表临时文件的默认目录的Path。下面是一个在C:\rafaelnadal\tmp目录下创建临时文件的例子:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix=".txt";

try {
    Path tmp_3 = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);
    System.out.println("TMP: " + tmp_3.toString());
} catch (IOException e) {
    System.err.println(e);
}

输出将是C:\rafaelnadal\tmp目录中的一个空临时文件:


TMP: C:\rafaelnadal\tmp\rafa_512352743612949417.txt

使用关机挂钩删除临时文件

临时文件只是一个简单的文件,直到您确定它确实是临时的,这意味着自动机制必须定期或在指定的时间删除临时文件。shutdown-hook 机制在本章前面的“使用 Shutdown-Hook 删除临时目录”一节中已经介绍过了这种机制对于临时文件也是一样的,所以我们将跳过这里的演示,直接看代码示例。

下面的代码片段将在C:\rafaelnadal\tmp目录中创建一个临时文件,等待 10 秒钟(模拟一些文件使用),并在 JVM 通过 shutdown-hook 机制关闭时删除该文件:

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp"); String tmp_file_prefix = "rafa_"; String tmp_file_sufix = ".txt";

try {     final Path tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);

    Runtime.getRuntime().addShutdownHook(new Thread() { @Override     public void run() {     System.out.println("Deleting the temporary file ...");

    try {         Files.delete(tmp_file);     } catch (IOException e) {         System.err.println(e);     }

    System.out.println("Shutdown hook completed...");   } });

//simulate some I/O operations over the temporary file by sleeping 10 seconds //when the time expires, the temporary file is deleted             Thread.sleep(10000); //operations done

} catch (IOException | InterruptedException e) {     System.err.println(e); }`

Image 注意前面的代码使用了一个Thread.sleep()方法在临时文件的创建时间和 JVM 关闭时间之间添加一个延迟。显然,代替它的是,您将为创建临时文件的作业提供使用临时文件的业务逻辑。

用 deleteOnExit()方法删除临时文件

删除临时文件的另一个解决方案是调用deleteOnExit()方法。这种机制在前面的“使用 deleteOnExit()方法删除临时目录”一节中有详细介绍,对于临时文件也是如此,所以我们在这里将跳过它,直接看代码示例。

下面的代码片段将在C:\rafaelnadal\tmp目录中创建一个临时文件,等待 10 秒钟(模拟一些文件使用),并在 JVM 通过deleteOnExit()机制关闭时删除它:

`Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp"); String tmp_file_prefix = "rafa_"; String tmp_file_sufix = ".txt";

try {     final Path tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);

    File asFile = tmp_file.toFile();     asFile.deleteOnExit();

    //simulate some I/O operations over the temporary file by sleeping 10 seconds     //when the time expires, the temporary file is deleted     Thread.sleep(10000);     //operations done

} catch (IOException | InterruptedException e) {     System.err.println(e); }`

Image 注意因为deleteOnExit()适用于File实例,而不是Path,所以需要通过调用Path.toFile()方法将Path转换为File

用 DELETE_ON_CLOSE 删除临时文件

删除临时文件的一个巧妙的解决方案是使用DELETE_ON_CLOSE选项。顾名思义,这个选项在流关闭时删除文件。例如,下面的代码片段使用createTempFile()方法在C:\rafaelnadal\tmp目录中创建一个临时文件,并使用显式指定的DELETE_ON_CLOSE为其打开一个流,因此当流关闭时,该文件应该被删除:

Path basedir = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp");
String tmp_file_prefix = "rafa_";
String tmp_file_sufix = ".txt";
Path tmp_file = null;

try {
    tmp_file = Files.createTempFile(basedir, tmp_file_prefix, tmp_file_sufix);
} catch (IOException e) {
    System.err.println(e);
}

try (OutputStream outputStream = Files.newOutputStream(tmp_file,
                                      StandardOpenOption.DELETE_ON_CLOSE);
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {

     //simulate some I/O operations over the temporary file by sleeping 10 seconds
     //when the time expires, the temporary file is deleted            
     Thread.sleep(10000);
     //operations done
} catch (IOException | InterruptedException e) {
     System.err.println(e);
}

此外,即使不调用createTempFile()方法,也可以模拟一个临时文件。只需定义一个文件名,并结合使用DELETE_ON_CLOSE选项和CREATE选项,如下面的代码片段所示(效果与前面的示例相同):

String tmp_file_prefix = "rafa_"; String tmp_file_sufix = ".txt"; `Path tmp_file = null;

tmp_file = FileSystems.getDefault().getPath("C:/rafaelnadal/tmp", tmp_file_prefix +                                                                 "temporary" + tmp_file_sufix);

try (OutputStream outputStream = Files.newOutputStream(tmp_file, StandardOpenOption.CREATE,                                                           StandardOpenOption.DELETE_ON_CLOSE);      BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {

     //simulate some I/O operations over the temporary file by sleeping 10 seconds      //when the time expires, the temporary file is deleted                  Thread.sleep(10000);      //operations done } catch (IOException | InterruptedException e) {      System.err.println(e); }`

删除、复制和移动目录和文件

删除、复制和移动是对文件和目录最常用的三种操作。NIO.2 提供了专门的方法来支持这些操作的不同方法。正如您将在本节中看到的,它们中的大多数来自于Files类。

删除文件和目录

NIO.2 提供了两种删除文件或目录的方法,Files.delete()Files.deleteIfExits()。两者都采用单个参数表示要删除的Path,但是Files.delete()返回 void,而Files.deleteIfExits()返回一个布尔值表示删除过程的成功或失败。delete()方法尝试删除传递的Path,如果失败,抛出以下异常之一:NoSuchFileException(如果传递的Path不存在)、DirectoryNotEmptyException(如果传递的Path是一个不为空的目录)、IOException(如果发生 I/O 错误),或者SecurityException(如果删除的访问被拒绝)。

以下代码片段从C:\rafaelnadal\photos\目录中删除文件rafa_1.jpg(该文件必须存在):

Path path = FileSystems.getDefault().getPath("C:/rafaelnadal/photos", "rafa_1.jpg");

//delete the file
try {
     Files.delete(path);
} catch (NoSuchFileException | DirectoryNotEmptyException | IOException |
         SecurityException e) {
     System.err.println(e);
}

顾名思义,Files.deleteIfExists()方法只删除存在的文件,这意味着如果文件因为不存在而无法删除,那么返回的布尔值将是false(而不是抛出NoSuchFileException异常)。当您有多个线程删除文件,并且您不想仅仅因为一个线程先这么做就抛出异常时,这很有用。记住前面的代码刚刚删除了rafa_1.jpg文件,下面的代码将返回false:

try {
    boolean success = Files.deleteIfExists(path);
    System.out.println("Delete status: " + success);
} catch (DirectoryNotEmptyException | IOException | SecurityException e) {
    System.err.println(e);
}

Image 注意如果删除的资源是一个目录,那么它一定是空的。删除整个目录内容(可能包含其他目录、文件等)是一项通常作为递归操作实现的任务。该操作在第五章的中介绍。

Image 注意如果文件是符号链接,那么删除的是符号链接本身,而不是链接的最终目标。

复制文件和目录

在 NIO.2 中,复制文件和目录是小菜一碟。它提供了三个Files.copy()方法来完成这项任务,并提供了一组选项来控制复制过程——这些方法采用由这些选项表示的一个varargs参数。这些选项在StandardCopyOptionLinkOption菜单下提供,如下所示:

  • REPLACE_EXISTING:如果复制的文件已经存在,那么它被替换(在非空目录的情况下,抛出FileAlreadyExistsException)。当处理一个符号链接时,链接的目标是而不是复制的;仅复制链接。
  • COPY_ATTRIBUTES:复制文件及其关联属性(至少支持复制lastModifiedTime属性)。
  • NOFOLLOW_LINKS:符号链接不应该跟随。

如果您不熟悉枚举类型,那么您应该知道它们可以按如下方式导入到应用中。这些被称为静态导入,可以导入任何静态字段或方法,而不仅仅是来自枚举类型的字段(例如来自java.lang.Math的方法)。

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

Image 注意默认情况下,复制符号链接时,复制的是该链接的目标。仅复制链接本身可以通过REPLACE_EXISTINGNOFOLLOW_LINKS选项完成。而且,不需要复制文件属性。

Image 警告试图复制一个非空的目录会导致一个空的目录。这是一个通常以递归操作实现的任务,正如你将在第五章中看到的。此外,复制文件不是原子操作,这意味着即使目标文件不完整或属性没有完全复制,也会抛出IOException异常并中止复制。

在两个路径之间复制

通常,当您复制文件时,您需要一个源路径(复制自)和一个目标路径(复制到)。基于这个简单的例子,NIO.2 提供了一个Files.copy()方法,该方法获取要复制的文件的路径、目标文件的路径以及一组用于控制复制过程的选项。它返回目标文件的路径。如果未指定选项,则仅当目标文件不存在并且不是符号链接时,复制才会成功结束。否则,除非源和目标不同,否则将抛出异常(方法isSameFile()返回true)。

下面的代码片段将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\USOpen(该文件必须存在)。它替换现有文件,将源的属性复制到目标,并且不遵循链接。

Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt");
Path copy_to= Paths.get("C:/rafaelnadal/grandslam/USOpen",copy_from.getFileName().toString());

try {

    Files.copy(copy_from, copy_to, REPLACE_EXISTING, COPY_ATTRIBUTES, NOFOLLOW_LINKS);

} catch (IOException e) {
    System.err.println(e);
}
从输入流复制到文件

当您需要将输入流中的所有字节复制到一个文件中时,您可以调用Files.copy()方法来获取要读取的输入流、文件的路径以及一组用于控制复制过程的选项。它返回读取或写入的字节数。默认情况下,如果目标文件已经存在或者是符号链接,复制将失败。

下面的代码片段将通过输入流将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\Wimbledon(该文件必须存在)。它将替换现有文件。

Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt");
Path copy_to = Paths.get("C:/rafaelnadal/grandslam/Wimbledon", "draw_template.txt");

try (InputStream is = new FileInputStream(copy_from.toFile())) {

     Files.copy(is, copy_to, REPLACE_EXISTING);

} catch (IOException e) {
     System.err.println(e);
}

可以用其他方式提取输入流。例如,以下代码片段将从 Internet URL 获取输入流(仅当文件不存在时,它才会将 URL 指示的图片复制到C:\rafaelnadal\photos目录):

Path copy_to = Paths.get("C:/rafaelnadal/photos/rafa_winner_2.jpg");
URI u = URI.create("https://lh6.googleusercontent.com/--
                        udGIidomAM/Tl8KTbYd34I/AAAAAAAAAZw/j2nH24PaZyM/s800/rafa_winner.jpg");

try (InputStream in = u.toURL().openStream()) {

     Files.copy(in, copy_to);

} catch (IOException e) {
     System.err.println(e);
}

Image 注意强烈建议您在发生 I/O 错误后立即关闭输入流。

从文件复制到输出流

当您需要将文件中的所有字节复制到输出流中时,您可以调用Files.copy()方法来获取文件的路径和要写入的输出流。它将返回读取或写入的字节数。

下面的代码片段将文件draw_template.txtC:\rafaelnadal\grandslam\AustralianOpen复制到C:\rafaelnadal\grandslam\RolandGarros。目标文件表示为输出流(如果目标文件存在,将被替换)。

`Path copy_from = Paths.get("C:/rafaelnadal/grandslam/AustralianOpen", "draw_template.txt"); Path copy_to = Paths.get("C:/rafaelnadal/grandslam/RolandGarros", "draw_template.txt");

try (OutputStream os = new FileOutputStream(copy_to.toFile())) {

     Files.copy(copy_from, os); } catch (IOException e) {      System.err.println(e); }`

Image 注意强烈建议您在发生 I/O 错误后立即关闭输出流。

移动文件和目录

在本节中,您将看到如何使用Files.move()方法移动文件和目录。此方法获取要移动的文件的路径、目标文件的路径以及控制移动过程的一组选项。这些选项在StandardCopyOption枚举下提供,在此列出:

  • REPLACE_EXISTING:如果目标文件已经存在,则仍然执行移动,并替换目标。处理符号链接时,符号链接被替换,但它指向的内容不受影响。
  • ATOMIC_MOVE:文件移动将作为一个原子操作来执行,这保证了任何监视文件目录的进程都将访问一个完整的文件。

同样,这些枚举类型可以像这样导入到应用中:

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;

默认情况下(没有显式指定选项时),move()方法尝试将文件移动到目标文件,如果目标文件存在则失败(FileAlreadyExistsException被抛出),除非源和目标是同一个文件(isSameFile()方法返回true),在这种情况下该方法无效。

Image 注意默认情况下,移动符号链接时,移动的是符号链接本身,而不是那个链接的目标。

Image 注意move()方法也可以用来移动空目录。试图移动一个非空的目录通常是一个递归复制操作,正如你在第五章中看到的。然而,如果不需要移动目录中的条目,也可以移动非空的目录。在某些情况下,目录包含创建目录时创建的特殊文件(如链接)的条目,如果目录只包含这些条目,则认为它是空的。

下面的代码片段试图将名为rafa_2.jpg(该文件必须存在)的文件从C:\rafaelnadal移动到C:\rafaelnadal\photos。如果目标已经存在,那么它将被替换,因为指定了REPLACE_EXISITING选项。

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/rafa_2.jpg");
Path moveto = FileSystems.getDefault().getPath("C:/rafaelnadal/photos/rafa_2.jpg");

try {
    Files.move(movefrom, moveto, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}

您可以通过使用Path.resolve()方法跳过对moveto路径中的文件名进行硬编码(更多详细信息,请参见第一章)。使用这种方法,您可以通过直接从movefrom路径中提取文件名来移动文件(在测试这段代码之前,不要忘记恢复C:\rafaelnadal中的rafa_2.jpg文件):

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/rafa_2.jpg");
Path moveto_dir = FileSystems.getDefault().getPath("C:/rafaelnadal/photos");

try {
    Files.move(movefrom, moveto_dir.resolve(movefrom.getFileName()),
                                                         StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}
重命名文件

最后,通过一个小技巧,您可以使用Files.move()Path.resolveSibling()方法重命名文件。下面的代码片段在C:\rafaelnadal\photos目录中将文件rafa_2.jpg重命名为rafa_renamed_2.jpg。如果您已经测试了前面的代码,那么rafa_2.jpg应该出现在这个目录中。

Path movefrom = FileSystems.getDefault().getPath("C:/rafaelnadal/photos/rafa_2.jpg");

try {
    Files.move(movefrom, movefrom.resolveSibling("rafa_2_renamed.jpg"),
                                                         StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    System.err.println(e);
}

总结

本章一开始探索了一些专门检查Path是否可读、可写、常规或隐藏的方法。然后重点讨论了目录操作以及如何列出、创建和读取目录。您看到了如何列出文件系统根目录,如何使用createDirectory()createTempDirectory()等方法创建目录,如何编写目录过滤器,以及如何使用newDirectoryStream()方法列出目录的内容。本章还探讨了文件操作,比如读、写、创建和打开文件。如您所见,有大量的文件 I/O 方法可供选择(针对缓冲和非缓冲流)。本章以众所周知的删除、复制和移动操作结束。*