読者です 読者をやめる 読者になる 読者になる

JDK 7の新機能 Peoject Coin編(その2)

前回のつづきです。

複数例外のキャッチ


よく「マルチキャッチ」と呼ばれている機能です。複数の例外を同じcatchブロックでキャッチできるようになりました。これまでは、tryブロックの中で複数の例外が発生する可能性がある場合には、それぞれに対してcatch文を書く必要がありました。catchの中身が同じ処理内容でも個別に書かなければいけないため、ちょっと面倒でした。JDK 7では、複数の例外を"|"でつなげて記述することで、まとめてキャッチすることができます。次のような感じです。

class MyExceptionA extends Exception {}
class MyExceptionB extends Exception {}

public class MultiCatch {
  static void testMultiCatch(int arg) {
    try {
      if (arg == 0) {
        throw new MyExceptionA();
      }
      if (arg == 1) {
        throw new MyExceptionB();
      }
    } catch (MyExceptionA | MyExceptionB ex) {
      System.out.println(ex.getClass() + " がスローされました。");
    }
  }
}

これを、

    testMultiCatch(0);
    testMultiCatch(1);

こんな感じで呼び出すと、

MyExceptionA がスローされました。
MyExceptionB がスローされました。

こうなります。どちらもキャッチできていることが分かります。

次のように、例外クラス同士に継承関係がある場合はどうでしょうか。

class MyExceptionC extends MyExceptionA {}

この場合、次のようにすれば問題ないのですが、

} catch (MyExceptionC | MyExceptionA ex) {

次のようにするとコンパイルエラーになります。

} catch (MyExceptionA | MyExceptionC ex) { // コンパイルエラー

左に書いた例外から先にキャッチしているように扱われるようで、通常の例外処理と同様に親クラスが先に書かれていると、「すでにキャッチされています」と言われます。

例外の安全な再スロー

これまでは、catchブロックで受け取った例外をそのままもう一度throwしたい場合には注意が必要でした。次のような感じにはできなかったわけです。

  static void testRethrow(int arg) throws MyExceptionA {
    try {
      if (arg == 0) {
        throw new MyExceptionA();
      }
    } catch (Exception ex) {
      System.out.println(ex.getClass() + " がスローされました。再スローします。");
      throw ex;    // コンパイルエラー
    }
  }

これを回避するには、testRethrow()メソッドのthrowsにExceptionクラスを追加する必要があります。しかし実際にはスローする例外が変わるわけではないので、メソッド定義には手を加えたくありません。JDK 7では、catch節にfinalを付けて次のように記述することで、メソッド自身に変更を加えなくても例外を再スローできるようになりました。

  static void testRethrow(int arg) throws MyExceptionA, MyExceptionB {
    try {
      if (arg == 0) {
        throw new MyExceptionA();
      }
    } catch (final Exception ex) {
      System.out.println(ex.getClass() + " がスローされました。再スローします。");
      throw ex;
    }
  }

このような再スローを許容するためには、catchブロックの中で受け取った例外が変更されていないことを保証する必要があります("安全な"と言われるのはそのためです)。finalはその検証のために導入されたそうなのですが、現時点ではfinalを付けなくても問題なく実行できてしまいます。生成されたバイトコードを調べてみましたが、finalを付けた場合と付けない場合で特に違いは見られませんでした。
いろいろ調べていたところ、Joseph Darcy氏のブログで次のようなエントリを見つけました。
Oracle Blogs | Homepage

First, as long as a catch parameter is effectively final (in other words not reassigned inside the catch block), the more precise analysis will be enabled if the exception is rethrown. An explicit final modifier will no longer be needed to enable the more precise analysis.

どうも、安全性の分析を正しく行うことによって、明示的なfinal修飾子は必要なくなるということのようです。

Stringによるswitch文の補足

前回、Stringを使うswitch文はJVMには修正を加えずにコンパイラのみで実現すると書きました。JVMのlookupswitchやtableswitchは整数しかサポートしていないので、文字列はどうやって比較するのでしょうか。生成されたクラスファイルを逆コンパイルしてみるとその方法がよくわかります。以下がJava Decompilerで逆コンパイルしてみた結果です。

static void testStringSwitch(String str) {
  String str = str;
  int i = -1;
  switch (str.hashCode()) {
    case 3208229:
      if (!str.equals("hoge")) break;
      i = 0;
      break;
    case 3441359:
      if (!str.equals("piyo")) break;
      i = 1;
  }
  switch (i) {
    case 0:
      System.out.println("ほげほげ");
      break;
    case 1:
      System.out.println("ぴよぴよ");
      break;
    default:
      System.out.println("デフォルト");
  }
}

最初にハッシュコードを調べた上で、それが一致していた場合はさらにequels()で比較し、それも一致していたら結果をintに記録して再度switchで判定しています。equals()だけでもいい気がしますが、hashCode()を使うのはその方が速度が速いからではないかと。