Android開発では100% Kotlinで開発しており、Javaに触れる機会がなくなったというAndroidエンジニアも多いことでしょう。Javaの経験が全くない方も珍しくはないと思います。
Javaで開発している場合、検査例外はcatchするか、throws宣言をしなければコンパイルが通らないため、否応なく意識しなければならないものですが、Kotlinではなにもしなくともコンパイルが通ってしまうこともあり、検査例外・非検査例外と言われても何のことかいまいち分からないという方がいらっしゃるかもしれません。
本稿ではそのような方に向けて、Throwableの基本的分類について説明しようと思います。
Kotlinでは「例外よりも戻り値によるエラーハンドリングを行う」というポリシーではありますが、完全に例外処理がなくなったわけではありませんし、特にJVM実行環境やJava由来のライブラリを扱う場合は、例外処理を避けることはできません。
Androidアプリ開発者なら、Crashlyticsを使ってリリース後のクラッシュ状況の監視を行う際に、発生している例外から原因を調査する必要もあります。
Throwableの分類を知っていれば、より適切な対応ができるようになるでしょう。
Throwableの基本的分類
Throwableは直系のサブクラスが2つ存在します。ErrorとExceptionです。
また、Exceptionには多数のサブクラスが存在しますが、その中でRuntimeExceptionとそのサブクラスは扱いが異なっています。
Exceptionは非検査例外と検査例外に分けられます。
- 非検査例外: RuntimeException とそのサブクラス
- Javaにおいてコンパイル時にチェックされない
- 検査例外: Exception とそのサブクラスから非検査例外を除いたもの
- Javaにおいてコンパイル時にチェックされる
以降、Errorと検査例外、非検査例外の3分類について説明します。
Error
まずはErrorについてです。Errorは、そのプログラム上では対処のしようがない問題の通知に使われます。
例えば、OutOfMemoryError はガベージコレクタによっても必要なメモリーが確保できず、オブジェクトを割り当てることができなくなったときに発生します。優先度の低いキャッシュをクリアするなど、できることもあると思うかもしれませんが、それらを実行するためのメモリーすら確保できない可能性が高い状態です。メモリーリークを修正する、メモリー消費を抑えるような実装に変更するなど、プログラムの外での対処が必要です。
catchしたところで、回復するための処理すら実行できない状態である可能性が高く、無理に処理を継続するとデータの不整合など予期せぬ二次災害を招く恐れもあるため、原則Errorはcatchしません。
Errorによるクラッシュは、環境起因でアプリ開発者ではどうにもできないことも含まれますが、OutOfMemoryErrorであればメモリリークを調査するなど、そのErrorそのものよりも、そのErrorのきっかけとなる原因を探し当て、修正し、発生を予防することが重要です。
また、Android開発では、R8/ProGuardの設定ミスにより、本来必要なクラスやメソッドが削除されてしまい NoClassDefFoundError や NoSuchMethodError が発生することがあります。また、OSバージョン分岐の不備で、未対応のAPIを含むクラスを読み込もうとして VerifyError などが発生することもあります。
このような Error によるクラッシュが発生している場合は、R8/ProGuardの設定や、OSバージョン分岐が適切かを確認しましょう。
発生数がごく少数である場合は、インストール失敗や、アップデート直後の不安定動作などユーザー環境起因で発生している可能性もあります。
検査例外
検査例外は、Javaでは処理することが強制される例外です。
この例外の発生は仕様であり、例外発生時の処理を含めて実装することが求められるものとなります。
例えば IOException は、何らかの入出力処理時の例外を通知するものですが、アプリ外部との入出力を行うなら、その入出力が失敗することを想定するのは当然であり、その対処処理は実装しなければならないものです。
Javaではthrowsでメソッドがthrowしうる検査例外を記述しなければならず、例外処理が仕様として公開されているのです。
Kotlinでは検査例外をthrowするJavaのメソッドをcatchしていなくとも、エラーにもならず、通常警告も出ません。そのため、本来実装すべき例外処理が実装されないままリリースされ、クラッシュしてしまう、ということが発生します。
検査例外によるクラッシュが発生した場合は、適切な例外処理が実装されていない、「実装もれ」であることを疑いましょう。
非検査例外
RuntimeExceptionおよびそのサブクラスは、非検査例外であり、Javaにおいても対処が必須ではない例外です。NullPointerException が代表例でしょう。ご存じの通り、null状態のオブジェクトを操作しようとしたときに発生するものです。
KotlinではNonNull型とNullable型が別れているため発生しにくくなっていますが、Java由来のメソッド(プラットフォーム型)で、実際にはnullが返ってくるものをNonNull型として扱ってしまった。とか、not-null assertion operator (!!) を使ったが、実際にはnullになることがあった。のような場合に発生します。
非検査例外は、本来発生すべきではない問題が発生した際に使われます。
NullPointerExceptionであれば、catchするのではなく、オブジェクト操作前にチェックするなど、そもそもNullPointerExceptionが発生しないように実装するのが正しいということです。
非検査例外によるクラッシュが発生したのであれば、発生する状況を作ってしまったこと、が問題です。
すなわち「実装上のバグ」を示していると考え、発生させないように修正を行いましょう。
catchに注意が必要なException
最後に、安易にcatchすると重大なバグにつながる例外について説明します。
これらは「問題の通知」ではなく、処理を中断・キャンセルするための「シグナル」として使われる例外です。
CancellationException
Kotlinを使った現代のAndroid開発で、最も注意が必要なのは、Coroutinesで用いられる CancellationException です。これは coroutine のキャンセルに使用されます。こちらは非検査例外であり、catchする必要はありませんが、逆にcatchしてはいけないExceptionです。
よくあるミスとして、クラッシュを回避するため、包括的にExceptionやThrowableでcatchしてしまうというものがあります。
try {
// 原因不明のクラッシュが発生する処理
} catch (e: Exception) {
// ログ出力
}
とか、runCatching を使って
runCatching {
// 原因不明のクラッシュが発生する処理
}.onFailure {
// ログ出力
}
のような処理を書いてしまう場合ですね。
すべての Exception が catch されてしまうため、CancellationException も catch してしまうことになります。そうなると、本来 cancel されるはずの coroutine が cancel されずに残ってしまい重大なバグになってしまいます。runCatching に至っては、内部で Throwable を catch しており、Error も含めてすべて握りつぶされてしまいます。利用には注意が必要です。
(本来包括的なcatchは良くないですが)包括的にcatchする場合でも、CancellationExceptionは別でcatchしてthrowするようにしましょう。
try {
// 原因不明のレアケースクラッシュが発生する処理
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
// ログ出力
}
InterruptedException
Android/Kotlinの開発では触れる機会は少ないとは思いますが、もう一つはJavaのスレッド制御で使われる InterruptedException です。スレッドの中断処理に利用されます。例えば、Thread.sleep() は、割り込みによってsleepが中断されると、 InterruptedException を throw します。
検査例外なのでcatchする必要がありますが、注意が必要なのは、InterruptedException は throw された時点でスレッドの割り込み状態がクリアされている点です。
本来であれば InterruptedException をcatchしてスレッドの終了処理を行う処理があるはずです。そこへ到達する前に catch して握りつぶしてしまうと、適切な終了処理ができなくなってしまいます。catch したなら、スレッド直ちに終了させるか、Thread#interrupt() で割り込み状態をセットするなどの対処が必要です。
基本中の基本であるThrowableの分類について説明しました。
Exceptionが発生しているけど、よく分からないから catch して無視しよう。という対処をした経験ありませんか?私も駆け出しのころよくやらかしていました。例外は catch すれば良いというモノではなく、その種類によって適切な対処方法が異なる、ということを理解いただけたなら幸いです。
クラッシュ調査などでは、ここで紹介した分類を頭に入れた上で、さらにその詳細の分類を調査していくと、原因やその対象方法が体系的に分かっていくと思います。
以上です。