问题
为了测试您的 Java I/O 编程能力,请看下面的问题。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
创建文件路径:写几个创建几种文件路径的例子(如绝对路径、相对路径等)。
转换文件路径:写几个转换文件路径的例子(例如,将文件路径转换成字符串、URI、文件等)。
连接文件路径:写几个连接(组合)文件路径的例子。定义一个固定路径并向其附加其他不同的路径(或用其他路径替换其中的一部分)。
在两个位置之间构造路径:写出几个例子,在两个给定路径之间(从一条路径到另一条路径)之间构造相对路径。
比较文件路径:写几个比较给定文件路径的例子。
遍历路径:编写一个程序,访问一个目录下的所有文件,包括子目录。此外,编写一个程序,按名称搜索文件、删除目录、移动目录和复制目录。
监视路径:编写多个程序,监视某条路径上发生的变化(如创建、删除、修改)。
“流式传输文件内容”:编写一个流式传输给定文件内容的程序。
在文件树中搜索文件/文件夹:编写一个程序,在给定的文件树中搜索给定的文件/文件夹。
“高效读写文本文件”:编写几个程序,举例说明高效读写文本文件的不同方法。
高效读写二进制文件:编写几个程序,举例说明高效读写二进制文件的不同方法。
在大文件中搜索:编写一个程序,在大文件中高效地搜索给定的字符串。
将 JSON/CSV 文件作为对象读取:编写一个程序,将给定的 JSON/CSV 文件作为对象读取(POJO)。
使用临时文件/文件夹:编写几个使用临时文件/文件夹的程序。
过滤文件:为文件编写多个自定义过滤器。
发现两个文件之间的不匹配:编写一个程序,在字节级发现两个文件之间的不匹配。
循环字节缓冲区:编写一个表示循环字节缓冲区实现的程序。
分词文件:写几个代码片段来举例说明分词文件内容的不同技术。
将格式化输出直接写入文件:编写一个程序,将给定的数字(整数和双精度)格式化并输出到文件中。
使用Scanner:写几个代码片段来展示Scanner的功能。
解决方案
以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释只包括解决问题所需的最有趣和最重要的细节。您可以从这个页面下载示例解决方案以查看更多详细信息并尝试程序。
129 创建文件路径
从 JDK7 开始,我们可以通过 NIO.2API 创建一个文件路径。更准确地说,可以通过Path
和Paths
API 轻松定义文件路径。
Path
类是文件系统中路径的编程表示。路径字符串包含以下信息:
文件名
目录列表
依赖于操作系统的文件分隔符(例如,在 Solaris 和 Linux 上为正斜杠/,在 Microsoft Windows 上为反斜杠\
其他允许的字符,例如,.(当前目录)和..(父目录)符号
Path类处理不同文件系统(FileSystem)中的文件,这些文件系统可以使用不同的存储位置(FileStore是底层存储)。
定义Path的一个常见解决方案是调用Paths辅助类的get()方法之一。另一种解决方案依赖于FileSystems.getDefault().getPath()方法。
Path驻留在文件系统中—文件系统存储和组织文件或某种形式的媒体,通常在一个或多个硬盘驱动器上,以便于检索。文件系统可以通过java.nio.file.FileSystems的final类获取,用于获取java.nio.file.FileSystem的实例。JVM 的默认FileSystem(俗称操作系统的默认文件系统)可以通过FileSystems().getDefault()方法获得。一旦我们知道文件系统和文件(或目录/文件夹)的位置,我们就可以为它创建一个Path对象。
另一种方法包括从统一资源标识符(URI)创建Path。Java 通过URI类包装一个URI,然后我们可以通过URI.create(String uri)方法从一个String获得一个URI。此外,Paths类提供了一个get()方法,该方法将URI对象作为参数并返回相应的Path。
从 JDK11 开始,我们可以通过两个方法创建一个Path。其中一个将URI转换为Path,而另一个将路径字符串或字符串序列转换为路径字符串。
在接下来的部分中,我们将了解创建路径的各种方法。
创建相对于文件存储根目录的路径
相对于当前文件存储根目录的路径(例如,C:/
)必须以文件分隔符开头。在下面的例子中,如果当前文件存储根为C
,则绝对路径为C:\learning\packt\JavaModernChallenge.pdf
:
Path path = Paths.get("/learning/packt/JavaModernChallenge.pdf"); Path path = Paths.get("/learning", "packt/JavaModernChallenge.pdf"); Path path = Path.of("/learning/packt/JavaModernChallenge.pdf"); Path path = Path.of("/learning", "packt/JavaModernChallenge.pdf"); Path path = FileSystems.getDefault() .getPath("/learning/packt", "JavaModernChallenge.pdf"); Path path = FileSystems.getDefault() .getPath("/learning/packt/JavaModernChallenge.pdf"); Path path = Paths.get( URI.create("file:///learning/packt/JavaModernChallenge.pdf")); Path path = Path.of( URI.create("file:///learning/packt/JavaModernChallenge.pdf"));
创建相对于当前文件夹的路径
当我们创建一个相对于当前工作文件夹的路径时,路径不应该以文件分隔符开头。如果当前文件夹名为books
并且在C
根目录下,那么下面代码段返回的绝对路径将是C:\books\learning\packt\JavaModernChallenge.pdf
:
Path path = Paths.get("learning/packt/JavaModernChallenge.pdf"); Path path = Paths.get("learning", "packt/JavaModernChallenge.pdf"); Path path = Path.of("learning/packt/JavaModernChallenge.pdf"); Path path = Path.of("learning", "packt/JavaModernChallenge.pdf"); Path path = FileSystems.getDefault() .getPath("learning/packt", "JavaModernChallenge.pdf"); Path path = FileSystems.getDefault() .getPath("learning/packt/JavaModernChallenge.pdf");
创建绝对路径
创建绝对路径可以通过显式指定根目录和包含文件或文件夹的所有其他子目录来完成,如以下示例(C:\learning\packt\JavaModernChallenge.pdf
)所示:
Path path = Paths.get("C:/learning/packt", "JavaModernChallenge.pdf"); Path path = Paths.get( "C:", "learning/packt", "JavaModernChallenge.pdf"); Path path = Paths.get( "C:", "learning", "packt", "JavaModernChallenge.pdf"); Path path = Paths.get("C:/learning/packt/JavaModernChallenge.pdf"); Path path = Paths.get( System.getProperty("user.home"), "downloads", "chess.exe"); Path path = Path.of( "C:", "learning/packt", "JavaModernChallenge.pdf"); Path path = Path.of( System.getProperty("user.home"), "downloads", "chess.exe"); Path path = Paths.get(URI.create( "file:///C:/learning/packt/JavaModernChallenge.pdf")); Path path = Path.of(URI.create( "file:///C:/learning/packt/JavaModernChallenge.pdf"));
使用快捷方式创建路径
我们理解快捷方式是.
(当前目录)和..
(父目录)符号。这种路径可以通过normalize()
方法进行归一化。此方法消除了冗余,例如.
和directory/..
:
Path path = Paths.get( "C:/learning/packt/chapters/../JavaModernChallenge.pdf") .normalize(); Path path = Paths.get( "C:/learning/./packt/chapters/../JavaModernChallenge.pdf") .normalize(); Path path = FileSystems.getDefault() .getPath("/learning/./packt", "JavaModernChallenge.pdf") .normalize(); Path path = Path.of( "C:/learning/packt/chapters/../JavaModernChallenge.pdf") .normalize(); Path path = Path.of( "C:/learning/./packt/chapters/../JavaModernChallenge.pdf") .normalize();
如果不规范化,路径的冗余部分将不会被删除。
为了创建与当前操作系统 100% 兼容的路径,我们可以依赖于FileSystems.getDefault().getPath(),或者是File.separator(依赖于系统的默认名称分隔符)和File.listRoots()(可用的文件系统根)的组合。
对于相对路径,我们可以依赖以下示例:
private static final String FILE_SEPARATOR = File.separator;
或者,我们可以依赖getSeparator()
:
private static final String FILE_SEPARATOR = FileSystems.getDefault().getSeparator(); // relative to current working folder Path path = Paths.get("learning", "packt", "JavaModernChallenge.pdf"); Path path = Path.of("learning", "packt", "JavaModernChallenge.pdf"); Path path = Paths.get(String.join(FILE_SEPARATOR, "learning", "packt", "JavaModernChallenge.pdf")); Path path = Path.of(String.join(FILE_SEPARATOR, "learning", "packt", "JavaModernChallenge.pdf")); // relative to the file store root Path path = Paths.get(FILE_SEPARATOR + "learning", "packt", "JavaModernChallenge.pdf"); Path path = Path.of(FILE_SEPARATOR + "learning", "packt", "JavaModernChallenge.pdf");
我们也可以对绝对路径执行相同的操作:
Path path = Paths.get(File.listRoots()[0] + "learning", "packt", "JavaModernChallenge.pdf"); Path path = Path.of(File.listRoots()[0] + "learning", "packt", "JavaModernChallenge.pdf");
根目录列表也可以通过FileSystems
获得:
FileSystems.getDefault().getRootDirectories()
130 转换文件路径
将文件路径转换为String
、URI
、File
等是一项常见任务,可以在广泛的应用中发生。让我们考虑以下文件路径:
Path path = Paths.get("/learning/packt", "JavaModernChallenge.pdf");
现在,基于 JDK7 和 NIO.2 API,让我们看看如何将一个Path转换成一个String、一个URI、一个绝对路径、一个实路径和一个文件:
将Path转换为String非常简单,只需(显式地或自动地)调用Path.toString()方法。注意,如果路径是通过FileSystem.getPath()方法获得的,那么toString()返回的路径字符串可能与用于创建路径的初始String不同:
// \learning\packt\JavaModernChallenge.pdf String pathToString = path.toString();
可以通过Path.toURI()方法将Path转换为URI(浏览器格式)。返回的URI包装了一个可在 Web 浏览器地址栏中使用的路径字符串:
// file:///D:/learning/packt/JavaModernChallenge.pdf URI pathToURI = path.toUri();
假设我们想要将URI
/URL
中的文件名提取为Path
(这是常见的场景)。在这种情况下,我们可以依赖以下代码片段:
// JavaModernChallenge.pdf URI uri = URI.create( "https://www.learning.com/packt/JavaModernChallenge.pdf"); Path URIToPath = Paths.get(uri.getPath()).getFileName(); // JavaModernChallenge.pdf URL url = new URL( "https://www.learning.com/packt/JavaModernChallenge.pdf"); Path URLToPath = Paths.get(url.getPath()).getFileName();
路径转换可按以下方式进行:
- 可以通过
Path.toAbsolutePath()
方法将相对Path
转换为绝对Path
。如果Path
已经是绝对值,则返回相同的结果:
// D:\learning\packt\JavaModernChallenge.pdf Path pathToAbsolutePath = path.toAbsolutePath();
通过Path.toRealPath()方法可以将Path转换为实际Path,其结果取决于实现。如果所指向的文件不存在,则此方法将抛出一个IOException。但是,根据经验,调用此方法的结果是没有冗余元素的绝对路径(标准化)。此方法获取一个参数,该参数指示应如何处理符号链接。默认情况下,如果文件系统支持符号链接,则此方法将尝试解析它们。如果您想忽略符号链接,只需将LinkOption.NOFOLLOW_LINKS常量传递给方法即可。此外,路径名元素将表示目录和文件的实际名称。
例如,让我们考虑下面的Path和调用此方法的结果(注意,我们故意添加了几个冗余元素并将PACKT文件夹大写):
Path path = Paths.get( "/learning/books/../PACKT/./", "JavaModernChallenge.pdf"); // D:\learning\packt\JavaModernChallenge.pdf Path realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
- 可通过
Path.toFile()
方法将Path
转换为文件。将一个文件转换成一个Path
,我们可以依赖File.toPath()
方法:
File pathToFile = path.toFile(); Path fileToPath = pathToFile.toPath();
131 连接文件路径
连接(或组合)文件路径意味着定义一个固定的根路径,并附加一个部分路径或替换其中的一部分(例如,一个文件名需要替换为另一个文件名)。基本上,当我们想要创建共享公共固定部分的新路径时,这是一种方便的技术。
这可以通过 NIO.2 和Path.resolve()
和Path.resolveSibling()
方法来实现。
让我们考虑以下固定根路径:
Path base = Paths.get("D:/learning/packt");
我们还假设我们想要得到两本不同书籍的Path
:
// D:\learning\packt\JBossTools3.pdf Path path = base.resolve("JBossTools3.pdf"); // D:\learning\packt\MasteringJSF22.pdf Path path = base.resolve("MasteringJSF22.pdf");
我们可以使用此函数循环一组文件;例如,让我们循环一String[]
本书:
Path basePath = Paths.get("D:/learning/packt"); String[] books = { "Book1.pdf", "Book2.pdf", "Book3.pdf" }; for (String book: books) { Path nextBook = basePath.resolve(book); System.out.println(nextBook); }
有时,固定根路径也包含文件名:
Path base = Paths.get("D:/learning/packt/JavaModernChallenge.pdf");
这一次,我们可以通过resolveSibling()
方法将文件名(JavaModernChallenge.pdf
替换为另一个名称。此方法根据此路径的父路径解析给定路径,如下例所示:
// D:\learning\packt\MasteringJSF22.pdf Path path = base.resolveSibling("MasteringJSF22.pdf");
如果我们将Path.getParent()
方法引入讨论,并将resolve()
和resolveSibling()
方法链接起来,那么我们可以创建更复杂的路径,如下例所示:
// D:\learning\publisher\MyBook.pdf Path path = base.getParent().resolveSibling("publisher") .resolve("MyBook.pdf");
resolve()
/resolveSibling()
方法分为两种,分别是resolve(String other)
/resolveSibling(String other)
和resolve(Path other)
/resolveSibling(Path other)
。
132 在两个位置之间构建路径
在两个位置之间构建相对路径是Path.relativize()
方法的工作。
基本上,得到的相对路径(由Path.relativize()
返回)从一条路径开始,在另一条路径上结束。这是一个强大的功能,它允许我们使用相对路径在不同的位置之间导航,相对路径是根据前面的路径解析的。
让我们考虑以下两种途径:
Path path1 = Paths.get("JBossTools3.pdf"); Path path2 = Paths.get("JavaModernChallenge.pdf");
注意,JBossTools3.pdf
和JavaModernChallenge.pdf
是兄弟姐妹。这意味着我们可以通过上一级然后下一级从一个导航到另一个。以下示例也揭示了这种导航情况:
// ..\JavaModernChallenge.pdf Path path1ToPath2 = path1.relativize(path2); // ..\JBossTools3.pdf Path path2ToPath1 = path2.relativize(path1);
另一种常见情况涉及公共根元素:
Path path3 = Paths.get("/learning/packt/2003/JBossTools3.pdf"); Path path4 = Paths.get("/learning/packt/2019");
所以,path3
和path4
共享相同的根元素/learning
。从path3
到path4
需要上两层下一层。另外,从path4
到path3
的航行,需要上一级,下两级。查看以下代码:
// ..\..\2019 Path path3ToPath4 = path3.relativize(path4); // ..\2003\JBossTools3.pdf Path path4ToPath3 = path4.relativize(path3);
两个路径都必须包含根元素。完成这个需求并不能保证成功,因为相对路径的构建依赖于实现。
133 比较文件路径
根据我们如何看待两个文件路径之间的相等性,有几种解决方案。主要来说,平等性可以通过不同的方式为不同的目标进行验证。
假设我们有以下三种路径(考虑在您的计算机上复制C:\learning\packt\JavaModernChallenge.pdf
:
Path path1 = Paths.get("/learning/packt/JavaModernChallenge.pdf"); Path path2 = Paths.get("/LEARNING/PACKT/JavaModernChallenge.pdf"); Path path3 = Paths.get("D:/learning/packt/JavaModernChallenge.pdf");
在下面的部分中,我们将研究用于比较文件路径的不同方法。
Path.equals()
path1
等于path2
吗?或者,path2
等于path3
吗?好吧,如果我们通过Path.equals()
进行这些测试,那么可能的结果将显示path1
等于path2
,但path2
不等于path3
:
boolean path1EqualsPath2 = path1.equals(path2); // true boolean path2EqualsPath3 = path2.equals(path3); // false
Path.equals()
方法遵循Object.equals()
规范。虽然此方法不访问文件系统,但相等性取决于文件系统实现。例如,一些文件系统实现可能会以区分大小写的方式比较路径,而其他文件系统实现可能会忽略大小写。
表示相同文件/文件夹的路径
然而,这可能不是我们想要的那种比较。如果两条路径是相同的文件或文件夹,那么说它们相等就更有意义了。这可以通过Files.isSameFile()
方法来实现。此方法分为两个步骤:
首先,它调用Path.equals(),如果此方法返回true,则路径相等,无需进一步操作。
其次,如果Path.equals()返回false,则检查两个路径是否代表同一个文件/文件夹(根据实现,此操作可能需要打开/访问两个文件,因此文件必须存在,以避免出现IOException)。
//true boolean path1IsSameFilePath2 = Files.isSameFile(path1, path2); //true boolean path1IsSameFilePath3 = Files.isSameFile(path1, path3); //true boolean path2IsSameFilePath3 = Files.isSameFile(path2, path3);
词典比较
如果我们只需要对路径进行词典比较,那么我们可以依赖于Path.compareTo()
方法(这对于排序很有用)。
此方法返回以下信息:
- 如果路径相等,则为 0
- 如果第一条路径在词典上小于参数路径,则该值小于零
- 如果第一条路径在词典中大于参数路径,则该值大于零:
int path1compareToPath2 = path1.compareTo(path2); // 0 int path1compareToPath3 = path1.compareTo(path3); // 24 int path2compareToPath3 = path2.compareTo(path3); // 24
请注意,您可能会获得与上一示例不同的值。此外,在您的业务逻辑中,重要的是依赖于它们的含义,而不是依赖于它们的值(例如,说if(path1compareToPath3 > 0) { ... }
,避免使用if(path1compareToPath3 == 24) { ... }
)。
部分比较
部分比较可通过Path.startsWith()
和Path.endsWith()
方法实现。使用这些方法,我们可以测试当前路径是否以给定路径开始/结束:
boolean sw = path1.startsWith("/learning/packt"); // true boolean ew = path1.endsWith("JavaModernChallenge.pdf"); // true