Java7 の例外ハンドリングの改善による非互換性

ひとつ前の記事で紹介した Java SE 7 and JDK 7 Compatibility - Oracle のなかに、面白い非互換性がありました。

Synopsis: Improved Exception Handling May Cause source Incompatibility



概要: 例外ハンドリングの改善が原因のソース非互換性

Java SE 7 and JDK 7 Compatibility

どういういこと?

まずは前提になる話から。
Java7 で変更された言語仕様に Rethrowing Exceptions with More Inclusive Type Checking(例外再スロー時のより包含的な型チェック) というものがあります。


この変更によって、Java7 だと以下のようなコードがコンパイルできるようになりました。*1

    static class FirstException extends Exception { }
    static class SecondException extends Exception { }

    public void rethrowException(String exceptionName) 
                    throws FirstException, SecondException {
        try {
            if (exceptionName.equals("First")) {
                throw new FirstException();
            } else {
                throw new SecondException();
            }
        } catch (Exception e) {
            throw e;
        }
    }

この例では、FirstException と SecondException をまとめて処理するために、その上位の型である Exception でキャッチしています。
そのうえで、その Exception をそのまま呼び元のメソッドに再スロー( throw e; )しています。
このとき、再スローする e の型は Exception なので、メソッドとしては throws Exception と宣言しておく必要がありました。


ただ、発生する可能性のあるキャッチ例外は FirstException か SecondException なのに、呼び元では Exception になっているのは不自然です。
そこで、Java7 では catch(Exception e) と宣言されていても、throw e; では実際に try 文の中で発生する可能性のあるキャッチ例外の型をそのまま引き継ぐようになり、throws FirstException, SecondException と宣言できるようになりました。

変更による弊害

一方で、この言語仕様の変更には弊害もありました。
それが先ほどの話で、Java6 ではコンパイルできていたコードが、Java7 でコンパイルできなくなるという非互換です。

Description: The following example shows two source incompatibilities:


class Foo extends Exception {}
class SonOfFoo extends Foo {}
class DaughterOfFoo extends Foo {}
...
try {
throw new DaughterOfFoo();
} catch (final Foo exception) {
try {
throw exception; // used to throw Foo, now throws DaughterOfFoo
} catch (SonOfFoo anotherException) {
// Reachable?
}
}


The first incompatibility is that the throw exception; statement throws a Foo exception in JDK 6, but throws a DaughterOfFoo exception in Java SE 7.
The second incompatibility is that the catch (SonOfFoo ...) statement compiled under JDK 6 but, under Java SE 7, gets the following error:


error: exception SonOfFoo is never thrown in body of corresponding try statement


Such code is likely to be rare, but should be fixed by removing the non-reachable statements.



概要: 以下の例は、2つのソースの互換性の問題を示しています。
(中略)
1つ目の非互換性は、throw exception; 文が JDK 6 だと Foo exception をスローするのに、Java SE 7 だと DaughterOfFoo exception をスローするということです。
2つ目の非互換性は、catch (SonOfFoo ...) 文が JDK 6 だとコンパイルできるのに、Java SE 7 だと次のエラーになるということです。


エラー: 例外SonOfFooは対応するtry文の本体ではスローされません


このようなコードは稀である可能性が高いですが、非到達可能な文を削除することで修正されるべきです。

Java SE 7 and JDK 7 Compatibility

「 throw exception; 」という文で、exception の型は Java 6 だと宣言通り Foo だったのに、Java 7 だと DaughterOfFoo という型とみなされるようになりました。
そうなると、DaughterOfFoo と継承関係のない SonOfFoo が発生することはないので、それをcatch (SonOfFoo anotherException) というようにキャッチしようとしても到達不能でエラーになる、ということだそうです。


とはいえ、エラーになっている個所はもともと到達不能なコードなので、消してしまって問題はないはずです。
なので、ソースコード非互換ですが、そのままになったようです。

ほかにも…

コンパイラのバグを直したせいでコンパイルできなくなるパターンもあるそうです*2
読んでいて、言語仕様の勉強になりましたw

*1:Java6 だとコンパイルエラーです。

*2:といっても、こちらも稀なパターンだと思います。