Javaのプログラムでは、メソッドは、スローされるすべてのチェック例外を、try-catch ブロックで処理するか、あるいは、(throws 節で)メソッドの外に伝播するように宣言するか、いずれかの方法で解決することが求められる。残念ながら、宣言されていないチェック例外を実行時に発生させる方法が存在する。それらの方法は、メソッド呼出しから伝播してくる可能性のあるすべてのチェック例外を、throws 節を使って調べることを不可能にしてしまう。それゆえ、そのような手法を使って、宣言されていないチェック例外をスローしてはいけない。
違反コード (Class.newInstance())
以下の違反コード例では、宣言されていないチェック例外をスローしている。undeclaredThrow() メソッドは Throwable を引数にとり、その引数を宣言なしにスローするメソッドを呼び出している。undeclaredThrow() は、メソッドがスローすると宣言している例外はどれもキャッチしているが、宣言された例外であるか否かに関係なく、引数に渡された例外をスローする。このコード例は「ERR07-J. RuntimeException, Exception, Throwable をスローしない」にも違反している。 しかし、「ERR08-J. NullPointerException およびその親クラスの例外をキャッチしない」については、ERR08-EX0 に該当するため、違反にはならない。Class.newInstance() は InstantiationException と IllegalAccessException のみをスローすると宣言されているが、実際には、java.lang.Class.newInstance()のデフォルトのコンストラクタによってスローされるチェック例外がすべて、呼出し元に伝播する。この違反コード例は、Class.newInstance() を使って任意のチェック例外やそれ以外の例外をスローする方法の1つを示している。
public class NewInstance { private static Throwable throwable; private NewInstance() throws Throwable { throw throwable; } public static synchronized void undeclaredThrow(Throwable throwable) { // これらの例外は引数として渡してはならない if (throwable instanceof IllegalAccessException || throwable instanceof InstantiationException) { // 未チェックにつき宣言は不要 throw new IllegalArgumentException(); } NewInstance.throwable = throwable; try { // 次の行は上で渡された Throwable 引数をスローする // class.newInstance の throws 節では // これが発生するとは宣言されていない NewInstance.class.newInstance(); } catch (InstantiationException e) { /* 到達不能 */ } catch (IllegalAccessException e) { /* 到達不能 */ } finally { // メモリリークを防ぐ NewInstance.throwable = null; } } } public class UndeclaredException { public static void main(String[] args) { // 宣言されていないチェック例外 NewInstance.undeclaredThrow( new Exception("Any checked exception")); } }
違反コード (Class.newInstance() Workarounds)
宣言されていないチェック例外をキャッチして処理したい場合であっても、コンパイラは、特定の状況で、どんな例外もスローされうるとは判断しない。
以下の違反コード例では、Class.newInstance() によってスローされる、宣言されていないチェック例外をキャッチしようとしている。Exception をキャッチし、それが想定していたチェック例外のインスタンスであるかどうかを動的にチェックしている(それ以外の例外は再度スローする)。
public static void main(String[] args) { try { NewInstance.undeclaredThrow( new IOException("Any checked exception")); } catch (Throwable e) { if (e instanceof IOException) { System.out.println("IOException occurred"); } else if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { // 実行をハンドラーに移す } } }
適合コード (Constructor.newInstance())
以下の適合コードでは、Class.newInstance() の代わりに java.lang.reflect.Constructor.newInstance() を使っている。Constructor.newInstance() メソッドはコンストラクタの中からスローされた例外を InvocationTargetException というチェック例外にラップする。
public static synchronized void undeclaredThrow(Throwable throwable) { // これらの例外は引数として渡してはならない if (throwable instanceof IllegalAccessException || throwable instanceof InstantiationException) { // 未チェックにつき宣言は不要 throw new IllegalArgumentException(); } NewInstance.throwable = throwable; try { Constructor constructor = NewInstance.class.getConstructor(new Class<?>[0]); constructor.newInstance(); } catch (InstantiationException e) { /* 到達不能 */ } catch (IllegalAccessException e) { /* 到達不能 */ } catch (InvocationTargetException e) { System.out.println("Exception thrown: " + e.getCause().toString()); } finally { // メモリリークを防ぐ NewInstance.throwable = null; } }
違反コード (sun.misc.Unsafe)
以下の違反コード例はセキュアではない。宣言していないチェック例外をスローする可能性があり、sun.misc.Unsafe クラスを使っているからである。すべての sun.* クラスは可搬性と後方互換性に問題があり、サポートされておらず、ドキュメントも存在しない。
ブートストラップクラスローダによってロードされるクラスは static ファクトリメソッドである Unsafe.getUnsafe() を呼び出すために必要なパーミッションを持つ。システムプロパティ sun.boot.class.path を変更せずにブートストラップクラスローダで任意のクラスをロードさせるのは難しい。しかし、アクセスを得る別の方法として、リフレクションの機能を使い、Unsafe のインスタンスを保持しているフィールドのアクセス制限を変更する、という方法がある。これは、実行中のセキュリティマネージャが許可する場合にのみ使える方法である(ただし「ENV03-J. 危険な組み合わせのパーミッションを割り当てない」に違反する)。Unsafe にアクセスできるプログラムは、Unsafe.throwException() メソッドを使うことで、宣言されていないチェック例外をスローできる。
import java.io.IOException; import java.lang.reflect.Field; import sun.misc.Unsafe; public class UnsafeCode { public static void main(String[] args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe u = (Unsafe) f.get(null); u.throwException(new IOException("No need to declare this checked exception")); } }
違反コード (Generic Exception)
例外の宣言を引数とするジェネリック型を無チェックキャストすると、予期せぬチェック例外の発生につながることがある。そのような形のすべてのキャストは、警告メッセージが抑制されていないかぎり、コンパイラによって検出される。
interface Thr<EXC extends Exception> { void fn() throws EXC; } public class UndeclaredGen { static void undeclaredThrow() throws RuntimeException { @SuppressWarnings("unchecked") // 警告を抑制 Thr<RuntimeException> thr = (Thr<RuntimeException>)(Thr) new Thr<IOException>() { public void fn() throws IOException { throw new IOException(); } }; thr.fn(); } public static void main(String[] args) { undeclaredThrow(); } }
違反コード (Thread.stop(Throwable))
Java API のクラス Thread の説明には以下のように書かれている[API 2006]。
[Thread.stop()] は、対象とするスレッドの処理準備ができていないという例外(このメソッドがなかったならスレッドがスローできないはずの、チェック例外を含む)の生成に使用されることがある。たとえば、以下のメソッドの動作は、Java の throw 操作と同じであるが、コンパイラは、呼び出し側メソッドがスローする可能性のあるチェック例外がすべて宣言されているかどうかを検証しない。
static void sneakyThrow(Throwable t) { Thread.currentThread().stop(t); }
Thread.stop() は非推奨であり、このコードは「MET02-J. 非推奨(deprecated)あるいは廃止された(obsolete)クラスやメソッドを使用しない」に違反している。
違反コード (バイトコードの改変)
クラスを逆アセンブルし、チェック例外の宣言を削除し、再度アセンブルすることで、実行時にクラスが使われるときにチェック例外をスローすることが可能である[Roubtsov 2003]。チェック例外を宣言するクラスをコンパイルし、実行時にその宣言を削除したクラスをロードさせることで、宣言されていないチェック例外を発生させることもできる。また、sun.corba.Bridge クラスを巧妙に使用することで、宣言されていないチェック例外の発生を引き起こすことができる。これらの手法はすべてこのルールへの違反となる。
リスク評価
宣言されていないチェック例外をドキュメントしないと、呼出し側で処理する準備ができていないチェック例外の発生を招き、安全性が保たれなくなる。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
ERR06-J | 低 | 低 | 高 | P1 | L3 |
関連ガイドライン
MITRE CWE | CWE-703. Improper check or handling of exceptional conditions |
CWE-248. Uncaught exception |
参考文献
[Bloch 2008] | Item 2. Consider a builder when faced with many constructor parameters |
[Goetz 2004b] | |
[JLS 2005] | Chapter 11, Exceptions |
[Roubtsov 2003] | |
[Schwarz 2004] | |
[Venners 2003] | Scalability of Checked Exceptions |
翻訳元
これは以下のページを翻訳したものです。
ERR06-J. Do not throw undeclared checked exceptions (revision 87)