java.nio.file.Path の分かりにくいメソッドについて


はじめに

ずいぶん昔、Java 1.7 で java.nio.file.Path が導入されました。

Path はファイルやディレクトリを表現するのではなく、あくまでもファイルシステム上のファイルパスの抽象です。

Path path = FileSystems.getDefault().getPath("logs", "access.log");

または

Path path = Paths.get("logs", "access.log");

または

Path path = Paths.get("logs/access.log");

Java 11 以降からは、

Path path = Path.of("logs", "access.log");

のように Path を取得すると、logs/access.log のようなパスを得ることができます(パスセパレータはOS依存です)。

ファイルパスの抽象から、実際のファイルやディレクトリには以下のよな相互変換が可能となっています。

File file = path.toFile();
Path path = file.toPath();

さて、この Path には、いくつか分かりにくいメソッドがあるのでここにメモしておきます。


startsWith() と endsWith()

指定されたパスで始まるかどうかをテストする startsWith() と、指定されたパスで終わるかどうかをテストする endsWith() は、勘違いすることが多いメソッドです。

パスは文字列ではなく、パスの階層自体が指定パスと一致するかを判定します。 そのため、パス階層文字列の、部分には一致せず、拡張子で判定するなどは機能しません。

Path path = Path.of("logs/access.log");

path.startsWith("log");      // false
path.endsWith(".log");       // false

path.startsWith("logs");     // true
path.endsWith("access.log"); // true


拡張子を判定するには、以下のように文字列として判定するか、

path.toString().endsWith(".log");

PathMatcher を使って判定することとなります。

PathMatcher pm = FileSystems.getDefault().getPathMatcher("glob:*.log");
pm.matches(path)


normalize()

normalize() はパス中に含まれる .(現在のディレクトリ) および ..(親ディレクトリ) という名前を除去することで正規化します。

Path dir = Path.of("./a/b/../c");
dir.normalize(); // a/c

実際のファイルシステムにアクセスする訳ではないので、以下のようなケースでは、..\.. のようなパスとして解決されます。

Path.of("./a/b/../../../../").normalize(); // ../..


resolve()

resolve() は、基本的にはパスの連結操作を行います。

Path path1 = Path.of("a/b");
Path path2 = Path.of("c/d");

path1.resolve(path2);  // a/b/c/d

以下のようにして、起点となる dir の親を指定したりすることもできます。

Path dir = Path.of("a", "b" "c");
Path log = Path.of("..", "access.log");

dir.resolve(log).normalize();  // a/b/access.log

挙動が変わるのは、ルートを含むパス同士の場合で、以下のような結果となります。

Path.of( "a/b").resolve(Path.of( "c/d"));  // a/b/c/d
Path.of("/a/b").resolve(Path.of( "c/d"));  // /a/b/c/d
Path.of( "a/b").resolve(Path.of("/c/d"));  // /c/d
Path.of("/a/b").resolve(Path.of("/c/d"));  // /c/d

resolve() の引数に渡すパスがルートからの場合、そちらが優先される動きになります。


同様なものに resolveSibling() があり、こちらは親パスに対しての連結操作を行います。

前述の例で言えば、以下のようになります。

Path dir = Path.of("a", "b", "c");
Path log = Path.of("..", "access.log");

dir.resolveSibling(log).normalize();  // a/access.log

resolveSibling() は以下の操作と同等となります。

dir.getParent().resolve(log);


relativize()

API のドキュメントを読むと、なんのこっちゃという感じで有名な relativize() です。

relativize() では、相対パスを得ることができます。

Path dir = Path.of("/a/b/");
Path log = Path.of("/a/b/c/access.log");

dir.relativize(log); // c/access.log

起点となるディレクトリ(dir)から見た、特定のディレクトリやファイル(log)の相対パスを得るものです。

以下も同様ですね。

Path dir = Path.of("/a/b/c");
Path log = Path.of("/a/b/access.log");

dir.relativize(log); // ../access.log


Zip ファイルシステムプロバイダ

おまけです。

Path は以下のように FileSystems.getDefault() で取得した FileSystem を経由して取得できることは先に述べました。

Path path = FileSystems.getDefault().getPath("logs");

FileSystems.newFileSystem() により Zip ファイル、または JAR ファイルを 1 つのファイルシステムとして扱い、そのファイルの内容を操作するファイルシステムを得ることができます。

Zip ファイルシステムプロバイダは以下のように取得することができます。

URI uri = URI.create("jar:file:/zipfs/foo.zip");
FileSystem fs = FileSystems.newFileSystem(uri, env);

または Path を指定して以下のようにすることもできます。

Path zipfile = Paths.get("/zipfs/foo.zip");
FileSystem fs = FileSystems.newFileSystem(zipfile, null);

後の例に示すように、URI にスキーム指定した方がわかり易いかもしれません。

FileSystems.newFileSystem(uri, env) で指定する env には、以下のような Zip ファイルシステムのプロパティー を渡すことができます。

Map<String, String> env = Map.of("create", "true", "encoding", "UTF-8");

create には、 Zip ファイルが存在しない場合に新規作成するかどうか(デフォルトは false)、encodingには、 Zip または JAR ファイル内のエントリの名前に使用されるエンコードスキームを指定します(デフォルトは UTF-8)。

以下のように使うことができます。

Path zip = Path.of("example.jar");
URI uri = new URI("jar", zip.toUri().toString(),  null);

try (FileSystem zipFs = FileSystems.newFileSystem(
        uri, Map.of("create", "true"))) {

    Path source =  Path.of("INDEX.LIST");

    Files.createDirectories(zipFs.getPath("META-INF"));
    Path dest = zipFs.getPath("/META-INF/INDEX.LIST");

    Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);

    Files.find(zipFs.getPath("/"), Short.MAX_VALUE, (p, a) -> true)
        .forEach(System.out::println);
}

example.jar が作成され、以下の出力が得られます。

/
/META-INF
/META-INF/INDEX.LIST