IOExceptionとは、ファイルの読み書きやネットワーク通信など、プログラムの外とのやり取り(入出力)がうまくいかなかったときに出る例外です。 IOException そのものよりも、NoSuchFileException や ConnectException といった子クラスの名前とエラー文を見れば、原因はほぼ特定できます。

この記事では、ファイル・ストリーム・ネットワークの入出力エラーを Linux 上で実際に再現し、エラー文・原因・直し方を一覧表にしました。あわせて、Files.lines やラムダ式の中で出る UncheckedIOException の正体、ラムダの中で IOException を包んで外で取り出す書き方、catch を書く順番とコンパイルエラーの例まで、コピペできるコードで説明します。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。ファイルの権限に関する例は一般ユーザーで実行しています。

IOExceptionの早見表(エラー文から原因と直し方を引く)

エラー文(クラス名: メッセージ) 原因 直し方
java.nio.file.NoSuchFileException: work/none.txt ファイル、または途中のフォルダがない パスを確かめる。書き込みなら先に Files.createDirectories(親フォルダ)
java.io.FileNotFoundException: work/none.txt (No such file or directory) 同上(FileInputStream など java.io 側の表示) 同上。かっこの中の理由を読む
java.nio.file.AccessDeniedException: secret.txt 読み書きの権限がない(java.io 側では FileNotFoundException: secret.txt (Permission denied)) ls -l で権限と所有者を確かめ、実行ユーザーか権限を合わせる
java.nio.file.FileAlreadyExistsException: work/a.txt 作成・コピー先がすでにある 上書きするなら StandardCopyOption.REPLACE_EXISTING。フォルダは createDirectories
java.nio.file.DirectoryNotEmptyException: work/full 中身のあるフォルダを削除しようとした 中のファイルから先に削除する
java.io.IOException: Is a directory フォルダをファイルとして読んだ Files.isRegularFile(path) で確かめる
java.nio.file.NotDirectoryException: work/a.txt ファイルをフォルダとして開いた Files.isDirectory(path) で確かめる
java.nio.charset.MalformedInputException: Input length = 1 ファイルの文字コードが、読み取りに指定したもの(既定は UTF-8)と違う 実際の文字コードを指定する(例:Charset.forName("Shift_JIS"))
java.io.IOException: Stream closed 閉じた後のストリームを使った 読み書きは try-with-resources のブロックの中で済ませる
java.io.EOFException 決まった長さを読む途中でデータが終わった データの長さ・形式を確かめる。終わりの判定は戻り値(-1 や null)で行う
java.io.IOException: No space left on device 書き込み先の空き容量がない df -h で空きを確かめる
java.net.UnknownHostException: no-such-host.invalid: Name or service not known ホスト名から接続先を引けない ホスト名のつづり、DNS の設定を確かめる
java.net.ConnectException: Connection refused 接続先のポートで誰も待ち受けていない 相手のサービスが起動しているか、ポート番号が合っているか確かめる
java.net.SocketTimeoutException: Read timed out 設定した時間内に応答が来なかった(HttpClient では HttpTimeoutException: request timed out) 相手の処理時間を確かめ、待つ時間の設定を見直す
java.net.SocketException: Connection reset 通信の途中で相手側から切断された 相手側のログを確かめる。必要なら接続し直す
java.net.BindException: Address already in use 待ち受けようとしたポートが使用中 別のポートにする。前のプロセスが残っていないか確かめる
java.io.UncheckedIOException: java.nio.charset.MalformedInputException: ... Stream の処理中に起きた IOException が包まれて届いた getCause() で中身の IOException を取り出して上の行を引く

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java IOExceptionの原因と対処|種類一覧とエラー文別の直し方」

IOExceptionとは:検査例外と子クラスの関係

IOException は java.io パッケージの例外で、Exception の直接の子クラスです。Java 21 の API ドキュメントでは「何らかの入出力例外が発生したことを示す。入出力処理がうまくいかなかった、または中断されたときに出る例外の一般的なクラス」と説明されています(出典は記事末尾)。

  • 検査例外です。IOException を投げる可能性のあるメソッドを呼ぶときは、try-catch で受けるか、メソッドに throws IOException を書く必要があります
  • 多くの子クラスがあります。API ドキュメントには直接の子クラスだけで35個が載っています
  • 原因はプログラムの外(ファイルの有無、権限、ネットワーク、相手のサーバー)にあることが多く、コードが正しくても起こりえます

try-catch も throws も書かずに呼ぶと、javac が error: unreported exception IOException; must be caught or declared to be thrown と知らせます。

種類一覧:よく出会う子クラス

親子関係は、各クラスの親クラスを実際にたどって確かめたものです。

分野 クラス 親クラス どんなとき
ファイル(java.io) FileNotFoundException IOException FileInputStream、FileReader などで開けなかった
ファイル(java.nio.file) NoSuchFileException FileSystemException ファイルやフォルダがない
ファイル(java.nio.file) AccessDeniedException FileSystemException 権限がない
ファイル(java.nio.file) FileAlreadyExistsException FileSystemException すでにある
ファイル(java.nio.file) DirectoryNotEmptyException FileSystemException フォルダが空ではない
ファイル(java.nio.file) NotDirectoryException FileSystemException フォルダではない
読み取り EOFException IOException 読んでいる途中でデータが終わった
文字コード MalformedInputException CharacterCodingException バイト列を文字にできない
ネットワーク UnknownHostException IOException ホスト名を引けない
ネットワーク ConnectException SocketException 接続できない
ネットワーク BindException SocketException ポートを確保できない
ネットワーク SocketTimeoutException InterruptedIOException 時間内に応答がない
ネットワーク HttpTimeoutException IOException HttpClient で時間内に応答がない
(別枠) UncheckedIOException RuntimeException IOException を包んだ非検査例外。IOException の子ではない

FileSystemException、CharacterCodingException、SocketException、InterruptedIOException はいずれも IOException の子クラスです。つまり表の UncheckedIOException 以外は、すべて catch (IOException e) で受け取れます。

ファイル操作のエラーを再現する

次のプログラムは、うまくいかない操作をわざと並べ、例外のクラス名とメッセージを表示します。権限の例のために、実行するフォルダに「実行ユーザーが読み書きできないファイル secret.txt」と「実行ユーザーが書き込めないフォルダ locked」を用意しました(管理者が作成し、secret.txt は権限 600、locked は権限 755)。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileErrors {
    interface Action { void run() throws IOException; }

    static void show(String label, Action action) {
        try {
            action.run();
            System.out.println(label + "\n   => 例外なし");
        } catch (IOException e) {
            System.out.println(label + "\n   => " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) throws IOException {
        // 準備:work フォルダの中に確認用のファイルを作る
        Path work = Path.of("work");
        Files.createDirectories(work.resolve("full"));
        Files.writeString(work.resolve("a.txt"), "hello");
        Files.writeString(work.resolve("full/child.txt"), "x");
        Files.write(work.resolve("sjis.txt"), "こんにちは".getBytes("Shift_JIS"));

        System.out.println("--- ファイルやフォルダが見つからない");
        show("Files.readString(work/none.txt)", () -> Files.readString(work.resolve("none.txt")));
        show("Files.writeString(work/nodir/b.txt)", () -> Files.writeString(work.resolve("nodir/b.txt"), "x"));
        show("new FileInputStream(work/none.txt)", () -> new FileInputStream("work/none.txt").close());

        System.out.println("--- すでにある・空ではない");
        show("Files.createFile(work/a.txt)", () -> Files.createFile(work.resolve("a.txt")));
        show("Files.copy(a.txt → full/child.txt)", () -> Files.copy(work.resolve("a.txt"), work.resolve("full/child.txt")));
        show("Files.delete(work/full)", () -> Files.delete(work.resolve("full")));

        System.out.println("--- ファイルとフォルダの取り違え");
        show("Files.readString(work/full)", () -> Files.readString(work.resolve("full")));
        show("Files.list(work/a.txt)", () -> Files.list(work.resolve("a.txt")).close());
        show("new FileInputStream(work/full)", () -> new FileInputStream("work/full").close());

        System.out.println("--- 権限がない");
        show("Files.readString(secret.txt)", () -> Files.readString(Path.of("secret.txt")));
        show("Files.writeString(secret.txt)", () -> Files.writeString(Path.of("secret.txt"), "x"));
        show("new FileInputStream(secret.txt)", () -> new FileInputStream("secret.txt").close());
        show("new FileOutputStream(secret.txt)", () -> new FileOutputStream("secret.txt").close());
        show("Files.createFile(locked/new.txt)", () -> Files.createFile(Path.of("locked/new.txt")));

        System.out.println("--- 文字コード・空き容量");
        show("Files.readString(work/sjis.txt) を UTF-8 で", () -> Files.readString(work.resolve("sjis.txt")));
        show("/dev/full に書き込む", () -> Files.writeString(Path.of("/dev/full"), "x"));
    }
}
--- ファイルやフォルダが見つからない
Files.readString(work/none.txt)
   => java.nio.file.NoSuchFileException: work/none.txt
Files.writeString(work/nodir/b.txt)
   => java.nio.file.NoSuchFileException: work/nodir/b.txt
new FileInputStream(work/none.txt)
   => java.io.FileNotFoundException: work/none.txt (No such file or directory)
--- すでにある・空ではない
Files.createFile(work/a.txt)
   => java.nio.file.FileAlreadyExistsException: work/a.txt
Files.copy(a.txt → full/child.txt)
   => java.nio.file.FileAlreadyExistsException: work/full/child.txt
Files.delete(work/full)
   => java.nio.file.DirectoryNotEmptyException: work/full
--- ファイルとフォルダの取り違え
Files.readString(work/full)
   => java.io.IOException: Is a directory
Files.list(work/a.txt)
   => java.nio.file.NotDirectoryException: work/a.txt
new FileInputStream(work/full)
   => java.io.FileNotFoundException: work/full (Is a directory)
--- 権限がない
Files.readString(secret.txt)
   => java.nio.file.AccessDeniedException: secret.txt
Files.writeString(secret.txt)
   => java.nio.file.AccessDeniedException: secret.txt
new FileInputStream(secret.txt)
   => java.io.FileNotFoundException: secret.txt (Permission denied)
new FileOutputStream(secret.txt)
   => java.io.FileNotFoundException: secret.txt (Permission denied)
Files.createFile(locked/new.txt)
   => java.nio.file.FileAlreadyExistsException: locked/new.txt
--- 文字コード・空き容量
Files.readString(work/sjis.txt) を UTF-8 で
   => java.nio.charset.MalformedInputException: Input length = 1
/dev/full に書き込む
   => java.io.IOException: No space left on device

結果から読み取れるポイントです。

  • Files のメソッド(java.nio.file)は、原因ごとに別のクラスを投げます。クラス名だけで「ない」「権限がない」「すでにある」が区別できます
  • FileInputStream など java.io のクラスは、どの原因でも FileNotFoundException です。原因はメッセージのかっこの中(No such file or directory、Permission denied、Is a directory)で見分けます
  • Files.writeString(work/nodir/b.txt) のように、書き込みでも NoSuchFileException が出ます。途中のフォルダがないためです
  • 権限の例は、管理者(root)で実行すると権限の確認を通過して「例外なし」になります。開発環境で動いて別の環境で AccessDeniedException になるときは、実行ユーザーの違いを確かめます
  • /dev/full は、書き込むと必ず「空きがない」と返す Linux の特別なファイルです。空き容量がないときのエラー文を確かめるのに使えます

直し方の例:フォルダの作成・上書き・文字コード

やりたいこと 書き方
途中のフォルダごと作ってから書き込む Files.createDirectories(path.getParent()); のあと Files.writeString(path, text);
すでにあっても例外にしないフォルダ作成 Files.createDirectories(dir)(createDirectory はすでにあると例外)
コピー先を上書きする Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING)
なくても例外にしない削除 Files.deleteIfExists(path)
Shift_JIS のファイルを読む Files.readString(path, Charset.forName("Shift_JIS"))
読む前に確かめる Files.isRegularFile(path)、Files.isReadable(path)

ストリームのエラー:Stream closed と EOFException

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class StreamErrors {
    interface Action { void run() throws IOException; }

    static void show(String label, Action action) {
        try {
            action.run();
            System.out.println(label + "\n   => 例外なし");
        } catch (IOException e) {
            System.out.println(label + "\n   => " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) throws IOException {
        Path file = Files.createTempFile("demo", ".txt");
        Files.writeString(file, "line1\nline2\n");

        show("閉じた FileInputStream を read()", () -> {
            InputStream in = new FileInputStream(file.toFile());
            in.close();
            in.read();
        });
        show("閉じた BufferedReader を readLine()", () -> {
            BufferedReader reader = Files.newBufferedReader(file);
            reader.close();
            reader.readLine();
        });
        show("try-with-resources の外で readLine()", () -> {
            BufferedReader keep;
            try (BufferedReader reader = Files.newBufferedReader(file)) {
                keep = reader;
            }
            keep.readLine();
        });
        show("閉じた FileOutputStream に write()", () -> {
            FileOutputStream out = new FileOutputStream(file.toFile());
            out.close();
            out.write(1);
        });
        show("2バイトしかないデータを readInt()", () -> {
            DataInputStream in = new DataInputStream(new ByteArrayInputStream(new byte[] {1, 2}));
            in.readInt();
        });
        // データの終わりは例外ではなく、戻り値で知らされる
        InputStream empty = new ByteArrayInputStream(new byte[0]);
        System.out.println("データの終わりで read() => 戻り値 " + empty.read());
        try (BufferedReader reader = Files.newBufferedReader(file)) {
            reader.readLine();
            reader.readLine();
            System.out.println("最後の行の次で readLine() => 戻り値 " + reader.readLine());
        }
        Files.delete(file);
    }
}
閉じた FileInputStream を read()
   => java.io.IOException: Stream Closed
閉じた BufferedReader を readLine()
   => java.io.IOException: Stream closed
try-with-resources の外で readLine()
   => java.io.IOException: Stream closed
閉じた FileOutputStream に write()
   => java.io.IOException: Stream Closed
2バイトしかないデータを readInt()
   => java.io.EOFException: null
データの終わりで read() => 戻り値 -1
最後の行の次で readLine() => 戻り値 null
  • Stream closed は「閉じた後に使った」 という意味です。FileInputStream と FileOutputStream では Stream Closed(C が大文字)、BufferedReader では Stream closed と表示されます
  • よくあるのは、try-with-resources のブロックを出た後で、中で作ったストリームやリーダーを使う形です。ブロックを出た時点で自動的に閉じられています。読み書きはブロックの中で済ませ、外には読み取った結果(文字列やリスト)だけを持ち出します
  • EOFException は、DataInputStream.readInt() のように「決まったバイト数を読む」メソッドが、足りないまま終わりに達したときに出ます。メッセージは null です
  • ふつうの read() や readLine() は、終わりに達しても例外を出さず、-1 や null を返します。ループの終了条件にはこの戻り値を使います

ネットワークのエラーを再現する

確認用に、同じプログラムの中で2つのサーバーを立てています。ポート 9040 は「接続は受けるが何も返さない」、9041 は「接続を受けたらすぐ強制切断する」、9042 は誰も待ち受けていないポートです。

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class NetErrors {
    interface Action { void run() throws Exception; }

    static void show(String label, Action action) {
        try {
            action.run();
            System.out.println(label + "\n   => 例外なし");
        } catch (IOException e) {
            System.out.println(label + "\n   => " + e.getClass().getName() + ": " + e.getMessage());
        } catch (Exception e) {
            System.out.println(label + "\n   => (IOException 以外) " + e);
        }
    }

    public static void main(String[] args) throws Exception {
        // 確認用のサーバー1:接続は受けるが、何も返さない(ポート 9040)
        ServerSocket silent = new ServerSocket(9040);
        // 確認用のサーバー2:接続を受けたらすぐに強制切断する(ポート 9041)
        ServerSocket reset = new ServerSocket(9041);
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    Socket s = reset.accept();
                    s.setSoLinger(true, 0);   // 閉じるときに RST を送る
                    s.close();
                }
            } catch (IOException ignored) {
                // サーバーを閉じたら終了
            }
        });
        t.setDaemon(true);
        t.start();

        show("存在しないホスト名", () -> InetAddress.getByName("no-such-host.invalid"));
        show("誰も待ち受けていないポートに接続", () -> new Socket("127.0.0.1", 9042).close());
        show("応答が来ない相手を1秒で打ち切る(読み取り)", () -> {
            try (Socket s = new Socket("127.0.0.1", 9040)) {
                s.setSoTimeout(1000);
                s.getInputStream().read();
            }
        });
        show("相手に強制切断された後で read()", () -> {
            try (Socket s = new Socket("127.0.0.1", 9041)) {
                Thread.sleep(200);
                InputStream in = s.getInputStream();
                in.read();
            }
        });
        show("閉じた Socket を使う", () -> {
            Socket s = new Socket("127.0.0.1", 9040);
            s.close();
            s.getInputStream();
        });
        show("同じポートでもう1つ待ち受ける", () -> new ServerSocket(9040).close());
        show("HttpClient:応答が来ない相手を1秒で打ち切る", () -> {
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:9040/"))
                    .timeout(Duration.ofSeconds(1)).build();
            client.send(request, HttpResponse.BodyHandlers.ofString());
        });
        silent.close();
        reset.close();
    }
}
存在しないホスト名
   => java.net.UnknownHostException: no-such-host.invalid: Name or service not known
誰も待ち受けていないポートに接続
   => java.net.ConnectException: Connection refused
応答が来ない相手を1秒で打ち切る(読み取り)
   => java.net.SocketTimeoutException: Read timed out
相手に強制切断された後で read()
   => java.net.SocketException: Connection reset
閉じた Socket を使う
   => java.net.SocketException: Socket is closed
同じポートでもう1つ待ち受ける
   => java.net.BindException: Address already in use
HttpClient:応答が来ない相手を1秒で打ち切る
   => java.net.http.HttpTimeoutException: request timed out

早見表のとおり、どの段階で止まったかがクラス名とメッセージに表れます。名前を引く段階なら UnknownHostException、接続の段階なら Connection refused、接続後に応答を待つ段階なら Read timed out、通信の途中で切れたら Connection reset です。Socket is closed は自分で閉じたソケットを使ったときに出ています。

待つ時間の上限を設定していない読み取りは、応答が来るまで待ち続けます。 Socket なら setSoTimeout(ミリ秒)、HttpClient なら HttpRequest.newBuilder(...).timeout(Duration.ofSeconds(秒)) で上限を決めておくと、上のように例外として知らせてくれます。

UncheckedIOExceptionの正体と扱い方

UncheckedIOException は、IOException を包んで運ぶための非検査例外です。API ドキュメントには「IOException を非検査例外でラップする」とあり、Java 8 で追加されました。コンストラクタは必ず IOException を受け取り、getCause() の戻り値の型も IOException です。

なぜ必要かというと、Stream の forEach や map に渡すラムダは、検査例外を投げられないからです。Files.lines は、ファイルを開くところまでは IOException を投げますが、Stream として読み進めている途中の入出力エラーは UncheckedIOException に包んで知らせます。Files.list や Files.walk も同じ仕組みです。

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

public class Unchecked {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("work/sjis2.txt");
        Files.createDirectories(file.getParent());
        Files.write(file, "1行目\nこんにちは\n".getBytes("Shift_JIS"));

        // Files.lines() を開くところまでは IOException(検査例外)
        try (Stream<String> lines = Files.lines(file)) {
            // 読み進めている途中の入出力エラーは UncheckedIOException で届く
            lines.forEach(System.out::println);
        } catch (UncheckedIOException e) {
            System.out.println("受け取った例外: " + e.getClass().getName());
            System.out.println("getMessage  : " + e.getMessage());
            IOException cause = e.getCause();          // 戻り値の型は IOException
            System.out.println("getCause    : " + cause.getClass().getName() + ": " + cause.getMessage());
        }
    }
}
受け取った例外: java.io.UncheckedIOException
getMessage  : java.nio.charset.MalformedInputException: Input length = 1
getCause    : java.nio.charset.MalformedInputException: Input length = 1

1行目は Shift_JIS のバイト列を UTF-8 として読めないため、何も表示されないまま例外になっています。catch (IOException e) だけでは、この例外は受け取れません(親子関係がないため)。Files.lines を使うときは、IOException と UncheckedIOException の両方を考えておきます。

ラムダの中で IOException を包み、外で取り出す

ラムダの中で IOException を投げるメソッドをそのまま呼ぶと、コンパイルエラーになります。

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class LambdaError {
    public static void main(String[] args) {
        List<Path> paths = List.of(Path.of("work/x.txt"));
        List<String> texts = paths.stream()
                .map(p -> Files.readString(p))
                .toList();
        System.out.println(texts);
    }
}
LambdaError.java:9: error: unreported exception IOException; must be caught or declared to be thrown
                .map(p -> Files.readString(p))
                                          ^
1 error

main に throws IOException を足しても、このエラーは消えません。ラムダは Function の apply メソッドの中身として扱われ、apply には throws がないためです。そこで、ラムダの中で UncheckedIOException に包み、Stream の外で getCause() を投げ直して IOException に戻します。

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;

public class LambdaWrap {
    /** IOException を投げる処理を表すインターフェース。 */
    @FunctionalInterface
    interface IOFunction<T, R> {
        R apply(T t) throws IOException;
    }

    /** IOException を UncheckedIOException に包んで、Function として渡せるようにする。 */
    static <T, R> Function<T, R> unchecked(IOFunction<T, R> f) {
        return t -> {
            try {
                return f.apply(t);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    /** 外側では IOException に戻して投げ直す。 */
    static List<String> readAll(List<Path> paths) throws IOException {
        try {
            return paths.stream()
                    .map(unchecked(Files::readString))
                    .toList();
        } catch (UncheckedIOException e) {
            throw e.getCause();
        }
    }

    public static void main(String[] args) throws IOException {
        Files.createDirectories(Path.of("work"));
        Files.writeString(Path.of("work/x.txt"), "X");
        Files.writeString(Path.of("work/y.txt"), "Y");

        System.out.println(readAll(List.of(Path.of("work/x.txt"), Path.of("work/y.txt"))));

        try {
            readAll(List.of(Path.of("work/x.txt"), Path.of("work/zzz.txt")));
        } catch (NoSuchFileException e) {
            System.out.println("ファイルが見つかりません: " + e.getFile());
        }
    }
}
[X, Y]
ファイルが見つかりません: work/zzz.txt
  • unchecked(...) は一度書けば使い回せます。.map(unchecked(Files::readString)) のように、メソッド参照をそのまま包めます
  • getCause() の戻り値が IOException 型なので、キャストなしで throw e.getCause(); と書けます
  • 外に出た後は、ふつうの IOException(ここでは NoSuchFileException)として、呼び出し元が種類ごとに受け取れます

catchを書く順番:子クラスを先に

catch は上から順に当てはめられます。親クラスの IOException を先に書くと、後ろの子クラスには届かなくなるため、コンパイルエラーになります。

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class CatchOrder {
    public static void main(String[] args) {
        try (FileReader reader = new FileReader("work/none.txt")) {
            System.out.println(reader.read());
        } catch (IOException e) {
            System.out.println("入出力エラー: " + e.getMessage());
        } catch (FileNotFoundException e) {
            System.out.println("ファイルが見つかりません: " + e.getMessage());
        }
    }
}
CatchOrder.java:11: error: exception FileNotFoundException has already been caught
        } catch (FileNotFoundException e) {
          ^
1 error

「FileNotFoundException はすでに捕捉されている」という意味です。子クラスを先に、親クラスを後に並べ替えると解消します。| で並べるマルチキャッチでも、親子関係にあるクラスは並べられません。

MultiCatch.java:9: error: Alternatives in a multi-catch statement cannot be related by subclassing
        } catch (FileNotFoundException | IOException e) {
                                         ^
  Alternative FileNotFoundException is a subclass of alternative IOException
1 error

この場合は catch (IOException e) だけで両方を受け取れます。

コピペ用:種類ごとに分けて受け取る

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.MalformedInputException;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.stream.Stream;

public class CatchOrderFixed {
    static void printAll(String name) {
        try (Stream<String> lines = Files.lines(Path.of(name))) {
            lines.forEach(System.out::println);
        } catch (NoSuchFileException e) {            // 子クラスを先に
            System.out.println("ファイルが見つかりません: " + e.getFile());
        } catch (AccessDeniedException e) {
            System.out.println("読み取る権限がありません: " + e.getFile());
        } catch (IOException e) {                    // 親クラスは後に
            System.out.println("入出力エラー: " + e);
        } catch (UncheckedIOException e) {           // 読み進めている途中のエラー
            if (e.getCause() instanceof MalformedInputException) {
                System.out.println("文字コードが UTF-8 ではないようです: " + name);
            } else {
                System.out.println("読み取り中の入出力エラー: " + e.getCause());
            }
        }
    }

    public static void main(String[] args) throws IOException {
        Files.createDirectories(Path.of("work"));
        Files.writeString(Path.of("work/ok.txt"), "1行目\n2行目\n");
        Files.write(Path.of("work/sjis3.txt"), "こんにちは\n".getBytes("Shift_JIS"));

        printAll("work/ok.txt");
        printAll("work/none.txt");
        printAll("work");
        printAll("work/sjis3.txt");
    }
}
1行目
2行目
ファイルが見つかりません: work/none.txt
読み取り中の入出力エラー: java.io.IOException: Is a directory
文字コードが UTF-8 ではないようです: work/sjis3.txt

3つ目の printAll("work") では、フォルダを Files.lines に渡した結果、開く段階ではなく読み進める段階で Is a directory が UncheckedIOException として届いています。UncheckedIOException の catch があるおかげで受け取れました。

catch を並べるときの目安です。

方針 理由
子クラスを先、IOException を後 逆にするとコンパイルエラー
対応を変えたい種類だけ個別に catch する 「ない → 新しく作る」「権限 → 案内を出す」のように処理が違うものだけ分ける
残りは catch (IOException e) でまとめる 子クラスは多く、すべてを列挙する必要はない
FileSystemException の子は e.getFile() で対象のパスを取れる メッセージを自分で組み立てるときに使う

原因を探す手順

  1. 例外のクラス名を見る。IOException とだけ書かれていたらメッセージを、子クラスならクラス名を早見表で引く
  2. メッセージのパス・ホスト名を見る。相対パスなら、実行時の作業フォルダ(System.getProperty("user.dir"))からの位置になる
  3. Caused by: があれば一番下までたどる。UncheckedIOException や、ライブラリが包み直した例外の中に元の IOException がある
  4. プログラムの外を確かめる。ファイルの有無と権限(ls -l)、空き容量(df -h)、相手のサービスの起動とポート

よくある質問

Q. IOException と FileNotFoundException は、どちらを catch すればよいですか? A. 「ファイルがないときは既定値を使う」のように対応を変えたいときだけ FileNotFoundException(Files を使うなら NoSuchFileException)を先に catch し、そのあとに IOException を書きます。対応が同じなら IOException だけで十分です。

Q. throws IOException と try-catch は、どう使い分けますか? A. そのメソッドの中で意味のある対応(別の値を使う、やり直す、利用者に案内する)ができるなら try-catch、できないなら throws IOException で呼び出し元に任せます。catch して何もしない書き方は、原因が見えなくなるので避けます。

まとめ

  • IOException は入出力の例外の親クラス。原因は子クラスの名前とメッセージに出る
  • Files のメソッドは原因ごとに別のクラス(NoSuchFileException、AccessDeniedException、FileAlreadyExistsException、DirectoryNotEmptyException)。java.io のクラスは FileNotFoundException のかっこの中を読む
  • Stream closed は閉じた後の利用。読み書きは try-with-resources の中で済ませる
  • ネットワークは段階で見分ける。名前が引けない(UnknownHostException)、接続できない(Connection refused)、応答が来ない(Read timed out)
  • UncheckedIOException は IOException を包んだ非検査例外。catch (IOException e) では受け取れず、getCause() で中身を取り出す
  • ラムダの中では UncheckedIOException に包み、外で throw e.getCause(); で IOException に戻す
  • catch は子クラスを先に、IOException を後に。逆はコンパイルエラーになる

参考・出典

  • Java SE 21 API ドキュメント「IOException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/IOException.html
  • Java SE 21 API ドキュメント「UncheckedIOException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/UncheckedIOException.html
  • Java SE 21 API ドキュメント「Files」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java IOExceptionの原因と対処|種類一覧とエラー文別の直し方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?