在丑陋的 Java I/O 编程方式诞生多年以后,Java终于简化了文件读写的基本操作。
打开并读取文件对于大多数编程语言来是非常常用的,由于 I/O 糟糕的设计以至于很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。
在 Java7 中对此引入了巨大的改进。这些新元素被放在 java.nio.file 包下面,过去人们通常把 nio 中的 n 理解为 new 即新的 io,现在更应该当成是 non-blocking 非阻塞 io(io就是input/output输入/输出)。java.nio.file 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。
看一下文件操作的两个基本组件:
文件或者目录的路径;
文件本身。
文件和目录路径
一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上。java.nio.file.Paths 类包含一个重载方法 static get(),该方法接受一系列 String 字符串或一个统一资源标识符(URI)作为参数,并且进行转换返回一个 Path 对象。
当 toString() 方法生成完整形式的路径, getFileName() 方法总是返回当前文件名。
通过使用 Files 工具类,可以测试一个文件是否存在,测试是否是一个"普通"文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。“PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在”。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。
“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,toRealPath() 将返回实际情况下的 Path,并且还会删除任何冗余元素。
这里你会看到 URI 看起来只能用于描述文件,实际上 URI 可以用于描述更多的东西;通过 维基百科 可以了解更多细节。现在我们成功地将 URI 转为一个 Path 对象。
Path 中看到一些有点欺骗的东西,这就是调用 toFile() 方法会生成一个 File 对象。听起来似乎可以得到一个类似文件的东西(毕竟被称为 File ),但是这个方法的存在仅仅是为了向后兼容。虽然看上去应该被称为"路径",实际上却应该表示目录或者文件本身。这是个非常草率并且令人困惑的命名,但是由于 java.nio.file 的存在我们可以安全地忽略它的存在。
选取路径部分片段
Path 对象可以非常容易地生成路径的某一部分:
可以通过 getName() 来索引 Path 的各个部分,直到达到上限 getNameCount()。Path 也实现了 Iterable 接口,因此我们也可以通过增强的 for-each 进行遍历。请注意,即使路径以 .java 结尾,使用 endsWith() 方法也会返回 false。这是因为使用 endsWith() 比较的是整个路径部分,而不会包含文件路径的后缀。通过使用 startsWith() 和 endsWith() 也可以完成路径的遍历。但是我们可以看到,遍历 Path 对象并不包含根路径,只有使用 startsWith() 检测根路径时才会返回 true。
路径分析
Files 工具类包含一系列完整的方法用于获得 Path 相关的信息。
在调用最后一个测试方法 getPosixFilePermissions() 之前我们需要确认一下当前文件系统是否支持 Posix 接口,否则会抛出运行时异常。
Paths的增减修改
我们必须能通过对 Path 对象增加或者删除一部分来构造一个新的 Path 对象。我们使用 relativize() 移除 Path 的根路径,使用 resolve() 添加 Path 的尾路径(不一定是“可发现”的名称)。
对于下面代码中的示例,我使用 relativize() 方法从所有的输出中移除根路径,部分原因是为了示范,部分原因是为了简化输出结果,这说明你可以使用该方法将绝对路径转为相对路径。
这个版本的代码中包含 id,以便于跟踪输出结果:
文件查找
粗糙的方法,在 path 上调用 toString(),然后使用 string 操作查看结果。
java.nio.file 有更好的解决方案:通过在 FileSystem 对象上调用 getPathMatcher() 获得一个 PathMatcher,然后传入感兴趣的模式。
模式
模式
glob
glob 比较简单,实际上功能非常强大,因此可以使用 glob 解决许多问题。
在 matcher 中,glob 表达式开头的 **/ 表示“当前目录及所有子目录”,这在当你不仅仅要匹配当前目录下特定结尾的 Path 时非常有用。
单 * 表示“任何东西”,然后是一个点,然后大括号表示一系列的可能性—我们正在寻找以 .tmp 或 .txt 结尾的东西
regex
如果问题更复杂,可以使用 regex
文件读写
如果一个文件很“小”,即“它运行得足够快且占用内存小”,那java.nio.file.Files 类中的实用程序将帮助你轻松读写文本和二进制文件。
Files.readAllLines()一次读取整个文件(因此,“小”文件很有必要),产生一个List- 只需将
Path传递给 readAllLines() readAllLines()有一个重载版本,包含一个Charset参数来存储文件的 Unicode 编码Files.write()被重载以写入byte数组或任何Iterable对象(它也有Charset选项):
ClassReader cr = new ClassReader("com/javaedge/asm/TestAsm"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new MyClassVisitor(cw); cr.accept(cv, ClassReader.SKIP_DEBUG); byte[] data = cw.toByteArray(); Files.write(Paths.get("src/main/java/com/javaedge/asm/out.class"), data); System.out.println("generate success!");
如果文件大小有问题怎么办? 比如说:
- 文件太大,如果你一次性读完整个文件,你可能会耗尽内存。
- 您只需要在文件的中途工作以获得所需的结果,因此读取整个文件会浪费时间。
Files.lines() 方便地将文件转换为行的 Stream:
流式处理,跳过 13 行,然后选择下一行并将其打印出来。
Files.lines() 对于把文件处理行的传入流时非常有用,但是如果你想在 Stream 中读取,处理或写入怎么办?这就需要稍微复杂的代码:
因为我们在同一个块中执行所有操作,所以这两个文件都可以在相同的 try-with-resources 语句中打开。
PrintWriter 是一个旧式的 java.io 类,允许你“打印”到一个文件,所以它是这个应用的理想选择
总结
虽然本章对文件和目录操作做了相当全面的介绍,但是仍然有没被介绍的类库中的功能——一定要研究 java.nio.file 的 Javadocs,尤其是 java.nio.file.Files 这个类。
Java 7 和 8 对于处理文件和目录的类库做了大量改进。如果您刚刚开始使用 Java,那么您很幸运。在过去,它令人非常不愉快,Java 设计者以前对于文件操作不够重视才没做简化。对于初学者来说这是一件很棒的事,对于教学者来说也一样。我不明白为什么花了这么长时间来解决这个明显的问题,但不管怎么说它被解决了,我很高兴。使用文件现在很简单,甚至很有趣,这是你以前永远想不到的。