在NIO.2的文件I/O中,Path
是必须掌握的关键类之一。Path
通常代表文件系统中的位置,比如C:workspacejava7developer(Windows文件系统中的目录)或/usr/bin/zip(*nix文件系统中zip程序的位置)。如果你能理解如何创建和处理路径,就能浏览任何类型的文件系统,包括zip归档文件系统。
我们通过图2-1(基于本书中的源码布局)来复习一下文件系统中的几个概念。
- 目录树
- 根路径
- 绝对路径
- 相对路径
图2-1 说明根目录、绝对路径和相对路径概念的目录树
之所以要讨论绝对路径和相对路径,是因为我们需要考虑程序运行的位置。比如说,有个程序可能在/java7developer/src/test目录下运行,代码要读取位于/java7developer/src/main目录下的文件名。为了进入/java7developer/src/main目录,可以用相对路径../main。
但如果程序运行在/java7developer/src/test/java/com/java7developer,用相对路径 ../main则无法进入目标目录,而会进到并不存在的目录/java7developer/src/test/java/com/main中。所以必须使用绝对路径,比如/java7developer/src/main。
反之亦然,你的程序可能一直运行在同一位置,比如图2-1中的target目录。但目录树的根目录可能会变,比如从/java7developer变成了D:workspacej7d。这时就不能依靠绝对路径,而要用相对路径转到你想到达的位置。
NIO.2中的Path
是一个抽象构造。你所创建和处理的Path
可以不马上绑定到对应的物理位置上。尽管看起来奇怪,但有时确实需要如此。比如说,你想创建一个Path
来表示即将创建的新文件。在调用Files.createFile(Path target)
1之前,这个文件是不存在的。如果在Path
所对应的文件创建之前,你试图读取这个文件中的内容,就会导致IOException
。如果你指定了一个并不存在的Path
并试图用Files.readAllBytes(Path)
之类的方法读取,结果是一样的。简言之,JVM只会把Path
绑定到运行时的物理位置上。
1 你一会儿就能在2.4节见到这个方法!
警告
在编写与具体文件系统相关的代码时要小心。创建
Path
为C:workspacejava7developer,然后试图读取它的程序,这只能在有C:workspacejava7developer位置的计算机上才能工作。一定要确保程序逻辑和异常处理考虑到了各种情况,包括可能在不同的文件系统上运行,或文件系统的结构可能被改动。本书的作者之一过去就犯过这个错误,结果把计算机系的一组硬盘全搞坏了!2
2 我们不会告诉你是谁,不过欢迎你把他查出来!
还是得再重复一遍,NIO.2把位置(由Path
表示)的概念和物理文件系统的处理(比如复制一个文件)分得很清楚,物理文件系统的处理通常是由Files
辅助类实现的。
有关Path
类以及将在本节讨论的其他类的进一步细节请见表2-1。
表2-1 学习文件I/O的关键基础类
Path
Path
类中的方法可以用来获取路径信息,访问该路径中的各元素,将路径转换为其他形式,或提取路径中的一部分。有的方法还可以匹配路径字串以及移除路径中的冗余项Paths
工具类,提供返回一个路径的辅助方法,比如get(String first, String... more)
和get(URI uri)
FileSystem
与文件系统交互的类,无论是默认的文件系统,还是通过其统一资源标识(URI)获取的可选文件系统FileSystems
工具类,提供各种方法,比如其中用于返回默认文件系统的FileSystems.getDefault
记住,Path
不一定代表真实的文件或目录。你可以随心所欲地操作Path
,用Files
中的功能来检查文件是否存在,并对它进行处理。
提示
Path
并不仅限于传统的文件系统,它也能表示zip或jar这样的文件系统。
我们来完成几个简单的任务,探索一下Path
类:
- 创建一个
Path
; - 获取
Path
的相关信息; - 移除
Path
中的冗余项; - 转换一个
Path
; - 合并两个
Path
,在两个Path
中间创建一个Path
,并对这两个Path
进行比较。
我们先从创建表示文件系统中某个位置的Path
开始。
2.2.1 创建一个Path
创建Path
没什么难的。调用Paths.get(String first,String...more)
是最快捷的做法,其中第二个变量一般用不到,它仅仅用来把额外的字符串合并起来形成Path
字符串。
提示 在NIO.2的API中,
Path
或Paths
中的各种方法抛出的受检异常只有IOException
。我们认为这虽然简单,但有时却会掩藏潜在的问题,而且如果你想处理IOException
的某个显式子类,则需要额外编写异常处理代码。
我们用Paths.get(String first)
方法为/usr/bin/目录下的文件压缩工具zip创建一个绝对Path
。
Path listing=Paths.get(/"/usr/bin/zip/");
调用Paths.get(/"/usr/bin/zip/")
的效果和调用下面这个长一点的代码效果一样:
Path listing=FileSystems.getDefault.getPath(/"/usr/bin/zip/");
提示
创建
Path
时可以用相对路径。比如,运行在/opt目录下的程序可以用../usr/bin/zip创建一个指向/usr/bin/zip的Path
。其含义是指进入/opt的上一层目录(即/),然后进入/usr/bin/zip。通过调用toAbsolutePath
方法,很容易把这个相对路径转换成绝对路径,如:listing.toAbsolutePath
。
你可以从Path
中获取信息,比如其父目录、文件名(如果有的话)等等。
2.2.2 从Path中获取信息
Path
类中有一组方法可以返回你正在处理的路径的相关信息。代码清单2-1为/usr/bin目录下的zip工具创建一个Path
,并输出相关信息,包括它的根目录和父目录。如果你的操作系统中zip也放在/usr/bin目录下,应该能见到下面这种输出信息。
File Name [zip]Number of Name Elements in the Path [3]Parent Path [/usr/bin]Root of Path [/]Subpath from Root, 2 elements deep [usr/bin]
你在计算机上看到的结果和你所用的操作系统及程序运行的位置有关。
代码清单2-1 从Path
中获取信息
import java.nio.file.Path;import java.nio.file.Paths;public class Listing_2_1 { public static void main(String args) { Path listing = Paths.get(/"/usr/bin/zip/");//创建绝对路径 System.out.println(/"File Name [/" + listing.getFileName + /"]/");//获取文件名 /*① 获取名称元素的数量*/ System.out.println(/"Number of Name Elements in the Path [/" + listing.getNameCount + /"]/"); /*② 获取Path的信息*/ System.out.println(/"Parent Path [/" + listing.getParent + /"]/"); System.out.println(/"Root of Path [/" + listing.getRoot + /"]/"); System.out.println(/"Subpath from Root, 2 elements deep [/" + listing.subpath(0, 2) + /"]/"); }}
在创建了/usr/bin/zip的Path
之后,你可以看一下组成Path
的元素个数,在此例中就是目录的数量①。相对其父目录和根目录,Path
的位置会更有用。也可以通过指定起始和终止的索引来挑出子路径。在本例中是从Path
的根(0)到其第二个元素(2)之间的子路径②。
如果这是你初次接触NIO.2文件API,这些方法对你来说非常重要,因为你可以借助它们查看路径处理的结果。
2.2.3 移除冗余项
在编写工具软件,比如属性文件分析器时,需要处理的Path
中可能会有一个或两个点:
- . 表示当前目录;
- .. 表示父目录。
假设你的程序在/java7developer/src/main/java/com/java7developer/chapter2/目录下运行(见图2-1)。你所在的目录和Listing_2_1.java
一样,如果传给你的Path
是./Listing_2.1.java
,其中的./
部分,即程序正在运行的目录,实际上并没什么用。在这里用短一点儿的Path
——Listing_2_1.java
就够了。
Path
中可能还有其他冗余项,比如符号链接(见2.4.3节)。假设在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt,但其实/usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置。要得到这个位置,就需要去掉冗余的符号信息。
所有这些冗余项都会导致 Path
指向的不是你认为它应该指向的位置。
在Java 7中,有两个辅助方法可以用来弄清Path
的真实位置。首先可以用normalize
方法去掉Path
中的冗余信息。下面的代码会返回Listing_2_1.java的Path
,去掉了表明它在当前目录(./部分)中的冗余符号。
Path normalizedPath = Paths.get(/"./Listing_2_1.java/").normalize;
此外,toRealPath
方法也很有效,它融合了toAbsolutePath
和normalize
两个方法的功能,还能检测并跟随符号连接。
还是回到*nix系统中的日志文件那个例子,在/usr/logs目录下有个日志文件log1.txt,而这个目录实际上是指向/application/logs的符号链接。通过调用toRealPath
,你能得到表示/application/logs/log1.txt的真正Path
。
Path realPath = Paths.get(/"/usr/logs/log1.txt/").toRealPath;
我们要讨论的最后一个Path API特性是比较多个Path
,并找出它们之间的Path
。
2.2.4 转换Path
转换路径最多的是工具软件。比如你可能需要比较文件之间的关系,以便了解源码目录树的结构是否符合编码规范。或者在shell脚本中执行程序时可能会传入一些Path
参数,并需要把这些参数变成有意义的Path
。在NIO.2里可以很容易地合并Path
,在两个Path
中再创建Path
或对Path
进行比较。
下面的代码将两个Path
合并,通过调用resolve
方法,将uat和conf/application.properties合并成表示/uat/conf/application.properties的完整Path
。
Path prefix = Paths.get(/"/uat//");Path completePath = prefix.resolve(/"conf/application.properties/");
要取得两个Path
之间的路径,可以用relativize(Path)
方法。下面的代码计算了从日志目录到配置目录之间的路径。
String logging = args[0];String configuration = args[1];Path logDir = Paths.get(logging);Path confDir = Paths.get(configuration);Path pathToConfDir = logDir.relativize(confDir);
如你所愿,你可以用startsWith(Path prefix)
、equals(Path path)
等值比较或endsWith(Path suffix)
来对路径进行比较。
现在使用Path
类对你来说应该没有问题了,但Java 7之前版本的那些代码怎么办呢?负责NIO.2的团队考虑到了向后兼容性,所以加了两个新的API特性来保证基于Path
的新I/O和老版本代码之间的互操作性。
2.2.5 NIO.2 Path和Java已有的File类
新API中的类可以完全替代过去基于java.io.File
的API。但你不可避免地要和大量遗留下来的I/O代码交互。Java 7提供了两个新方法:
java.io.File
类中新增了toPath
方法,它可以马上把已有的File
转化为新的Path
。Path
类中有个toFile
方法,它可以马上把已有的Path
转化为File
。
下面的代码演示了这一功能。
File file = new File(/"../Listing_2_1.java/");Path listing = file.toPath;listing.toAbsolutePath;file = listing.toFile;
现在,我们完成了对Path
类的探索。接着要研究一下Java 7对目录处理的支持,特别是对目录树。