1. Java はどのようにファイルを扱うようになったか
Java とファイル:最初はどうだったか
かつて 1996 年、言語の開発者たちは最初のファイル操作の方法としてパッケージ java.io を提案しました。そこには File、FileInputStream、FileOutputStream、Reader、Writer などのクラスが登場しました。これらのクラスにより、ファイルの存在確認、サイズ、更新日時の取得、ディレクトリの操作などが可能になりました。
しかし、初期バージョンによくあるように、すべてが完璧というわけではなく、もっと便利で安全にできる点が多く残っていました。
旧 API(java.io)の制限
- File はファイルそのものではなく「ショートカット」。 File クラスは中身の読み書きができません。表すのはパスとメタデータです。読み書きには別のストリーム(FileInputStream/FileOutputStream や Reader/Writer)が必要です。
- パス操作はつらい。 "C:\\Users\\" + user + "\\Documents" のように手作業で連結すると、Windows と Linux 間の移植で壊れがちでした。
- シンボリックリンク非対応。 旧 API はシンボリックリンクを理解せず、正しく扱えないことがありました。
- 属性サポートが弱い。 アクセス権、所有者、「隠し」フラグなどの取得が難しい。
- 大きなファイルに非効率。 古典的なストリームは大容量データに対する便利で高速なパターンを提供せず、非同期 I/O もありませんでした。
java.nio.file の登場(NIO.2、Java 7)
Java 7 で新しいパッケージ java.nio.file(通称 NIO.2)が登場しました。これにより次が導入されました:
- Path — 現代的なパス抽象(ZIP 内やクラウド・ネットワークのファイルシステム内のパスも含む)。
- Files — 読み書き・コピー・削除のための静的ユーティリティ。
- FileSystem — ローカルディスクだけでなく他のファイルシステムも扱える。
- シンボリックリンク、拡張属性、アクセス権のサポート。
- 非同期 I/O と、大量データでの性能向上。
- ディレクトリ操作の容易さと、ストリーム API との良好な連携。
NIO は「New Input/Output」の略で、登場から 10 年以上経った今でも、Java にとっての現代的な標準です。
2. アプローチ比較: java.io と java.nio.file
クラス File(java.io)
- ファイルまたはディレクトリのパスとそのメタデータを表す。
- ファイル内容の読み書きはできない。
- 存在、サイズ、種類(ファイル/ディレクトリ)、絶対パスなどを取得できる。
import java.io.File;
public class ExampleIO {
public static void main(String[] args) {
File file = new File("example.txt");
if (file.exists()) {
System.out.println("ファイルは存在します!");
System.out.println("サイズ: " + file.length() + " バイト");
}
}
}
クラス Path(java.nio.file)
- 現代的なパス抽象で、自然に結合や正規化ができる。
- ローカル、アーカイブ、ネットワークのパスを表現できる。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ExampleNIOPath {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
if (Files.exists(path)) {
System.out.println("ファイルが見つかりました!");
System.out.println("サイズ: " + Files.size(path) + " バイト");
}
}
}
クラス Files(java.nio.file)
- 読み書きを丸ごとまたは部分的に行うための静的メソッド群。
- ファイルのコピー、削除、移動。
- ディレクトリの作成/削除。
- 拡張属性へのアクセス。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ExampleNIOFiles {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
}
}
比較表
| 機能 | |
|
|---|---|---|
| パスの表現 | はい | はい |
| 内容の読み書き | いいえ | はい(Files 経由) |
| パス操作 | 不便 | 便利(resolve, normalize) |
| シンボリックリンク | いいえ | はい |
| ファイル属性 | 制限的 | 拡張 |
| 非同期 I/O | いいえ | はい(NIO.2) |
| クロスプラットフォーム性 | 部分的 | 優秀 |
3. いつどちらを使うか?
旧 API(java.io):いつ必要?
- レガシーコードのサポート用。 プロジェクトが Java 7 以前に始まり、至る所で File、FileInputStream、FileOutputStream を使っているなら、互換性を維持する必要があるかもしれません。
- 新規プロジェクトでは非推奨。 今日「ベテラン」の File を使うのは、VHS テープで動画を見るようなものです。
新しい API(java.nio.file):現代の標準
- 新規プロジェクトでは常に Path と Files を優先しましょう。
- より簡単で安全かつ強力で、ストリームやラムダ、try-with-resources との統合も良好です。
クイックリファレンス
- 読み書き・コピー・削除? — Files 経由。
- パス操作(結合、正規化)? — Path。
- サイズ・日時・アクセス権? — Files.getAttribute() などのメソッド。
- ディレクトリを再帰的に走査? — Files.walk、Files.list。
4. 例:旧 API と新 API のコード比較
例1: ファイルの存在チェック
旧方式:
import java.io.File;
File file = new File("data.txt");
if (file.exists()) {
System.out.println("ファイルが見つかりました!");
}
新方式:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("data.txt");
if (Files.exists(path)) {
System.out.println("ファイルが見つかりました!");
}
例2: ファイルの全行読み込み
旧方式:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
File file = new File("data.txt");
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
新方式:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
Path path = Paths.get("data.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
コメント: 新方式の方が短くて安全。try-with-resources を用いればリソース管理はさらに簡単になります。
例3: 文字列をファイルへ書き込む
旧方式:
import java.io.FileWriter;
FileWriter writer = new FileWriter("output.txt");
writer.write("やあ、ファイル!");
writer.close();
新方式:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("output.txt");
Files.write(path, "やあ、ファイル!".getBytes());
5. 役立つポイント
なぜ Java は作り直したのか?
- 安全性と信頼性。 リソースのクローズやパス操作でのミスが減ります。
- クロスプラットフォーム。 Windows、Linux、さらには ZIP アーカイブに対しても共通のクラスで扱えます。
- 拡張の容易さ。 クラウドやネットワークのファイルシステムのサポートを追加しやすい。
- パフォーマンス。 大きなファイルでの挙動が向上し、非同期操作に対応。
- 現代の Java スタックとの親和性。 ラムダ、ストリーム、try-with-resources。
旧 API から新 API へ移行するには?
ほぼ常に File を Path に、またその逆に変換できます:
File file = new File("data.txt");
Path path = file.toPath();
File file2 = path.toFile();
古いコードに出会ったら?
- 慌てないでください。もっと簡単かつモダンにできます。
- 可能であれば、java.nio.file に書き換えましょう。
- 難しい場合はブリッジ(toPath()、toFile())を使い、段階的に移行します。
6. 最後の例:ミニアプリ
例: ファイルの存在を確認し、サイズを出力する。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileInfo {
public static void main(String[] args) {
Path path = Paths.get("test.txt");
if (Files.exists(path)) {
try {
long size = Files.size(path);
System.out.println("ファイルが見つかりました。サイズ: " + size + " バイト");
} catch (Exception e) {
System.out.println("ファイルサイズ取得時のエラー: " + e.getMessage());
}
} else {
System.out.println("ファイルが見つかりません!");
}
}
}
ここで使ったもの:
- Path — パスの表現。
- Files.exists() — 存在確認。
- Files.size() — サイズ取得。
7. ファイル API での典型的なミス
エラー No.1: File が内容を読み書きできると思い込む。 実際には File は単なる「ショートカット」です。読み書きには FileInputStream/FileOutputStream(旧 API)や、Files(新 API)のユーティリティを使いましょう。
エラー No.2: + やスラッシュでパスを手作業で連結する。 これは OS ごとに不具合の原因になります。Path.resolve() や、複数引数の Paths.get(...) を使いましょう。
エラー No.3: ストリームを閉じ忘れる。 旧 API ではリソースリークの典型的な原因です。新 API の多くの操作は Files を通すためストリームを生成しませんし、必要な場面でも try-with-resources を適用しましょう。
エラー No.4: 新規プロジェクトで旧 API を使う。 新しくプロジェクトを始めるなら java.nio.file を選びましょう。java.io はレガシーコードとの互換性が必要なときにのみ意味があります。
GO TO FULL VERSION