1. NIO2: さらに詳しく見ていく
これまでに NIO2 に軽く触れましたが、ここではファイルやディレクトリ操作に便利なこのライブラリについて復習しつつ理解を深めます。
かつて Java には File クラスしかありませんでした。ファイルの存在確認、ファイルやフォルダの作成・削除、ディレクトリ内の一覧取得などはできましたが、制約が多くありました:
- パス操作が不便。OS ごとの差異(Windows の C:\Users\user\file.txt と Linux の /home/user/file.txt など)を考慮するのが面倒;
- シンボリックリンク、アクセス権、ファイル属性のまともなサポートがない;
- ディレクトリツリーの走査機能が限定的;
- エラー処理が貧弱。
Java 7 で NIO2(New Input/Output の第2版)が登場してから、開発者の生活は格段に楽になりました。今では次のものが使えます:
- ファイルやフォルダのパスを扱うための Path クラス;
- 読み書き・コピー・削除・メタ情報取得など主要操作を担う Files クラス;
- FileVisitor インターフェースや Files.walk のようなメソッドで、柔軟かつ簡単にファイルシステムを走査。
なぜ重要?
- クロスプラットフォーム性: 同じコードが Windows、Linux、macOS で区切り文字(/ や \)を意識せずに動作します。
- 安全性と使いやすさ: エラー情報が充実し、不可解な挙動や予期せぬサプライズが減ります。
- 高い表現力: 巨大なディレクトリでも処理でき、フィルタリングや並列処理を伴う再帰的走査も可能です。
2. 基本クラス: Path と Files
クラス Path
Path はファイルやフォルダへの現代的なパス表現です。実在するファイルを指している必要はなく、扱いやすい「パス」を表します。
Path の取得
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // 相対パス
Path path2 = Paths.get("/home/user/file.txt"); // 絶対パス
Path path3 = Path.of("mydir", "subdir", "file.txt"); // Java 11+ から
ポイント: Path は OS 非依存です。/ や \ で手動連結するのはもう忘れましょう!
文字列への変換
System.out.println(path1.toString());
親ディレクトリとファイル名の取得
Path parent = path1.getParent(); // 相対パスでは null の場合あり
Path fileName = path1.getFileName(); // ファイル名のみ
クラス Files
Files はファイルやディレクトリ操作のための静的メソッド集です:
- 存在確認: Files.exists(path)
- ファイルの読み書き: Files.readAllBytes(path), Files.write(path, bytes)
- 情報取得: Files.size(path), Files.getLastModifiedTime(path)
- コピー、削除、移動: Files.copy, Files.delete, Files.move
例:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("ファイルは存在します!");
System.out.println("サイズ: " + Files.size(path) + " バイト");
System.out.println("最終更新: " + Files.getLastModifiedTime(path));
} else {
System.out.println("ファイルが見つかりません。");
}
3. ファイルシステムの走査: Files.walk など
従来手法の問題
旧 API では、フォルダ配下のすべてのファイルを辿るには再帰関数を書き、ファイルかディレクトリかを手作業で判定し、無限再帰に陥らないよう注意する必要がありました。面倒なうえ、ミスもしやすい方法でした。
現代的な方法: Files.walk
Files.walk(Path start) は Stream<Path> を返します。指定パスから始まり、配下のサブディレクトリを含む全ファイルとフォルダのストリームです。ファイルシステムの走査は、ストリーム処理として記述できます。
例: すべてのファイルとフォルダを表示
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
ここでは mydir から始まるファイルとフォルダのすべてのパスが出力されます。
例: ファイルのみ(ディレクトリなし)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Files.isRegularFile(path) は通常のファイルに対してのみ true を返します(ディレクトリやシンボリックリンクは除外)。
例: 拡張子でのファイル検索
たとえば、あるディレクトリとサブディレクトリ内のすべての .txt ファイルを探したいとします:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
例: すべてのファイルの合計サイズを計算
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("サイズ読み取りエラー: " + path);
return 0L;
}
})
.sum();
}
System.out.println("ファイルの合計サイズ: " + totalSize + " バイト");
重要!
- Files.walk が返すストリームは 必ずクローズが必要です(AutoCloseable を実装)。そのため try-with-resources を使いましょう。
- デフォルトでは最下層まで走査します。深さは Files.walk(path, maxDepth) で制限できます。
4. 実践タスク
課題1: ディレクトリ内の画像をすべて見つける
.jpg、.png、.gif の拡張子を持つファイルを images フォルダで探し、名前を出力します。
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
課題2: すべての .txt ファイルを別フォルダへコピー
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("コピーしました: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("コピーエラー: " + path);
}
});
}
ここではサブディレクトリ構造を保持しています。
5. 役立つポイント
NIO2 の利点
クロスプラットフォーム
Path はディレクトリ区切りを自動で扱います。コードは Windows、Linux、macOS のいずれでも同様に動きます。
ストリーム処理
Files.walk のようなメソッドは Stream<Path> を返します。Stream API ができるフィルタ、変換、コレクションへの収集などがそのまま使えます。
巨大なディレクトリの扱い
ファイル数が多すぎると(例えば 100 000 枚の写真)旧 API はクラッシュすることがありました。NIO2 はすべてを一度にメモリへ載せないため、このようなケースも容易に処理できます。
シンボリックリンク、属性、アクセス権のサポート
パスがシンボリックリンクかの判定(Files.isSymbolicLink(path))、アクセス権の取得(Files.getPosixFilePermissions(path))、ファイル所有者の取得などが可能です。
旧 API と新 API の比較
| 操作 | 旧 API (File) | 新 API (Path, Files) |
|---|---|---|
| 存在確認 | |
|
| サイズ取得 | |
|
| フォルダ内のファイル一覧 | |
|
| 再帰的走査 | 手作業の再帰 | |
| ファイルのコピー | file.renameTo()(不安定) | |
| 拡張子の取得 | 文字列をパース | |
| 親の取得 | |
|
| 権限の扱い | ほぼ不可能 | |
重要なポイント
ファイル種別のチェック
- Files.isRegularFile(path) — 通常のファイル
- Files.isDirectory(path) — ディレクトリ
- Files.isSymbolicLink(path) — シンボリックリンク
巨大なディレクトリの扱い
- すべてのパスをリストに集めないでください。Stream<Path> を使い、逐次処理しましょう。
- 処理が終わったらストリームは必ずクローズします(try-with-resources)。
例外
- ほぼすべてのメソッドは IOException をスローし得ます。エラーを無視せず、処理する(または上位へ投げる)ようにしましょう。
走査の深さ制限
try (var paths = Files.walk(Path.of("mydir"), 2)) { // 最大 2 階層
// ...
}
6. NIO2 でよくある誤り
エラー1: walk のストリームを閉じ忘れる。 try-with-resources を使わないと、ファイルシステムのストリームが開いたままでリソースリークを起こします。必ず try (var paths = Files.walk(...)) { ... } 構文を使いましょう。
エラー2: パスがディレクトリかを確認していない。 Files.walk にフォルダではなくファイルのパスを渡すと、予期しない動作やエラーの原因になります。
エラー3: 例外を処理しない。 NIO2 のメソッドの多くは IOException を投げる可能性があります。これらを放置せず、少なくともメッセージを出すかログに記録しましょう。
エラー4: パス区切り文字の混同。 / や \ でパスを手動連結するのは避けてください。Path.of(...) や resolve(...) を使えば、OS ごとの差異を意識せずに正しく扱えます。
エラー5: 巨大なディレクトリを「メモリに」読み込もうとする。 ファイルが非常に多い場合は、すべてのパスをリストに集めず、Stream<Path> で逐次処理しましょう。
エラー6: クロスプラットフォーム性を失念。 Windows 風や Unix 風の絶対パスをハードコードしないでください。Path と resolve/relativize などの操作を使えば、どの OS でも正しく動作します。
GO TO FULL VERSION