🐈

例外クラスのポリモーフィズム[Java入門]

に公開

はじめに

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

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

対象読者

  • Javaを勉強中の方
  • Java Silver試験を勉強中の方

目次

1. 例外クラスのポリモーフィズム
2. catchブロックを複数記述する場合
3. マルチキャッチの場合
4. throwsが宣言されたメソッドをオーバーライドする場合

本文

1. 例外クラスのポリモーフィズム

java言語において、例外的状況は例外クラスで表現します。
例外「クラス」である以上、スーパークラス・サブクラスといった継承関係が存在しますし、ポリモーフィズムを使う事も可能です。

この記事では、例外クラスを扱う上で継承関係に注意しなければならない状況と注意点をまとめていきます。

  • catchブロックを複数記述する場合
  • マルチキャッチの場合
  • throwsが宣言されたメソッドをオーバーライドする場合

2. catchブロックを複数記述する場合

try-catch-finally構文やtry-with-resources構文では、例外クラスをcatchするためにcatchブロックを記述します。
複数の例外クラスをcatchする場合は、catchブロックを複数記述できます。

catchブロックを複数記述した場合、例外が発生すると、記述した順に一番上のcatchブロックから評価していきます。もし、発生した例外が一番上のcatchブロックの例外クラス(またはそのサブクラス)で補足できない場合、次は1つ下のcatchブロックが評価するという具合に、次々にcatchブロックを評価していく仕組みになっています。

この時、継承関係がある例外クラスを別々にcatchしたい場合は、catchブロックの記述順に注意が必要です。

例外クラスにもポリモーフィズムが適用されるため、スーパークラス側の例外クラスで宣言されたcatchブロックは、そのサブクラス側の例外クラスをcatchすることが可能です。
もし、スーパークラス側の例外クラスを受け取るcatchブロックを、サブクラス側の例外を受け取るcatchブロックより上に記述すると、スーパークラス側もサブクラス側も、スーパークラス側のcatchブロックが受け取ってしまいます。そうなると、サブクラス側の例外クラスを受け取るcatchブロックの記述が実行されることはありません。
Java言語において、到達不可能なコードを記述するとコンパイラエラーになります。そのため、継承関係のある例外クラスをcatchする場合に、スーパークラス側の例外をサブクラス側の例外より上のcatchブロックに記述すると、コンパイラエラーになります。

スーパークラス側の例外をサブクラス側の例外より下のcatchブロックに記述すると、コンパイラエラーになりません。
継承関係のある複数の例外クラスを別のcatchブロックで処理したい場合は、必ず、サブクラス側の例外クラスを上に記述しましょう。

コード例を見ていきます。

public class SubException extends SuperException {}
//コンパイラエラーになるコード例
public class Main {
  public static void main (String[] args) {
    try {
      //anycode
    } catch (SuperException e) { //スーパークラスの例外クラスが上に記述されている
      //anycode
    } catch (SubException e) { //このブロックは到達不可能となる 
      //anycode
    }
  }
}
//コンパイラが通るコード例
public class Main {
  public static void main (String[] args) {
    try {
      //anycode
    } catch (SubException e) { 
      //anycode
    } catch (SuperException e) { 
      //anycode
    }
  }
}

SubExceptionクラスがSuperExceptionクラスを継承しているため、SubException がサブクラス側、SuperExceptionがスーパークラス側であることがわかります。
コンパイラエラーになるコード例では、スーパークラス側の例外クラスが上側のcatchブロックに記述されています。SubExceptionクラスもSuperExceptionクラスも上側のcatchブロックでcatchされてしまうため、下側のcatchブロックが到達不可能コードとなります。コード中に到達不可能コードが発生すると、コンパイラエラーとなります。

コンパイラが通るコード例では、サブクラス側の例外クラスが上側のcatchブロックに、スーパークラス側の例外クラスが下側のcatchブロックに記述されています。これにより、SubExceptionが発生したら上側のcatchブロックが実行され、SuperExceptionが発生したら下側のcatchブロックが実行されます。到達不可能なブロックは存在しないため、コンパイラエラーになりません。

3. マルチキャッチの場合

複数の例外クラスで同じ処理を実行したい場合は、「マルチキャッチ」を利用します。
マルチキャッチとは、複数の例外クラスをまとめてcatchする方法です。catchブロックに例外クラスを記述する際に、「|」で区切ることで、複数の例外クラスを1つのcatchブロックで処理することができます。

コード例を見ていきましょう。

public class Main {
  public static void main (String[] args) {
    try {
      System.out.println(10/0);  //ArithmeticExceptionが発生する
      FileReader rf = new FileReader("text.txt"); //FileNotFoundExceptionが発生する可能性がある
    } catch (FileNotFoundException | ArithmeticException e) { 
      System.out.println("例外が発生しました" + e.getMessage());
    } 
  }
}

上のコードはtryブロック内で2種類の例外が発生する可能性があります。2つの例外クラスに同じ処理で対応する場合、catchブロックを2つ書くとコードが重複します。
マルチキャッチを利用すると、1つのcatchブロックで2つの例外クラスをcatchできます。コードの重複を防ぐことができ、コードが簡潔になります。

便利なマルチキャッチですが、注意点があります。
継承関係がある例外クラスを並べて書くことができないという点です。継承関係がある例外クラスをマルチキャッチすると、コンパイラエラーになります。
継承関係がある複数の例外クラスをまとめてキャッチしたい場合は、スーパークラス側の例外クラスを1つ記述するだけで十分です。

4. throwsが宣言されたメソッドをオーバーライドする場合

メソッドをオーバーライドする際、サブクラス側は様々なルールを守る必要があります。

  • サブクラス側のメソッドのシグネチャは、スーパークラス側と同じであること
  • サブクラス側のメソッドの戻り値は、スーパークラス側の戻り値と同じ型であるか、その型のサブクラス側でなければならない
  • サブクラス側のメソッドのアクセス修飾子は、スーパークラス側のアクセス修飾子と同じか、それよりも公開範囲が広いものでなければならない

throwsが宣言されているメソッドをオーバーライドする場合も同様に、サブクラス側のthrows宣言に守るべきルールが存在します
具体的には以下の3つのルールは存在し、1つでもルールを破るとコンパイラエラーになります。

1, サブクラス側で検査例外をthrowsしたい場合、サブクラス側は、スーパークラスがthrowsしているクラスか、その例外クラスのサブクラスしかthrowsできない
2, サブクラス側でRuntimeException系の非チェック例外をthrowsしたい場合は、スーパークラスのthrows宣言に関係なく、throwsできる
3, スーパークラスのメソッドにthrowsがあっても、サブクラス側でthrowsを宣言しなくても良い

何やらややこしいルールですが、これらはSOLID原則の1つ「リスコフの置換原則(LSP)」に従ったものです。
「LSP」とは、「スーパークラスを使うコードは、サブクラス追加の影響を受けてはいけない」という原則です。言い換えると、スーパークラスが使われている箇所を、サブクラスに置き換えても、プログラムが正しく動き続けなければならない、とも言えます。LSPは、ポリモーフィズムを支える重要な原則の1つです。
ポリモーフィズムによって、スーパークラスの変数にサブクラスのインスタンスへの参照を代入することができます。それは、スーパークラスのインスタンスを利用する部分に、サブクラスのインスタンスも代入可能ということでもあります。もしスーパークラスのインスタンスを利用するコードにサブクラスのインスタンスを代入して、予期せぬ挙動やエラーが発生してしまうとしたら、危なっかしくてポリモーフィズムを扱うことはできなくなります。
LSPを遵守することで、ポリモーフィズムの安全性を向上させているのです。

まずは、オーバーライド時のルール1について、コードで確認していきます。

class ExceptionA extends Exception{}
class ExceptionB extends Exception{}
public class Super{
  public void method() throws ExceptionA{
    throw new ExceptionA();
  }
}
public class Sub extends Super{
  public void method() throws ExceptionB{
    throw new ExceptionB();
  }
}
public class Main {
  public static void main(String[]args){
    Super sup = new Super();
    someMethod(sup); // Super.method() が呼ばれる

    Sub sub = new Sub();
    someMethod(sub); // Sub.method() が呼ばれる
  }

  public static void someMethod(Super obj) {
    try {
        obj.method();
    } catch (ExceptionA e) {
        // ExceptionA の処理
    }
 }
}

MainクラスのsomeMethod()はSuperクラスのインスタンスを引数として受け取り、引数のインスタンスのmethod()を実行します。Superのmethod()はExceptionAをthrowsすると宣言しているため、ExceptionAをcatchする例外処理も記述しています。
一方、MainクラスではSubクラスのインスタンスが生成されています。SubクラスはSuperクラスと継承関係にあるため、someMethod(Super obj)の引数にSubクラスのインスタンスを入れる事も可能です。
しかし、Superクラスのmethod()はSubクラスでオーバーライドされており、throws宣言が変更されています。 Sub.method() が呼ばれると、ExceptionBがthrowsされます。一方、someMethod()内のcatchブロックはExceptionAしか記述されていません。ExceptionBはExceptionAと継承関係がないため、例外クラスをcatchできないという事態に陥るのです。

しかし、スーパークラス側の例外クラスで宣言されたcatchブロックは、そのサブクラス側の例外クラスをcatchすることができます。
そのため、ルールを遵守し、Subクラスのmethod()が、Superクラスのmethod()と同じ例外クラスかサブクラス側の例外クラスをthrowsしていれば、someMethod()内のcatchブロックで問題なくcatchできるのです。

このルールの利点は、他にもあります。
今後作られる新しいSubクラスもこのルールに対応していれば、メソッドの呼び出し元であるsomeMethod()を修正しなくて良いという点です。コードを修正してもその影響範囲が限定されるということは、ソフトウェアの保守性を高めることに繋がります。

オーバーライド時のルール1を守ることによって、サブクラスインスタンスを利用した際に予期せぬ挙動が起きたり、例外処理が抜け落ちることを防いでいるのです。

次にオーバーライド時のルール2ですが、RuntimeException系の例外は非チェック例外であるため、そもそも例外処理は強制されていません。そのため、オーバーライド時においてもスーパークラスのthrowsに関係なく宣言したり省略したりできるのです。

最後のオーバーライド時のルール3ですが、サブクラス側で例外クラスがthrowsされないのであれば、例外処理が抜け落ちる事もないため、例外処理において問題となりません。

まとめ

  • 複数のcatchブロックで継承関係にある例外クラスをcatchする場合、必ずサブクラス側の例外クラスを上に記述する

  • 1つのcatchブロックで複数の例外クラスをcatchする仕組みをマルチキャッチという。しかし、継承関係にある例外クラス同士では利用できない

  • メソッドをオーバーライドする際、サブクラス側が宣言できるチェック例外は、スーパークラス側がthrows宣言している例外クラス、またはそのサブクラス側に限定される


記事は以上です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

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

GitHubで編集を提案

Discussion