bigywater.tech
Java · 354

PathMatcher:文件匹配模式的边界

glob 看起来像 shell 通配符,但 PathMatcher 的规则有不少细节:匹配的是整个路径、* 不跨目录、** 与 ** / 的差别、大小写、取反写法。这篇用一张匹配矩阵与几个目录实例逐项验证。

· vs ✓
**/*.java 对顶层的 Main.java 为假(需要 {*.java,**/*.java} 或 **.java)
[^A-M]
在 glob 里不是取反(匹配 Main.java),取反要写 [!A-M]
0 个
walk 里用「绝对路径」去匹配 *.java 的结果(要么用 getFileName(),要么用 **.java)

glob × 路径矩阵:星号、取反与字符类

实测 · 本机执行输出
  [matrix]
                               P1  | P2  | P3  | P4  | P5  | P6  | P7  | P8  | P9  | P10 | P11 | P12
        P1  = Main.java
        P2  = src/Main.java
        P3  = src/a/b/Main.java
        P4  = /abs/src/Main.java
        P5  = ./Main.java
        P6  = src/../Main.java
        P7  = .hidden.java
        P8  = main.java
        P9  = Nain.java
        P10 = src
        P11 = a.class
        P12 = Main.java/
        *.java                 ✓ | · | · | · | · | · | ✓ | ✓ | ✓ | · | · | ✓ |
        **.java                ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | ✓ |
        **/*.java              · | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | · | · | · | · |
        {*.java,**/*.java}     ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | · | · | ✓ |
        src/*.java             · | ✓ | · | · | · | · | · | · | · | · | · | · |
        src/**.java            · | ✓ | ✓ | · | · | ✓ | · | · | · | · | · | · |
        src/**/*.java          · | · | ✓ | · | · | ✓ | · | · | · | · | · | · |
        src/**/Main.java       · | · | ✓ | · | · | ✓ | · | · | · | · | · | · |
        *.{java,class}         ✓ | · | · | · | · | · | ✓ | ✓ | ✓ | · | ✓ | ✓ |
        ?ain.java              ✓ | · | · | · | · | · | · | ✓ | ✓ | · | · | ✓ |
        [A-M]ain.java          ✓ | · | · | · | · | · | · | · | · | · | · | ✓ |
        [!A-M]ain.java         · | · | · | · | · | · | · | ✓ | ✓ | · | · | · |
        [^A-M]ain.java         ✓ | · | · | · | · | · | · | · | · | · | · | ✓ |
        Main.java              ✓ | · | · | · | · | · | · | · | · | · | · | ✓ |
        src/**                 · | ✓ | ✓ | · | · | ✓ | · | · | · | · | · | · |
        **                     ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
        *                      ✓ | · | · | · | · | · | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
        */*                    · | ✓ | · | · | ✓ | · | · | · | · | · | · | · |
        .*                     · | · | · | · | · | · | ✓ | · | · | · | · | · |
        *.java*                ✓ | · | · | · | · | · | ✓ | ✓ | ✓ | · | · | ✓ |
        [a-z]*                 · | · | · | · | · | · | · | ✓ | · | ✓ | ✓ | · |
    (列依次是 P1..P12;✓ 表示 matches(Path) 为 true;Paths.get("Main.java/") 的 toString 是 Main.java,所以 P12 与 P1 结果相同)

大小写

实测 · 本机执行输出
  [case]
    FileSystem:MacOSXFileSystem;separator = /
        glob:*.JAVA         匹配 a.java   → false
        glob:*.java         匹配 A.JAVA   → false
        glob:[A-Z].java     匹配 a.java   → false
        glob:{A,b}.java     匹配 a.java   → false
        glob:main.java      匹配 Main.java → false
    文件系统本身是否区分大小写(Files.exists(hello.txt),实际文件是 Hello.txt):true;isSameFile 两者 = true
    newDirectoryStream(dir, "hello.*") 返回 [](glob 区分大小写,即使文件系统不区分)
    newDirectoryStream(dir, "Hello.*") 返回 [Hello.txt]
    regex 想忽略大小写:regex:(?i).*\.JAVA → true

转义与分隔符

实测 · 本机执行输出
  [escape]
        glob:\*.java      匹配 *.java    → true
        glob:\*.java      匹配 a.java    → false
        glob:a\?b         匹配 a?b       → true
        glob:a\?b         匹配 axb       → false
        glob:\{a\}        匹配 {a}       → true
        glob:[*]          匹配 *         → true
        glob:[?]x         匹配 ?x        → true
        glob:[{]          匹配 {         → true
        glob:a/b          匹配 a/b       → true
        glob:*            匹配 a/b       → false
        glob:?            匹配 /         → false
        glob:a?b          匹配 a/b       → false
    regex 匹配的是 Path.toString() 的整串:regex:.*\.java 对 src/Main.java → true;regex:src/.*  对 src/a/b.java → true;regex:\.java 对 Main.java(不是整串)→ false
    Path.toString 的归一化:Paths.get("src//a///b.java") = src/a/b.java;Paths.get("src/a/") = src/a;g("src/*").matches(Paths.get("src//a")) = true

非法 pattern

实测 · 本机执行输出
  [errors]
        "glob:{a,b"        → PatternSyntaxException: Missing '} near index 3 ⏎ {a,b ⏎
        "glob:[abc"        → PatternSyntaxException: Missing '] near index 3 ⏎ [abc ⏎
        "glob:{a,{b,c}}"   → PatternSyntaxException: Cannot nest groups near index 3 ⏎ {a,{b,c}} ⏎
        "glob:[]"          → PatternSyntaxException: Unclosed character class near index 11 ⏎ ^[[^/]&&[]]$ ⏎
        "glob:[z-a]"       → PatternSyntaxException: Invalid range near index 1 ⏎ [z-a]
        "glob:a\"          → PatternSyntaxException: No character to escape near index 1 ⏎ a\
        "glob:{}"          → 创建成功;对 "a" = false,对 "ab" = false
        "glob:{,a}"        → 创建成功;对 "a" = true,对 "ab" = false
        "glob:***"         → 创建成功;对 "a" = true,对 "ab" = true
        "glob:"            → 创建成功;对 "a" = false,对 "ab" = false
        "regex:("          → PatternSyntaxException: Unclosed group near index 1 ⏎
        "regex:"           → 创建成功;对 "a" = false,对 "ab" = false
        "foo:bar"          → UnsupportedOperationException: Syntax 'foo' not recognized
        "glob"             → IllegalArgumentException: null
        "GLOB:*.java"      → 创建成功;对 "a" = false,对 "ab" = false
        "glob:[!]a"        → PatternSyntaxException: Unclosed character class near index 13 ⏎ ^[[^/]&&[^]]a$ ⏎
        "glob:a[/]b"       → PatternSyntaxException: Explicit 'name separator' in class near index 2 ⏎ a[/]b ⏎

目录中的用法

实测 · 本机执行输出
  [stream]
    newDirectoryStream(root, "*.java") = [.d.java, a.java](只有一层;.d.java 也在内——隐藏文件不特殊)
    newDirectoryStream(root, "**.java") = [.d.java, a.java](流里每个条目的文件名与 glob 比较,不会进入子目录)
    newDirectoryStream(root, "{a,b}.*") = [a.java, b.class]
    newDirectoryStream(root, "*/") 条目数 0
    walk + glob:**/*.java              用「相对路径」匹配 → [sub/deep/g.java, sub/e.java]
    walk + glob:{*.java,**/*.java}     用「相对路径」匹配 → [.d.java, a.java, sub/deep/g.java, sub/e.java]
    walk + glob:**.java                用「相对路径」匹配 → [.d.java, a.java, sub/deep/g.java, sub/e.java]
    walk + glob:*.java                 用「相对路径」匹配 → [.d.java, a.java]
    walk + glob:**.java 用「绝对路径」匹配:[.d.java, a.java, sub/deep/g.java, sub/e.java](** 可以吞掉绝对路径的前缀目录)
    walk + glob:*.java 用绝对路径匹配:0 个
      用 path.getFileName() 匹配 *.java:[.d.java, a.java, sub/deep/g.java, sub/e.java]

分步演示

PathMatcher
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,macOS(MacOSXFileSystem,路径分隔符 /,文件系统不区分大小写)
    • 没有验证:Windows 上的 glob(分隔符 \ 与不区分大小写)、Jar / Zip 文件系统与 Jimfs 里的 PathMatcher、JDK 17 / 27 的差异、非 ASCII 文件名与 Unicode 归一化(macOS 会把文件名转成分解形式)的匹配、性能
    京ICP备2026067502号-1京公网安备11010502063487号