白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Java使いがC#を勉強する その⑧ 例外処理

こんばんは。

平穏無事な日々を送りたいわたしです。
でも、世の中には思いがけないことも多いものです。
そんなわけで今日は例外処理です。

例外の比較


ひとまずJavaC#の例外を比較してみましょう。

一般的な例外(Javaでは非検査例外)

説明 C# Java
null値の参照型変数を参照しようとした NullReferenceException NullPointerException
境界外のインデックスを使用して配列、
コレクションなどの要素にアクセスしようとした
IndexOutOfRangeException ※1 IndexOutOfBoundsException
├ArrayIndexOutOfBoundsException
└StringIndexOutOfBoundsException
算術計算で例外的条件が発生した ArithmeticException
├DivideByZeroException
├NotFiniteNumberException
└OverflowException
ArithmeticException
不正な引数、または不適切な引数をメソッドに渡した ArgumentException
├ArgumentNullException
└ArgumentOutOfRangeException
IllegalArgumentException
無効なキャストが行われた InvalidCastException ClassCastException
オブジェクトの現在の状態に対して、不正または不適切なときにメソッドが呼び出された InvalidOperationException IllegalStateException
文字列を数値型に変換しようとしたとき、文字列の形式が正しくない ※2 FormatException NumberFormatException ※3
要求されたオペレーションがサポートされていない NotSupportedException UnsupportedOperationException
間違った型の要素を配列に格納しようとした ArrayTypeMismatchException ArrayStoreException
リソースが欠落している MissingManifestResourceException MissingResourceException

※1 String.RemoveなどはArgumentOutOfRangeExceptionになる
※2 FormatExceptionの範囲はもう少し広く、「引数の書式が仕様に一致していない」ことを表す
※3 NumberFormatExceptionはIllegalArgumentExceptionのサブクラス

JavaではErrorとして扱われるもの

説明 C# Java
入れ子になったメソッド呼び出しが多くなりすぎ、
実行スタックがオーバーフローした
StackOverflowException StackOverflowError
プログラムの実行を継続するためのメモリが不足している OutOfMemoryException OutOfMemoryError


C#にはあるけどJavaにはなさそうなもの

説明 C#
特定のプラットフォームで機能が実行されない PlatformNotSupportException
Dispose済みのオブジェクトで操作が実行される ObjectDisposedException
要求されたメソッドまたは操作が実装されない ※4 NotImplementedException
コレクションに該当するキーが無い ※5 KeyNotFoundException

※4 Javaではテンプレートでの自動挿入時にはUnsupportedOperationExceptionを使う
※5 Javaではキーがない場合にはnullを返す

JavaにはあるけどC#にはなさそうなもの

説明 Java
オブジェクトの並行変更を検出したメソッドによって、
そのような変更が許可されていない ※6
ConcurrentModificationException
この列挙にそれ以上の要素がない ※6 NoSuchElementException

※6 C#ではInvalidOperationExceptionが発生する


リフレクション関連とかI/O関連とか見ていくともっとたくさんあるとは思うのですが、
ひとまずはこのくらいで。

検査例外と非検査例外


C#には非検査例外がありません。というかJavaには検査例外があります。
この一言で終わってしまう気もするのですが、せっかくなのでもう少し踏み込んでみます。

まず先に言っておくと、Java以降に誕生した主要な言語に検査例外を持つものはほとんどありません。
というかわたしはJavaしか知らないのですが、他にもあるんでしょうか。

非検査例外のメリット

非検査例外の良いところはコンパイル時点でどのような例外が発生するかを想定できることです。
つまり、メソッドを呼び出すときなどに
「いいですか、私を呼んだら例外出るかもしれませんよ。覚悟してくださいね。」
と呼び出す相手に脅してお知らせしてくれるわけです。

これは単にプログラマの心構えの問題だけではなくて、
例外のハンドリングの責任を誰が持つか、というのを明確にすることができます。

つまり、発生した例外を自分で処理できる場合にはその例外の存在を上の階層に意識させる必要がないですし、
処理しきれない場合にはthrows節を使って上の階層に明確に例外発生の可能性を通知することができるわけです。

非検査例外のデメリット

当然メリットの裏返しがデメリットにもなるわけで、
メソッドが投げる例外を想定できないということが挙げられます。

これだけであれば設計思想とか趣味の範囲で済むのかもしれませんが、
致命的なのがシグネチャの変更がメソッドの互換性とか拡張性を著しく下げる場合があるということです。

例えば、検査例外であるSampleExceptionを投げるmethod3の例外処理を、二階層上のmethod1で行う場合を考えます。

void method1() {
    try {
        method2();
    } catch (SampleException e) {
        System.out.println("例外だよ");
    }
}

void method2() throws SampleException {
    method3();
}

void method3() throws SampleException {
    throw new SampleException();
}

method2を書く際にはSampleExceptionはこの階層では処理しきれないので上に渡そうと考えるわけですが、
method3が投げる例外の種類が増えた場合、method2もthrows節を変更しなければなりません。
しかし、method2には実質的には何も変更はないのです。

void method1() {
    try {
        method2();
    } catch (SampleException e) {
        System.out.println("例外だよ");
    } catch (SampleException2 e) {
        System.out.println("例外2だよ");
    }
}

void method2() throws SampleException, SampleException2 {
    method3();
}

void method3() throws SampleException, SampleException2 {
    if (Math.random() >= 0.5) {
        throw new SampleException();
    } else {
        throw new SampleException2();
    }
}

ネストがもっと深ければ、軽微な変更でもかなりの修正コストを要することになります。

これが面倒で複数の例外をラップして投げたりすることによって、
結果的にはどんな例外が飛んでくるのかよくわからなくなったりするのです。

もういっそ上がってくる例外をそのまま上に流すよって意味で新しい文法作ったらいいんじゃないでしょうか。
throwsに似せた感じでthroughとかどうです?

void method2() through {
    method3();
}

もちろんIDEでちゃんとチェックしてくださいね。

好きだよ

 
なんだか検査例外に対して否定的になってしまいましたが、わたしは検査例外好きですよ。
何が出るかなお楽しみなメソッドは怖いのです。はい。

でも、C#にはないんですよね。
ちょっと悲しい。