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#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 } }