🐙

例外処理の基本<後編>[Java入門]

に公開

はじめに

こんにちは。
プログラミング初心者wakinozaと申します。
Java勉強中に調べたことを記事にまとめています。

十分気をつけて執筆していますが、なにぶん初心者が書いた記事なので、理解が浅い点などあるかと思います。
記事を参考にされる方は、初心者の記事であることを念頭において、お読みいただけると幸いです。
間違い等あれば、指摘いただけると助かります。

対象読者

  • Javaを勉強中の方
  • Java Silver試験を勉強中の方
  • Javaの例外処理について知りたい方

目次

1. try-with-resources
2. throws
3. throw

本文

1. try-with-resources

前回の記事で、javaプログラムの不具合3種、例外3種、例外処理の基本であるtry-catch-finally文を説明しました。

https://zenn.dev/wakinoza/articles/218665d7c33031

例外が起きても起こらなくても必ず実行しなければならない処理は、finallyブロックに記載すると説明しました。しかし、finallyブロックは必須でないため、うっかり記述し忘れてもコンパイルエラーにはなりません。もし、リソースのクローズ処理を記述し忘れると、「リソースリーク」を起こす可能性があります。

「リソースリーク」とは、リソースの閉じ忘れによって起こるプログラムのトラブルのことです。
ファイル・ネットワーク・データベースなどのプログラム外部の資源を「リソース」と言います。Java言語で、リソースを利用する場合はリソースを抽象化したリソースオブジェクトを使用します。リソースオブジェクトは、クローズ処理が行われない限り、リソースは解放されせん。もしクローズ処理を忘れると、リソースが占有され続けてしまいます。これが「リソースリーク」です。リソースリークとなると、新しいリソースオブジェクトを生成できなくなったり、システムのパフォーマンスが低下したり、メモリ不足によってプログラムが中断したり、などの問題が発生します。

リソースリークを防ぐために導入されたのが、try-with-resources構文です。

try-with-resources構文の主な特徴は、tryブロックの丸括弧内にクローズ処理が必要なリソースの生成が宣言できることです。
tryブロックの丸括弧で宣言されたリソースオブジェクトは、例外が起きても起きなくても、tryブロックを抜ける際に、自動的にクローズ処理が呼び出されます。
このクローズ処理はtryブロックを抜けた直後、catchブロックやfinallyブロックの実行前に行われます。例外が発生してもしなくても、クローズ処理のタイミングは変わりません。

また、try-with-resources構文を利用すると、クローズ処理を省略できるため、コードが簡潔になります。

まとめると、try-with-resources構文を利用することで、リソースリークを防ぐことができ、コードもより簡潔になるのです。

ちなみに、tryブロックの丸括弧内に記述できるリソースオブジェクトは、java.lang.AutoCloseableインターフェースを実装しているものに限られます。しかし、標準ライブラリの主要なリソースオブジェクトは、基本的にAutoCloseableを実装しているため、問題なく利用できます。

次に記述方法を見ていきましょう。
try-with-resources構文の記述方法は、以下の通りです。

try (クローズ処理が必要なリソースオブジェクトの変数の宣言){
  例外が起こるかもしれない処理
} catch (例外クラス e){
  例外が発生した時の処理
} finally {
  例外が起こっても起こらなくても実施したい処理
}

try-with-resources構文では、catchブロックを複数書くこともできますが、省略することも可能です。finallyブロックは1つだけ書くことができますが、省略も可能です。

次に具体的なコード例です。
同じ処理をtry-catch-finally構文で記述したものと、try-with-resources構文と記述したものとを比べて、両者の違いを確認します。
まずは、try-catch-finally構文から見ていきます。

//try-catch-finally構文

import java.io.*;

public class Main{
  public static void main(String[] args){
    FileWriter fw = null;
    try{
      fw = new FileWriter("data.txt");
      fw.write("hello!");
    } catch (Exception e){
      System.out.println("例外が発生しました" + e.getMessage());
    } finally {
      if (fw != null) {
        try {
          fw.close();
        } catch (IOException e){
          System.out.println("例外が発生しました" + e.getMessage());
        }
      }
    }
  }
}

ファイル操作のリソースオブジェクトのインスタンスを生成し、ファイルに"hello!"と書き込んでいます。ファイル操作はチェック例外を発生される可能性があるため、例外処理が必要です。また、ファイル操作の最後にはクローズ処理が必要になります。クローズ処理は、例外が起きても起きなくても必ず実行しなければならないため、finallyブロックに記述しています。しかし、クローズ処理であるclose()メソッドも例外処理が必要なので、finallyブロック内にさらにtry-catchする必要があります。そのため、コードがやや冗長となっています。

一方、同様の処理をtry-with-resources構文で記述してみましょう。

//try-with-resources構文

import java.io.*;

public class Main{
  public static void main(String[] args){
    try (FileWriter fw = new FileWriter("data.txt");){
      fw.write("hello!");
    } catch (Exception e){
      System.out.println("例外が発生しました" + e.getMessage());
    }
  }
}

上のtry-catch-finally構文と同様の処理をしていますが、try-with-resources構文の方が簡潔になっています。

クローズ処理を伴うリソースオブジェクトを利用する際は、特別な理由がない限り、try-with-resources構文で記述する方が良いでしょう。

2. throws

これまで、try-catch-finally構文やtry-with-resources構文などの例外処理を紹介してきました。これらはメソッド内の例外を、メソッド内で処理する方法でした。

しかし、実際のプログラムでは、メソッドが別のメソッドを呼び出すという繋がりがあります。
仮に、メソッドAがメソッドBを呼び出し、メソッドBがさらにメソッドCを呼び出すといった繋がりがあるとします。この時、メソッドCで例外が発生した場合、JVMの内部ではどのようなことが起こるでしょうか?

実は、あるメソッドで例外が発生し、メソッド内で例外がcatchされない場合は、呼び出し元のメソッドに例外が伝播する仕組みになっています。
先ほどの例では、メソッドCに例外が起こり、メソッドC内で例外がcatchされない場合は、メソッドBに例外処理が委ねられます。メソッドBでも例外がcatchされないと、さらにメソッドAに例外処理が委ねられます。
このように、例外がcatchされない限り、メソッドの呼び出し元に例外処理が委譲され続けていく現象を「例外の伝播」と言います。
もし、伝播した例外がmainメソッドでもcatchされないと、プログラムは強制終了してしまいます。

このような「例外の伝播」を防ぐため、Java言語ではチェック例外には例外処理を強制する仕様となっています。しかし、この例外処理は、例外が発生したメソッドではなく、その呼び出し元メソッドで行っても構わないのです。

状況によっては、呼び出し元のメソッドに例外処理をお願いしたい場合があります。
例えば、あるメソッドが複数のメソッドを呼び出していて、呼び出し先のメソッドが同じ例外を起こす可能性がある場合などです。呼び出し先で個別に例外処理をするよりも、呼び出し元でまとめて例外処理した方が、コードが簡潔になり、コードの重複も防げます。

例外が発生したメソッドで例外処理を行わず、メソッドの呼び出し元に例外処理を委ねるシステムがJava言語には備わっています。 それが、「throws」です

例外が発生する可能性のあるメソッドの宣言時に、「throws 例外クラス」と記述することで、「メソッドの呼び出し元に例外をスローする可能性がある」と宣言することができます。
「throws」を宣言している場合は、チェック例外を発生させる可能性のあるメソッドが例外処理を省略しても、コンパイラエラーになりません。
一方、例外をスローするメソッドを呼び出すメソッドは、伝播した例外をcatchする例外処理を記述するか、より前の呼び出し元のメソッドに例外をスローするか、どちらかの対応が強制されます。

次に、throwsの記述方法を見ていきます。

アクセス修飾子 戻り値 メソッド名(引数リスト) throws 例外クラス1,例外クラス2{
  //例外が発生する可能性のある処理
}

例外クラスが複数ある場合は、コンマ(,)で区切ることで、複数宣言することができます。

次に具体的なコードで見ていきましょう。

public class A{
  public static void methodA(){
    try{
      B.methodB();
    }
    catch (IOException e){
      System.out.println("例外が発生しました" + e.getMessage());
    }
  }
}
import java.io.*;

public class B{
  public static void methodB() throws IOException {
    FileWriter fw = new FileWriter("data.txt");
  }
}

BクラスのmethodB()で、チェック例外が発生する可能性のあるファイル処理を行っています。しかし、methodB()はthrowsで例外を伝播する可能性があると宣言しているため、例外処理が強制されません。methodB()内で例外がcatchされないため、例外は呼び出し元であるmethodA()に伝播します。methodA()で例外処理を記述しているため、無事例外がcatchされています。

このように、例外を呼び出し元メソッドに伝播させる場合は、「throws」を宣言することで実現できるのです。

3. throw

例外的状況が発生しているかどうかは、常にJVMが監視しています。そして、JVMが例外を検知すると、自動的に処理がcatchブロックに移行します。
時には、JVMが例外と検知しない状況を、例外として扱ってほしい場合があります。例えば、メソッドの引数が不正である場合、意図的に例外を発生させ、呼び出し元に例外をスローすることで、エラーが発生したことを呼び出し元のメソッドに知らせることができます。

こういった開発者が意図的に例外を発生させたい場合に用いるのが、「throw」です。
例外を発生させたい場所で、throwと例外クラスを記述することで、例外を発生したことをJVMに知らせることができます。このようにJVMに例外の発生を知らせることを、「例外を投げる」もしくは「例外を送出する」と表現します。例外が投げつけられると、JVMはそれを検知し、即座にcatchブロックへの実行や、例外の伝播などの処理を行います。

次に、記述方法を見ていきます。

throw new 例外クラス();
//括弧内にエラーメッセージを書くこともできる

次に具体的なコードを見ていきます。

public class Main {
  public static void main(String[] args){
    Person p = new Person();
    try {
      p.setAge(-10);
    } catch (IllegalArgumentException e) {
      System.err.println("エラー: " + e.getMessage());
    } 
  }
}
public class Person {
  private int age;
 
  public void setAge(int age) throws IllegalArgumentException{ 
    //IllegalArgumentExceptionは非チェック例外なのでthrowsは省略可能
    if (age < 0){ //ここで引数をチェック
      throw new IllegalArgumentException("年齢は0以上を指定してください。指定値= " + age);  //指定値が範囲外なら、例外を投げる
    }
    this.age = age; //引数に問題がなければ、フィールドに値を代入する
  }
}

PersonクラスのsetAge()メソッドでは、引数をフィールドへ代入する前に、不正な値かどうかをチェックしています。もし引数に問題がある場合は、IllegalArgumentExceptionを投げて、JVMに「引数が異常であるため、処理を継続できない」という例外的状況を報告し、呼び出し元に例外をスローすることで異常を知らせます。
このコードでは、setAge()の引数が不正な値であったため、IllegalArgumentExceptionが投げられています。例外はスローされ、呼び出し元のmainメソッドでcatchされています。

このように、開発者が意図的に例外を投げる時に用いるのが、「throw」です。
「throw」は、前の節で紹介した「throws」とよく似ていますが、全く別のものなので注意が必要です。

まとめ

  • 「try-with-resources文」は、リソースのクローズ処理を自動で行うため、クローズ処理忘れによるリソースリークを防ぐのに有効である

  • 「throws」で、呼び出し元メソッドに例外を伝播(スロー)させる可能性があることを宣言することができる。これにより、例外処理の強制は、呼び出し先メソッドではなく、呼び出し元メソッドに移る

  • 「throw」を記述すると、開発者が意図的に例外を発生させ、JVMに例外を投げることができる


記事は以上です。
次回は、例外処理のポリモーフィズムについてまとめる予定です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

この記事は以下の情報を参考にして執筆しました。

GitHubで編集を提案

Discussion