argius note

プログラミング関連

Java SE 7 (4) - "NIO.2" ファイルシステム関連APIの増強

残りの言語仕様変更は「その他の雑多な変更」に含めるとして、次はAPIのほうを見ていきましょう。


NIO.2。Java1.4のときに追加された、新I/OのAPI群につけられたのが「New I/O」すなわちNIOです。
歴史的な都合が影響を与えて構成されてきたAPIツリーは、整合性という観点からするととても美しいとは言いがたいものだと思います。カテゴライズというのはかくも難しいものなんですね。


NIO.1のときは、チャネルとバッファという概念が追加され、主にデータの入出力においてメモリの効率的な操作を行うためのAPIが追加されました。ただ設計が高度過ぎて?とっつきにくいところがありました。

今回のNIO.2では、ファイルシステムに関する操作が大幅に追加されたのが特徴です。

  • ファイルのパス・属性・操作の分離
    • Fileクラスには、ファイルの属性、パスの概念、ファイルの操作などが混在しており、抽象度の低いAPIでした。NIO.2では、パスを表すPathインタフェース、ファイルの属性を表すjava.nio.file.attributeクラス群、ファイル操作ユーティリティメソッドクラスのFilesクラスが抽出されました。もちろん、Fileクラス自体には元の機能は具わったままです。
  • ファイルの移動・コピー
    • ファイルの移動はFile#renameToがありますが、これはあまり使い勝手が良くなく、「上書き移動」などをサポートしていません。今回のは移動とコピーを同じように扱えるのが特徴。そしてコピーが標準装備されました。
  • 監視サービス
    • ファイルの変更などを監視する監視サービス機能の追加。例えばGUIファイラなどを作ったとして、ファイルが削除されたら表示を更新する、のような場合に使用します(多分)。


それでは、それぞれのAPIの具体的な機能を見ていきます。

なお、使用しているJDKは本家の"build 1.7.0-b147"です。

java.nio.file.Path

Paths(java.nio.file.Paths)はPathのファクトリメソッドクラスで、Pathはインタフェースです。実際のクラスは私の環境ではPaths.get("").getClass()="class sun.nio.fs.WindowsPath"でした。
Javaでは、ファクトリメソッドクラスやユーティリティメソッドクラスには複数形の名前が付くのが慣習です(例:Collections,Arrays,Channels,Executors)。PathsやFilesもこの一種です。
私はあまりこのルールは好きではありませんが、"Utils"とか"Utilities"(サード発祥のライブラリやApache Commonsなどに見られる)とするのも同様にしっくり来ません(FactoryはOK)。かと言って、「なら何なら良いんだ」と言われても思いつきません。強いて言うなら、CollectionsはCollectionで、CollectionインタフェースはICollectionかな。これはこれでJavaの流儀に根本的に反するし、もう手遅れなので却下でしょう。命名というのは重要だけど難しいですね。


Fileクラスでもパスの操作をいくつか持っていたわけですが、Pathはパスに関する機能だけを持っています。その分、その操作の種類は充実しています。もちろん、immutableでthread-safeです。
PathSeparatorで区切った要素をiterateする操作などがあり、階層数を扱えるようになりました。

final Path path = Paths.get("D:/tmp/aaa/bbb");
System.out.println(path.getNameCount()); // => 3
for (final Path name : path) {
    System.out.printf("%s,", name); // => tmp,aaa,bbb,
}

あとは、結構使えるんじゃないかと思うのがrelativize。パス1からのパス2の相対パスを求めることができます。

final Path path1 = Paths.get("D:/tmp/aaa/bbb");
final Path path2 = Paths.get("D:/tmp/aaa/ccc");
System.out.println(path1.relativize(path2)); // => ..\ccc


それともうひとつ、PathMatcher(java.nio.file.PathMatcher)というインタフェースがあります。これ自体はメソッドmatches(Path)だけしか持っていないのですが、FileSystem#getPathMatcher(String syntaxAndPattern)がファクトリで、このsyntaxAndPatternに設定するパターンはglobとregexが選べます。使い方の例は以下のとおり。詳細はFileSystem#getPathMatcherの説明をご覧ください。

final FileSystem fs = FileSystems.getDefault();
final PathMatcher glob = fs.getPathMatcher("glob:*.java");
final PathMatcher regex = fs.getPathMatcher("regex:(?i).*\\.JAVA$");
String[] a = {"a.java", "a.Java", "a.JAVA", "a.class",};
for (final String name : a) {
    final Path path = Paths.get(name);
    System.out.printf("%-8s %-5s %-5s%n", path,
                      glob.matches(path), regex.matches(path));
}

これを実行すると、

a.java   true  true 
a.Java   true  true 
a.JAVA   true  true 
a.class  false false

となります。

java.nio.file.Files

Filesには、いままでありそうで無かった、あらゆるファイル操作に関するユーティリティメソッドが含まれています。全部で45種類、55メソッド(オーバーロードメソッド含む)もあります。ここには、Fileクラスで実装されていたものをFiles+Pathで置き換えたものも含まれます。
では、試してみましょう。ファイルを扱うバッチプログラムやユーティリティプログラムを作るなら有用なメソッドばかりですが、さすがに45種類もあるので、それ以外でも使うことがありそうなメソッドを抜粋します。

move/copy
ファイルの移動・コピー

上述したとおり、File#renameToは使いにくいメソッドです。コピーに至っては、標準では存在すらしませんでした。これらの一般的な操作であるmove/copyが導入されました。
上書き移動の場合は次のようにします。

// throws IOException
Files.move(Paths.get("aaa.txt"),
           Paths.get("bbb.txt"),
           StandardCopyOption.REPLACE_EXISTING);
write
ファイルのopen/close操作を直接使わずに、ファイルにデータを出力できます。

言わば、"echo aaa > file"のような操作ができます。
バイナリファイル用とテキストファイル用の2つのメソッドがあります。ここではテキストファイル版で試します。appendモードで書き込んでいます。

String[] lines = {"aaa", "bbb", "ccc",};
// throws IOException
Files.write(Paths.get("D:/tmp/java7/writetest.txt"),
            Arrays.asList(lines),
            Charset.defaultCharset(),
            StandardOpenOption.CREATE,
            StandardOpenOption.APPEND);

なお、readAllBytes/readAllLinesはこれらと対になるメソッドです。

createLink/createSymbolicLink
ハードリンク・シンボリックリンクを作成します。

WindowsXPではシンボリックリンクはUnsupportedOperationExceptionがthrowされます。(いい加減サポートされてくれないでしょうか。追記2011-10-20: Vista以降ではシンボリックリンクが使えるようになっていて、createSymbolicLinkがちゃんと使えます。
ハードリンクは作成できます。"writetest-linked.txt"を編集すると、"writetest.txt"も編集されていることが確認できます。

// throws IOException
Files.createLink(Paths.get("D:/tmp/java7/writetest-linked.txt"),
                 Paths.get("D:/tmp/java7/writetest.txt"));
Files.createSymbolicLink(Paths.get("D:/tmp/java7/writetest-symlinked.txt"),
                         Paths.get("D:/tmp/java7/writetest.txt"));
                         // => java.lang.UnsupportedOperationException: Symbolic links not supported on this operating system
walkFileTree
ディレクトリツリーをたどります。

変り種のメソッド。個人的には断然おもしろい一品。
あるディレクトリから下のサブディレクトリとファイルを全部チェックしたいときに使います。FileVisitorというインタフェースがセットになっていて、ファイル毎にFileVisitorのメソッドが呼ばれます。
次のプログラムは、カレントディレクトリからすべてのファイル(ディレクトリ除く)をたどって表示します。I/Oエラーが発生した場合は処理を中断します(IOExceptionがスローされる)。

// throws IOException
Files.walkFileTree(Paths.get(""), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
        System.out.println(path);
        return FileVisitResult.CONTINUE;
    }
});

次のプログラムは、カレントディレクトリからサブディレクトリを含むすべてのファイル(ディレクトリ除く)をたどって表示します。I/Oエラーが発生した場合はそれを表示して処理を続行します。

// throws IOException
Files.walkFileTree(Paths.get(""), new SimpleFileVisitor<Path>() {

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        return process(dir, attrs);
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        return process(file, attrs);
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
        if (ex != null) {
            handleError(dir, ex);
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException ex) throws IOException {
        if (ex != null) {
            handleError(file, ex);
        }
        return FileVisitResult.CONTINUE;
    }

    FileVisitResult process(Path path, BasicFileAttributes attrs) {
        System.out.println(path);
        return FileVisitResult.CONTINUE;
    }

    void handleError(Path path, IOException ex) {
        System.out.printf("%s at %s%n", ex, path);
    }

});

java.nio.file.WatchService関連

ディレクトリのエントリが追加されたり変更されたりした場合に検知することができます。
以下のコードは、"D:/tmp/java7"以下のファイルが変更されるまでws.take()でwaitし続けます。ディレクトリ以下のファイル(ここでは"writetest.txt")を変更すると、waitが解放され、その変更されたファイル名を表示します。

// throws IOException, InterruptedException
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
    final Path dir = Paths.get("D:/tmp/java7");
    dir.register(ws, StandardWatchEventKinds.ENTRY_MODIFY);
    for (final WatchEvent<?> watchEvent : ws.take().pollEvents()) {
        System.out.println(watchEvent.context());
        // => writetest.txt
    }
}

まとめ

このように、Javaでもファイルシステムの操作が扱いやすくなってきました。
他にも追加されたAPIjava.nio.file.attribute.*やjava.nio.channels.SeekableByteChannelなど多数ありますが、ここでは割愛します。