ファイルI/O・NIO.2(入出力)完全攻略ガイド|Java Gold SE17頻出

【図解】Java Gold試験頻出のI/O・NIO.2(入出力)。従来のFile・FileInputStreamと最新のPath・Filesの仕組み、ファイル操作の頻出ポイントを分かりやすく解説したアイキャッチ画像。

Java Gold試験において、受験者の心をへし折りにくるのが「入出力(I/O・NIO.2)」のセクションです。

「似たような名前のクラスが多すぎる」「絶対パスと相対パスを混ぜたらどうなるんだっけ?」と、試験中に頭が真っ白になった経験がある方も多いはず。

この記事では、従来のjava.ioパッケージのディープな仕様(シリアライズの裏側など)から、新世代のjava.nio.file(NIO.2)の複雑なパス操作まで、本試験で狙われるポイントを「これでもか」というほど徹底的に解説します。

目次

1. java.ioの基礎:ストリームの全体像と「flush」の真実

従来のjava.ioパッケージは、「どこから読み込み、どこへ書き出すか(ノードストリーム)」と、「データをどう加工するか(処理ストリーム)」の組み合わせで成り立っています。

バイトストリームと文字ストリームの違い

試験でまず問われるのは、クラス名からそのストリームの役割を正確に推測できるかです。

  • バイトストリーム(画像や音声、バイナリデータ用): クラス名が InputStream / OutputStream で終わる。 (例:FileInputStream, BufferedOutputStream, ObjectInputStream
  • 文字ストリーム(テキストファイル用): クラス名が Reader / Writer で終わる。 (例:FileReader, BufferedReader, PrintWriter

処理ストリーム(ラップ)の構造

ストリームはマトリョーシカのように包んで使います。

Java

// ファイル書き出しにバッファリング機能を追加する
BufferedWriter bw = new BufferedWriter(new FileWriter("data.txt"));

試験での罠:クローズ処理。一番外側のストリーム(上記の例ならbw)をclose()すれば、内側のストリーム(FileWriter)も連動して自動的にクローズされます。内側から順に閉じる必要はありません。

flush()メソッドはなぜ必要なのか?

書き込み用ストリーム(OutputStreamWriter)を扱う際、試験で頻出するのがflush()の挙動です。

Javaはパフォーマンス向上のため、データを1バイトずつディスクに書き込むのではなく、一旦メモリ上の「バッファ」に溜め込みます。そして、バッファが満杯になったときに初めてディスクへ物理的に書き出します。

  • flush()の役割: バッファが満杯になっていなくても、「今すぐ強制的にディスクへ書き出せ!」と指示を出すメソッドです。
  • 試験で狙われるポイント: コードの途中で例外が発生し、close()flush()も実行されずにプログラムが終了した場合、バッファに溜まっていたデータはファイルに書き込まれず消失します(ファイルが空のままになる)。 ※ただし、close()メソッドを呼び出すと、内部で自動的にflush()が実行されてからストリームが閉じられます。現在のJava(try-with-resources文)を使っていれば自動でclose()されるため意識しにくいですが、レガシーなコードを読ませる問題ではこの「書き忘れによるデータ未達」が答えになることがあります。

2. シリアライズ(直列化)の深淵:ObjectInputStream / ObjectOutputStream

Java Goldのjava.io分野において、最も配点が高く、かつ複雑なのが「シリアライズ」です。オブジェクトをそのままバイト列に変換してファイルに保存し、後から復元(デシリアライズ)する技術です。

基本ルール:Serializableインターフェース

直列化したいクラスには、必ずjava.io.Serializableインターフェースを実装(implements)する必要があります。このインターフェースにはメソッドが一つもありません(マーカーインターフェース)。

Java

// これだけで直列化可能になる
class Employee implements Serializable {
    private String name;
    private int age;
}

writeObject と readObject の基本

  • 保存時: ObjectOutputStreamwriteObject(Object obj)
  • 復元時: ObjectInputStreamreadObject() (※戻り値はObject型なので、元のクラスへのキャストダウンが必要です)

試験で狙われるシリアライズの「対象外」ルール

オブジェクトのすべての情報が保存されるわけではありません。以下の2つは保存されず、復元時には「型のデフォルト値(参照型ならnull、数値なら0、booleanならfalse)」になります。

  1. transientキーワードがついたフィールド: パスワードなど、ファイルに保存したくない機密データに付けます。
  2. staticフィールド: インスタンスではなく「クラス」に属するデータであるため、シリアライズの対象外です。

クラス継承時のシリアライズの罠(超頻出!)

「親クラスがSerializableを実装しているか否か」で、復元時の挙動が激変します。ここが最大の罠です。

  • パターンA:親がSerializableを実装している場合 子は自動的にシリアライズ可能になります。復元時、コンストラクタは一切呼ばれません。保存時の状態がそのままメモリに展開されます。
  • パターンB:親がSerializableを実装していない場合 子クラスだけが実装している場合、エラーにはなりませんが、復元時に「親クラスの引数なしコンストラクタ」が必ず実行されます。 ※もし親クラスに「引数ありコンストラクタ」しか定義されておらず、「引数なしコンストラクタ」が存在しない場合、コンパイルは通りますが、実行時にInvalidClassExceptionが発生します

カスタムシリアライズ(private writeObject / readObject)

通常のシリアライズ処理に割り込んで、独自の処理(暗号化など)を行いたい場合、クラス内に以下のメソッドを「private」で定義します。

Java

private void writeObject(ObjectOutputStream oos) throws IOException {
    // デフォルトの保存処理を呼ぶ
    oos.defaultWriteObject();
    // 独自の追加処理(例:日付を別途保存)
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    // デフォルトの復元処理を呼ぶ
    ois.defaultReadObject();
    // 独自の追加処理
}
  • 試験のポイント: インターフェースのオーバーライドではないため、修飾子は必ずprivateです(publicにすると機能しません)。Javaの内部処理がリフレクションを使ってこのメソッドを探し出して実行します。

3. NIO.2の核心:PathとPathsインターフェース

ここからはJava 7で登場し、現在の主流となっているjava.nio.fileパッケージです。 従来のjava.io.Fileの欠点を克服したNIO.2の中心となるのが、ファイルのパス(経路)を扱うPathインターフェースです。

  • 生成方法: Paths.get("C:\\data\\test.txt") または、Java 11以降で推奨される Path.of("C:\\data\\test.txt") を使います。

Pathオブジェクトは「文字列の計算」に過ぎない

試験を解く上で最も重要なマインドセットは、「Pathインターフェースのメソッド(resolve, relativizeなど)は、物理的なファイルが存在しなくても実行できる」ということです。単に文字列としてパスを切り貼りしているだけです。

頻出メソッド①:パス情報の取得

  • getFileName():ファイル名・末尾のディレクトリ名を返す(例:test.txt)。
  • getParent():親のパスを返す。
  • getRoot():ルート要素(C:\/)を返す。
  • getNameCount():ルートを除いた要素数を返す。
  • subpath(begin, end):指定したインデックスのパス要素を切り出す。(※endのインデックスは含まれない点に注意!)

頻出メソッド②:normalize()

パスに含まれる .(カレントディレクトリ)や ..(親ディレクトリ)を取り除き、最短のきれいなパスに整理します。

Java

Path p = Path.of("/a/b/../c").normalize(); // 結果: /a/c

4. 激ムズ!resolve() と relativize() の完全攻略

Java GoldのNIO.2で、全受験者が一度は絶望するのがこの2つのメソッドです。絶対に丸暗記してください。

resolve(Path) : パスの結合

ベースとなるパスに、引数のパスをくっつけます。

  • ルール1:引数が「相対パス」の場合 → 単純に後ろにくっつく。 Path.of("/a/b").resolve("c/d")/a/b/c/d
  • ルール2:引数が「絶対パス」の場合ベースのパスは破棄され、引数の絶対パスがそのまま返る。 Path.of("/a/b").resolve("/c/d")/c/d

relativize(Path) : 相対パスの計算(道案内)

「自分の場所(基準)」から「引数の場所(目的地)」へ行くための、相対パス(道順)を計算します。

  • 例: Path.of("a/b").relativize(Path.of("a/b/c/d"))c/d (a/bから見て、a/b/c/dへ行くには、さらにc/dへ進めばよい)
  • 例: Path.of("a/b/c").relativize(Path.of("a/b")).. (a/b/cから見て、a/bへ行くには、一つ上に戻ればよい)
  • 【超重要】例外が発生する罠ルール: relativize()の比較対象は、「両方とも絶対パス」か「両方とも相対パス」のどちらかでなければなりません。 片方が絶対パス、もう片方が相対パスの場合、コンパイルは通りますが、実行時に IllegalArgumentException が発生します。 Path.of("/a/b").relativize(Path.of("c/d")) ⇒ 💥実行時例外!

5. Filesクラス:実際のファイル操作と属性チェック

Pathが「文字列の計算」なら、Filesクラスは「実際のOS上のファイルシステムに対する操作」を行います。そのため、ファイルが存在しなかったり、権限がなかったりするとIOExceptionをスローします。

存在確認と同一性

  • Files.exists(Path):ファイルが存在するか確認。
  • Files.isSameFile(Path1, Path2):2つのパスが「物理的に同じファイル」を指しているか確認。一見違うパス(シンボリックリンクや .. を含むパス)でも、実体が同じならtrueになります。

コピー・移動・削除

  • Files.copy(source, target)
  • Files.move(source, target)
    • 罠: デフォルトでは、ターゲットのファイルが既に存在するとFileAlreadyExistsExceptionが出ます。上書きしたい場合は、第3引数に StandardCopyOption.REPLACE_EXISTING を指定する必要があります。
  • Files.delete(Path):ファイルがなければ NoSuchFileException
  • Files.deleteIfExists(Path):ファイルがなくても例外にならず false を返す。ディレクトリを削除する場合、中身が空でないと DirectoryNotEmptyException が発生します。

ファイル属性の読み取り(Attributes)

ファイルの作成日時やサイズなどを取得します。

  • 単一の取得:Files.getLastModifiedTime(Path) など。
  • まとめて取得:パフォーマンスを考慮し、一度にすべての属性を取得する Files.readAttributes() が推奨されます。
    • BasicFileAttributes:全OS共通の基本属性(サイズ、作成日時、ディレクトリか否か等)。
    • DosFileAttributes:Windows特有(隠しファイル、読み取り専用など)。
    • PosixFileAttributes:Linux/Mac特有(パーミッション、所有者グループなど)。

6. ディレクトリツリーの走査(Stream APIとの融合)

最後に、ディレクトリ内を検索・探索するメソッドです。ここでもStreamが返されるため、遅延評価される点に注意が必要です。返されたStreamは、try-with-resources文で確実にクローズする必要があります。

  • Files.list(Path) ディレクトリの「直下」にある要素だけをStreamで返します(深さ1固定)。サブディレクトリの中までは見に行きません。
  • Files.walk(Path, maxDepth, options) サブディレクトリの中まで潜って、すべてのファイルのPathをStreamで返します(深さ優先探索)。引数で探索する深さ(maxDepth)を指定できます。
  • Files.find(Path, maxDepth, BiPredicate) walkと似ていますが、指定した条件(BiPredicate:PathとBasicFileAttributesを受け取ってbooleanを返す)に一致するファイルだけを抽出してStreamで返します。

🚨 シンボリックリンクの無限ループ問題

walkfindは、デフォルトではシンボリックリンク(ショートカット)を辿りません。 オプションで FileVisitOption.FOLLOW_LINKS を指定すると辿るようになりますが、もしリンクが親ディレクトリを指していると「無限ループ」に陥る危険があります。Javaはこれを検知すると、賢く FileSystemLoopException をスローして処理を安全に停止します。


7. 実戦演習:Java Gold形式クイズ(全10問)

【第1問】Path.subpathの挙動

次のコードを実行した場合の出力結果は何ですか?

Java

Path path = Paths.get("/user/home/documents/data.txt");
System.out.println(path.subpath(1, 3));

A. user/home B. home/documents C. user/home/documents D. home/documents/data.txt

正解:B subpathのインデックスはルート直後から0, 1, 2…と数えます(0:user, 1:home, 2:documents)。subpath(1, 3) は「インデックス1から3の手前(2)まで」を返すため、home/documents となります。

【第2問】Path.relativizeの計算

次のコードを実行した場合の出力結果は何ですか?

Java

Path p1 = Paths.get("code");
Path p2 = Paths.get("code/java/Gold.java");
System.out.println(p1.relativize(p2));

A. code/java/Gold.java B. java/Gold.java C. ../java/Gold.java D. コンパイルエラー

正解:B p1(codeフォルダ)から見て p2 はどこにあるか。code の中にいる状態から code/java/Gold.java へ行くには、java/Gold.java と進むだけです。

【第3問】シリアライズとtransient

以下のクラスをシリアライズし、その後デシリアライズして変数 value を出力した結果はどうなりますか?

Java

class Data implements Serializable {
    static int id = 1;
    transient String value = "Default";
}

A. Default B. null C. 1 D. 例外が発生する

正解:B transient が付与されたフィールドは保存されません。デシリアライズ時、参照型変数は初期値である null に戻ります。

【第4問】Files.walkの特性

Files.walk(Path start) メソッドについて正しい説明はどれですか? A. デフォルトでシンボリックリンクを追跡する B. 幅優先探索(BFS)でファイルを辿る C. 深さ優先探索(DFS)でファイルを辿る D. ディレクトリは含まれず、ファイルのみを抽出する

正解:C Files.walk はデフォルトで深さ優先探索です。また、無限ループ防止のため、デフォルトではシンボリックリンクは追跡しません。

【第5問】System.console()の罠

System.console() に関する記述として正しいものはどれですか? A. 常に非nullのオブジェクトを返す B. 背景プロセスやIDEのコンソールで実行される場合、nullを返すことがある C. パスワード入力用の readPassword() メソッドは存在しない D. 文字入力専用であり、フォーマット出力(printf)はできない

正解:B インタラクティブな端末が割り当てられていない場合、null を返します。これは試験の超定番ひっかけです。

【第6問】Path.resolveの結合

次のコードの出力結果は何ですか? Path p1 = Paths.get("/a/b"); Path p2 = Paths.get("c"); System.out.println(p1.resolve(p2)); A. /a/b/c B. c C. /c D. /a/b

正解:A resolve に相対パスを渡すと、単純に結合されます。もし p2 が絶対パス(/c など)だった場合は、p2 が優先されます。

【第7問】Path.normalizeの整理

Paths.get("/a/./b/../c").normalize() の出力結果は何ですか? A. /a/b/c B. /a/c C. /a/b/../c D. /c

正解:B .(カレント)は消去、..(親)は一つ前の要素(この場合はb)と共に消去されます。結果として /a/c となります。

【第8問】Files.linesの戻り値

Files.lines(path) メソッドが返す型は何ですか? A. List<String> B. Stream<String> C. String[] D. String

正解:B ファイルの内容を1行ずつ処理するための Stream<String> を返します。メモリ効率が良いため、巨大なファイルに適しています。

【第9問】BufferedReaderのメソッド

BufferedReader クラスで使用できる、行を丸ごと読み込むメソッドはどれですか? A. read() B. readAll() C. readLine() D. getNextLine()

正解:C readLine() です。戻り値は String で、ファイルの終端(EOF)に達すると null を返します。

【第10問】Serializableと継承

親クラスが Serializable を実装しておらず、子クラスだけが実装している場合、デシリアライズ時に親のフィールドはどうなりますか? A. 保存されていた値に復元される B. 親の引数なしコンストラクタが実行され、初期化される C. コンパイルエラーになる D. 実行時に NotSerializableException が投げられる

正解:B 親が Serializable でない場合、親のフィールドは保存されません。復元時は親の引数なしコンストラクタが呼ばれて初期化されます。もし引数なしコンストラクタがない場合は実行時に例外が発生します。


まとめ:合格へのチェックリスト

いかがでしたでしょうか。膨大な量に見えますが、Java Goldの試験問題は以下のパターンに集約されます。

  1. クラスやメソッドの役割をすり替える問題(例:文字データなのにFileInputStreamを使っている)
  2. シリアライズの継承ルール違反(親に引数なしコンストラクタがないのに復元しようとする)
  3. パス操作の例外relativizeで絶対パスと相対パスを混ぜている)
  4. ファイル操作のオプション忘れ(コピー時に既存ファイルがあり例外終了する)

本番の試験画面でコードを読むときは、まず「例外が発生する罠が仕掛けられていないか?」を疑う目を持ってください。この膨大な仕様を一つずつマスターしていくことが、Gold合格への最短にして唯一の道です。頑張りましょう!

I/OやNIO.2は、実際にコードを書いてみると「え、そうなるの?」という発見が多い分野です。特にパス操作は、物理ファイルを作らなくても Paths.get() だけで試せるので、迷ったら一度動かしてみるのが合格への近道です!

理解したら、問題で確認

Java Gold SE17のコード読解問題に挑戦する

この記事で学んだ内容を、Java Gold SE17向けの問題演習アプリで復習できます。 カテゴリ別問題と60問の模擬試験を、登録不要で利用できます。

Java Gold問題を解く →
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

tibiyaのアバター tibiya ITエンジニア

文系卒エンジニア(8年目)
Java / JavaScript / C# を中心に業務システム開発を経験。
Java資格・IT就活・文系エンジニアの実体験を発信しています。

コメント

コメントする

目次