Java 的 Path.endsWith(String) 方法很容易用错

用 IDE 写了一段代码,目的是遍历某一目录下的所有文件,并对所有以 .xml 结尾的文件进行处理,代码如下:

1try (Stream<Path> pathStream = Files.list(Paths.get("data"))) {
2    pathStream.filter(path -> path.endsWith(".xml))
3              .forEach(System.out::println); // do something with the file 
4}

看起来很直截了当,filter 中对 path 用 endsWith(String) 方法过滤出所有以 .xml 结尾的文件,然而在目录中存在 *.xml 文件的情况下却没有任何输出。假如测试时未该发现问题,在运行期大有可能当成是该目录中未找到任何以 .xml 结尾的文件而怀疑是数据问题。

这样的代码即使是让人工进行 review 也未必能发现有什么问题,遍历 data 目录下的文件,如果文件以 .xml 结尾就进行处理,很自然,正确啊。

后来经过调试发现 filter 中 path.endsWith(".xml") 永远返回 false,产生这个 Bug 的主要原因是

  • 写代码时看到 Path.endsWith(String) 会想当然的主为会采用 path.toString().endsWith(String) 以字符串的方式来判断

如果进一步验证的话就会发现

1Path.of("a/bc/hello.txt").endsWith(".txt"); // false
2Path.of("a/bc/hello.txt").toString().endsWith(".txt"); // true
3Path.of("a/bc/hello.txt").endsWith("hello.txt"); // true
4Path.of("hello.txt").endsWith(".txt"); // false
5Path.of("a/bc/hello.txt").endsWith("c/hello.txt"); // false
6Path.of("a/bc/hello.txt").endsWith("bc/hello.txt"); // true
7Path.of("a/bc/hello.txt").endsWith("/bc/hello.txt"); // false

从以上不同情景下的输出可意识到 Path.endsWith(String) 是判断文件路径的最后一至多段路径是否与参数字符串相等,而不是判断文件名是否以字符串结尾。

如果跟踪该方法调用

1Path.endsWith(String other) {
2    return endsWith(getFileSystem().getPath(other));
3}

调用的是 Path 中另一个方法

1Path.endsWith(Path other);

JDK 在同时在 Path 中提供两个 endsWith 重载方法

1boolean endsWith(String other);
2boolean endsWith(Path other);

endsWith(String) 极易造成误用为路径字符串的比较,如果只有 endsWith(Path) 方法就不会有这个问题, 因为显然不大可能会写出如下代码

1path.endsWith(Path.of(".xml"));

JDK 引入 Path.endsWith(String) 这个方法确实能直接使用字符串作为参数,而不用时时显式的构建 Path 对象而有所便利, 但同时更带来了方法误用的风险,这种方法便利就得不偿失了, 还不如只提供 endsWith(Path) 方法更为合理些。

最后,前面问题代码应该修正为

1try (Stream<Path> pathStream = Files.list(Paths.get("data"))) {
2    pathStream.filter(path -> path.toString().endsWith(".xml"))
3              .forEach(System.out::println); // do something with the file 
4}

真是走个捷径,结果却是误入了歧途。

永久链接 https://yanbin.blog/java-path-endswith-string-error-prone/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。