この記事から得られる知識
- Java 7で導入されたNIO.2 (
java.nio.file
) の基本的な概念と、従来のjava.io.File
との違い。 - ファイルシステムのパスを表現する
Path
インターフェースの具体的な使い方。 Files
クラスを利用した、モダンで効率的なファイル・ディレクトリの基本操作(作成、コピー、移動、削除)。- 小さなファイルから巨大なファイルまで対応できる、様々なファイル読み書きの方法。
- Stream APIと連携した、宣言的で強力なファイル・ディレクトリ操作。
FileVisitor
を用いた、ファイルツリーの柔軟な再帰的走査方法。WatchService
を利用した、ディレクトリの変更をリアルタイムに監視する実装方法。
はじめに:なぜ今、java.nio.fileなのか?
Javaでのファイル操作と言えば、多くの開発者が長年java.io.File
クラスに親しんできました。しかし、このクラスはJavaの初期から存在する古いAPIであり、現代的なプログラミングの要求に応えるにはいくつかの課題を抱えています。
例えば、エラー処理が不親切(多くが例外をスローせずブール値を返す)であったり、シンボリックリンクの扱いに一貫性がなかったり、ファイル属性へのアクセスが限定的であったりといった問題です。
これらの課題を解決すべく、Java 7で全面的に刷新されたファイルI/O API、通称NIO.2 (New I/O 2)がjava.nio.file
パッケージとして導入されました。NIO.2は、より堅牢で、柔軟かつ高機能なファイル操作を実現します。
第1章: すべての基本 `Path` インターフェース
NIO.2の世界では、ファイルやディレクトリのパスを抽象的に表現するためにjava.nio.file.Path
インターフェースを使用します。これは、java.io.File
がファイルそのものとパスの両方を扱っていたのに対し、より明確に「パス」という概念に特化したものです。
Pathオブジェクトの生成
Path
はインターフェースであるため、new
で直接インスタンス化することはできません。代わりに、ユーティリティクラスであるjava.nio.file.Paths
のget()
メソッドを使って生成するのが最も一般的です。
import java.nio.file.Path;
import java.nio.file.Paths;
// 絶対パスからPathオブジェクトを生成 (Windowsの場合)
Path path1 = Paths.get("C:\\Users\\Guest\\Documents\\sample.txt");
// 絶対パスからPathオブジェクトを生成 (macOS/Linuxの場合)
Path path2 = Paths.get("/home/user/documents/sample.txt");
// 複数の文字列を連結してPathを生成
Path path3 = Paths.get("C:", "Users", "Guest", "Documents", "sample.txt");
// 相対パスからPathオブジェクトを生成
Path path4 = Paths.get("data", "input.csv");
Paths.get()
は、実行環境のOSに応じた適切なファイルセパレータ(Windowsなら\
, macOS/Linuxなら/
)を自動的に使用してくれるため、プラットフォーム依存のコードを書く必要がありません。
Pathが持つ便利なメソッド
Path
オブジェクトは、パス情報を操作するための豊富なメソッドを提供します。
メソッド | 説明 | コード例 |
---|---|---|
getFileName() |
パスの最後の要素(ファイル名またはディレクトリ名)を返します。 | Paths.get("C:\\docs\\file.txt").getFileName() → file.txt |
getParent() |
親ディレクトリのパスを返します。 | Paths.get("C:\\docs\\file.txt").getParent() → C:\docs |
getRoot() |
パスのルート部分を返します。 | Paths.get("C:\\docs\\file.txt").getRoot() → C:\ |
resolve(Path other) |
現在のパスに別のパスを連結(解決)します。 | Paths.get("/home/user").resolve("app") → /home/user/app |
relativize(Path other) |
現在のパスから別のパスへの相対パスを構築します。 | Paths.get("/a/b").relativize(Paths.get("/a/b/c/d")) → c\d |
normalize() |
. や.. などの冗長なパス要素を正規化します。 |
Paths.get("/a/b/../c").normalize() → /a/c |
toAbsolutePath() |
相対パスを絶対パスに変換します。 | Paths.get("data").toAbsolutePath() → (実行カレントディレクトリ)/data |
第2章: ファイル操作の主役 `Files` クラス
実際のファイルやディレクトリの操作(作成、削除、コピーなど)は、java.nio.file.Files
クラスが提供する数多くのstaticメソッドを通じて行います。これにより、Path
はあくまで「パス」の表現に徹し、Files
が「操作」を担当するという役割分担が明確になっています。
Files
クラスのメソッドは、操作に失敗した場合、その原因を示す詳細なIOException
またはそのサブクラスをスローするのが特徴です。これにより、信頼性の高いエラーハンドリングが可能になります。
ファイル・ディレクトリの存在確認
操作の前に、対象が存在するかどうかを確認するのは基本です。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path p = Paths.get("non_existent_file.txt");
// 存在するかどうか (シンボリックリンクを辿る)
boolean exists = Files.exists(p); // false
// 存在しないかどうか
boolean notExists = Files.notExists(p); // true
// 読み取り可能か
boolean isReadable = Files.isReadable(p); // false
// 書き込み可能か
boolean isWritable = Files.isWritable(p); // false
ファイル・ディレクトリの作成
import java.nio.file.*;
import java.io.IOException;
try {
// 空のファイルを作成
Path file = Paths.get("new_file.txt");
if (Files.notExists(file)) {
Files.createFile(file);
System.out.println("ファイルを作成しました: " + file);
}
// 単一のディレクトリを作成
Path dir = Paths.get("./new_dir");
if (Files.notExists(dir)) {
Files.createDirectory(dir);
System.out.println("ディレクトリを作成しました: " + dir);
}
// 複数の階層のディレクトリを一度に作成
Path dirs = Paths.get("./parent_dir/child_dir");
Files.createDirectories(dirs);
System.out.println("複数階層のディレクトリを作成しました: " + dirs);
} catch (IOException e) {
e.printStackTrace();
}
ファイルのコピー・移動
コピーや移動の際には、StandardCopyOption
列挙型を使って、上書きや属性のコピーなどの振る舞いを細かく制御できます。
import java.nio.file.*;
import java.io.IOException;
try {
Path source = Paths.get("source.txt");
Files.createFile(source); // 事前にコピー元を作成
Path targetCopy = Paths.get("copied.txt");
Path targetMove = Paths.get("renamed.txt");
// ファイルをコピー (同名ファイルがあれば上書き)
Files.copy(source, targetCopy, StandardCopyOption.REPLACE_EXISTING);
System.out.println("ファイルをコピーしました。");
// ファイルを移動 (リネーム) (同名ファイルがあれば上書き)
Files.move(targetCopy, targetMove, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
System.out.println("ファイルを移動/リネームしました。");
} catch (IOException e) {
e.printStackTrace();
}
ファイルの削除
import java.nio.file.*;
import java.io.IOException;
try {
Path fileToDelete = Paths.get("renamed.txt");
// ファイルを削除
Files.delete(fileToDelete);
System.out.println("ファイルを削除しました。");
// 存在する場合のみファイルを削除 (ファイルが存在しなくても例外は発生しない)
boolean deleted = Files.deleteIfExists(Paths.get("non_existent_file.txt"));
System.out.println("ファイルが存在しなかったので削除されませんでした: " + deleted);
} catch (IOException e) {
e.printStackTrace();
}
Files.delete()
でディレクトリを削除する場合、そのディレクトリは空でなければなりません。空でないディレクトリを削除しようとするとDirectoryNotEmptyException
がスローされます。
第3章: モダンなファイルの読み書き
NIO.2は、ファイルの読み書きにおいても、様々なニーズに応えるための多様なメソッドを提供します。
小さなファイルの簡単な読み書き
ファイルサイズが比較的小さく、メモリに一括で読み込んでも問題ない場合は、非常に簡潔なコードで読み書きが可能です。
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
import java.nio.charset.StandardCharsets;
Path file = Paths.get("small_text_file.txt");
try {
// 文字列をファイルに書き込む (Java 11以降)
Files.writeString(file, "こんにちは、NIO.2の世界へ!\nようこそ!", StandardCharsets.UTF_8);
// ファイルから文字列を一度に読み込む (Java 11以降)
String content = Files.readString(file, StandardCharsets.UTF_8);
System.out.println("--- readString ---");
System.out.println(content);
// ファイルの全行をリストとして読み込む
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
System.out.println("--- readAllLines ---");
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
Files.writeString()
とFiles.readString()
は、少量のテキストデータを扱う際の記述を大幅に簡略化します。
大きなファイルのためのストリーム処理
巨大なファイルを扱う場合、全コンテンツをメモリにロードするのは現実的ではありません。そのような場合は、バッファリングされたストリームを使用するのが定石です。Files
クラスは、従来のReader
/Writer
やInputStream
/OutputStream
を簡単に取得するメソッドを提供します。
import java.nio.file.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
Path largeFile = Paths.get("large_data.csv");
// try-with-resources文でリソースを自動的にクローズする
try (BufferedWriter writer = Files.newBufferedWriter(largeFile, StandardCharsets.UTF_8)) {
for (int i = 0; i < 10000; i++) {
writer.write(String.format("ID_%d,Value_%d%n", i, i * 10));
}
} catch (IOException e) {
System.err.println("書き込みエラー: " + e.getMessage());
}
try (BufferedReader reader = Files.newBufferedReader(largeFile, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 1行ずつ処理することでメモリ消費を抑える
// System.out.println(line);
}
System.out.println("大きなファイルの読み込みが完了しました。");
} catch (IOException e) {
System.err.println("読み込みエラー: " + e.getMessage());
}
Java 8 Stream APIとの華麗なる連携
NIO.2の真骨頂の一つが、Java 8で導入されたStream APIとのシームレスな統合です。これにより、ファイルやディレクトリの操作を、非常に宣言的かつ表現力豊かに記述できます。
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
Path file = Paths.get("small_text_file.txt");
// Files.lines(): ファイルの各行をStreamとして取得
System.out.println("--- Files.lines() ---");
try (Stream<String> stream = Files.lines(file, StandardCharsets.UTF_8)) {
stream.filter(line -> line.contains("ようこそ"))
.map(String::toUpperCase)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
// Files.list(): ディレクトリ直下の内容をStreamとして取得
System.out.println("\n--- Files.list() ---");
Path dir = Paths.get(".");
try (Stream<Path> stream = Files.list(dir)) {
stream.map(Path::getFileName)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
// Files.walk(): ディレクトリ階層を再帰的に走査した結果をStreamとして取得
System.out.println("\n--- Files.walk() ---");
try (Stream<Path> stream = Files.walk(dir, 2)) { // 2階層まで
stream.filter(p -> p.toString().endsWith(".txt"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
第4章: `FileVisitor`によるファイルツリーの探索
Files.walk()
は手軽ですが、ディレクトリ走査中の各ステップでより複雑な処理を行いたい場合や、エラーハンドリングを細かく制御したい場合には力不足です。そこで登場するのがFileVisitor
パターンです。
Files.walkFileTree()
メソッドに、FileVisitor
インターフェースの実装を渡すことで、ファイルツリーの走査を詳細にコントロールできます。
FileVisitor
インターフェースには4つのメソッドが定義されています。
preVisitDirectory
: ディレクトリに入る前に呼び出される。visitFile
: ファイルを訪れた際に呼び出される。visitFileFailed
: ファイルへのアクセスに失敗した際に呼び出される。postVisitDirectory
: ディレクトリ内のすべてのエントリを訪れた後に呼び出される。
通常は、これらのメソッドをすべて実装したSimpleFileVisitor
クラスを継承して、必要なメソッドだけをオーバーライドするのが便利です。
以下の例では、ファイルツリーを再帰的に削除する処理を実装してみます。
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class RecursiveDeleteVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("Deleting file: " + file);
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) {
// ディレクトリ内の処理でエラーが発生した場合
throw exc;
}
System.out.println("Deleting directory: " + dir);
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
public static void main(String[] args) {
Path rootDir = Paths.get("./parent_dir");
try {
// 事前にディレクトリとファイルを作成
Files.createDirectories(rootDir.resolve("child_dir"));
Files.createFile(rootDir.resolve("file1.txt"));
Files.createFile(rootDir.resolve("child_dir/file2.txt"));
System.out.println("Test directory created.");
System.out.println("--- Starting deletion ---");
// FileVisitorを使って再帰的に削除
Files.walkFileTree(rootDir, new RecursiveDeleteVisitor());
System.out.println("--- Deletion complete ---");
} catch (IOException e) {
e.printStackTrace();
}
}
}
第5章: 高度な機能 `WatchService` によるディレクトリ監視
アプリケーションによっては、特定のディレクトリでファイルが作成・変更・削除されたことをリアルタイムに検知したい場合があります。例えば、設定ファイルの自動リロードや、アップロードディレクトリに置かれたファイルの自動処理などです。
このような要求に応えるのがjava.nio.file.WatchService
です。これは、ファイルシステムの変更イベントを監視するための効率的な仕組みを提供します。
WatchService
の利用は、以下のステップで行います。
FileSystems.getDefault().newWatchService()
でWatchService
インスタンスを取得する。- 監視したい
Path
オブジェクトでregister()
メソッドを呼び出し、WatchService
と監視したいイベントの種類(StandardWatchEventKinds
)を登録する。 - 無限ループ内で
watchService.take()
を呼び出し、イベントが発生するまで待機する。このメソッドはブロッキングします。 - イベントが発生すると
WatchKey
が返されるので、pollEvents()
で発生したWatchEvent
のリストを取得し、それぞれ処理する。 - 最後に
watchKey.reset()
を呼び出し、キーをリセットして次のイベントを受け取れるようにする。
以下に、カレントディレクトリの変更を監視する簡単な例を示します。
import java.nio.file.*;
import java.io.IOException;
public class DirectoryWatcherExample {
public static void main(String[] args) {
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path dirToWatch = Paths.get(".");
// 監視したいイベントを登録
dirToWatch.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("Watching directory: " + dirToWatch.toAbsolutePath());
WatchKey key;
// take()はイベントが発生するまでブロックする
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(
"Event kind: " + event.kind()
+ ". File affected: " + event.context() + "."
);
}
// キーをリセットして次のイベントを待つ
boolean valid = key.reset();
if (!valid) {
// ディレクトリがアクセス不能になった場合など
break;
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
第6章: `java.io.File` との比較と移行
最後に、改めてjava.io.File
とjava.nio.file
パッケージ(主にPath
とFiles
)の違いをまとめます。
機能 | java.io.File |
java.nio.file.Path / Files |
---|---|---|
役割 | パスの表現とファイル操作の両方を担う。 | Path がパスを表現し、Files が操作を担う。役割が明確。 |
エラー処理 | 多くのメソッドが例外をスローせず、false やnull を返す。エラー原因の特定が困難。 |
操作の失敗時に詳細な情報を持つIOException をスローする。堅牢なエラー処理が可能。 |
シンボリックリンク | 限定的なサポートしかなく、意図しない動作をすることがある。 | 完全にサポート。リンクを辿るかどうかの制御も可能。 |
ファイル属性 | 基本的な属性(サイズ、更新日時、読み取り専用など)しか取得できない。 | ファイル所有者、パーミッション、タイムスタンプなど、詳細な属性にアクセス可能。 |
パフォーマンス | 大量のファイル操作において、オーバーヘッドが大きい場合がある。 | より効率的なI/O操作が可能。特にWatchService などは高効率。 |
APIの設計 | メソッドが少なく、複雑な操作は自前で実装する必要が多い。 | 豊富で高機能なユーティリティメソッドがFiles クラスに用意されている。 |
相互運用性
既存のライブラリやコードがまだjava.io.File
を要求する場合でも、心配は無用です。Path
とFile
は簡単に相互変換できます。
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
// Path から File へ
Path path = Paths.get("my-file.txt");
File file = path.toFile();
// File から Path へ
File anotherFile = new File("another-file.txt");
Path anotherPath = anotherFile.toPath();
この相互変換機能があるため、既存のコードベースを段階的にNIO.2へ移行させることが可能です。
まとめ
java.nio.file
パッケージは、JavaにおけるファイルI/Oのデファクトスタンダードです。その明確な役割分担、堅牢な例外処理、豊富な機能、そしてStream APIとの優れた親和性は、開発者の生産性とコードの品質を大きく向上させます。
最初は覚えることが多いと感じるかもしれませんが、一度その強力さと便利さを体験すれば、もはや古いjava.io.File
クラスには戻れなくなるでしょう。本記事で解説した内容を足がかりに、ぜひモダンなJavaファイル操作の世界を探求してみてください。