例外の投げ方。作法。たった1つの冴えたやりかた。
RuntimeException(非検査例外)の正しい投げ方は、次の通りです。
- 例外オブジェクトを優しく new してあげましょう
- システムを落とす勢いで、豪快に throw で放り投げましょう
検査例外と非検査例外を使い分ける指針は「呼び出し元にチェックしてもらいたいかどうか」という教科書的な解答に集約されます。チェックされなければそのままシステム全体が落ちるという形です。
web サーバを作りました。クライアントと接続します。クライアント側の LAN ケーブルが猫によって引き抜かれました。サーバ側で例外が飛びます。
そして見事にサーバ全体が落ちました。めでたしめでたし。
で終わりたくないので、良い API は【お節介にも】検査例外を使うという選択を行います。その心は
- 『残念ながら君の望みは叶えられなかった。だが私は君に、もう1度やりなおすチャンスを与えよう』
……と、なんか生意気な野郎になったけど、こんな感じ。
- 『残念ながら君の望んだことは行えなかったし、不慮の事故だと思って諦めてくれ。このままシステムを暴走させるのもなんだし、システムをHaltさせることをおすすめするよ』
になると、非検査例外をスローするという選択になる。
逆に例外の受け手側のことを考える。
- 『僕が望んだことは行えなかった。このままシステムを暴走させるのもなんだし、システムをHaltさせることをおすすめするよ』
になると、検査例外も非検査例外も同様に呼び出し元へとスローされる。
- 『僕が望んだことは行えなかった。だけど僕は諦めない。まだ戦える』
になると、検査例外も非検査例外も、同様にキャッチされる。
原則はこれだけだ。
以下、非チェック例外多用作戦のトレードオフ認識 - 都元ダイスケ IT-PRESS への重箱の隅。
上記のソースコードをみると、setterが削除されてコンストラクタが増えることによって、クライアントのコード(コード1)を確実に破壊することがわかるだろう。これも避けようがない。
非チェック例外多用作戦のトレードオフ認識 - 都元ダイスケ IT-PRESS
引数ならばオーバーロードできるから大丈夫。何なら doSomething(Foo, Bar) を追加するのでも、元のコードを破壊しなくて済む。
ならば、なぜ throws だけ破壊されてしまうのか。その答えは簡単で「return と throws はオーバーロードの対象にならない」から。どちらもメソッドから【戻される】値なので、それを元にオーバーロードすることはできないという形。例外はメソッドからの戻り値なのだ。
String を返す getName というメソッドが「StringBuilder も返したいな。よし、戻り値の型を Object にしよう」と言い出すと、元のコードが破壊される。これは throws も return も、どちらも同じこと。
「メソッドが変更されても、呼び出し元は変更したくない。よし、全てのメソッドは Object 型を返すようにしよう!」という話は、少なくとも Java では異端と見なされる。そういうのが好きならば動的型付け言語を楽しむべきだ。
だけど、同じ戻り値でも throws RuntimeException は、ときどき叫ばれる。この差は一体何なんだろう。どこに差があるんだろうか。