The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目61:抽象概念に適した例外をスローする

例外翻訳

メソッドの内部で発生した下位レイヤの例外は、外側に伝播させないほうがいい場合があります。 下位レイヤで発生した例外をそのままスローすると、利用者が混乱したり API を実装の詳細で汚染する場合があります。

そのレイヤの概念と合わない例外が発生した場合、その例外をキャッチして正しい抽象概念を持った例外をスローしなおすべきです。

次のコードイディオムは「例外翻訳(exception translation)」と呼ばれています。

//
// 例外翻訳イディオム
//
try {
    // 下位レイヤの処理
    ... 
} catch (LowerLevelException e) {
    throw new HighLevelException(...);
}

たとえば、List インタフェースの骨格実装である AbstractSequentialList では get メソッドで実際に例外翻訳を行っています。

/**
 * このリストの指定された位置の要素を返す
 * @throws IndexOutOfBoundsException index が範囲外 ({@code index < 0 || index >= size()}).
 */
public E get(int index) {
    ListIterator<E> i = listIterator(index);
    try { 
        return i.next();
    } catch (NoSuchElementException e) {
        // 例外翻訳
        throw new IndexOUtOfBoundsException("index: " + index);
    } 
}

例外連鎖

上位レベルにスローする例外に、原因となる下位レベルの例外を持たせることができます。

これを例外連鎖(exception chaining)と呼びます。

//
// 例外連鎖イディオム
//
try {
   // 下位レイヤの処理
   ... 
} catch (LowerLevelException cause) {
   // 原因となった例外を HigherLevelException にセット
   throw new HigherLevelException(cause);
}

HigherLevelException のクラス定義は次のようになります。

class HigherLevelException extends Exception {
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

ほとんどの標準例外はこのコンストラクタを持っているため自身で定義する必要はありません。 このコンストラクタを持たない例外の場合、Throwable.initCause() を利用して、原因の例外を設定することができます。

セットされた原因の例外は Throwable.getCause() で取得することができます。

利用ケース

例外翻訳は乱用するべきではありません。

冒頭でも述べたとおり、何も考えずにすべての下位レイヤの例外を伝播させるよりはましですが、注意して利用するべきです。

まず最初に検討するべきなのは、下位レイヤのメソッドを呼び出す前にチェックメソッドなどを使ってメソッドの実行可否を判定することです。 そうすればそもそも下位レイヤの例外を発生させないですみます。

もし、チェックメソッドがない場合、できるならば下位レイヤの例外をキャッチして何かの処理するべきです。 たとえばログを出力し、例外を無視して上位レイヤの適切な処理を行う、などです。 そうすれば下位レイヤの問題を上位レイヤから隔離することができます。

それも不可能な場合で、かつ、上位レイヤに直接例外をスローすることが正しくない場合だけ、例外翻訳を使うべきです。

【Effective Java】項目60:標準例外を使用する

Java ではほとんどの場面で必要となる基本的な例外が標準ライブラリに含まれています。

すでに用意されている標準の例外を再利用することには次のような利点があります。

  • コードを確立されている慣例と一致させることができる
  • 見慣れた例外によってコードの読みやすさが向上する
  • ロードするクラスが少ないことで空間効率・時間効率が向上する

ただし、スローしようとしている例外が標準例外に合致しない場合、新たな例外を作ることを検討してください。 例外の再利用では、例外の名前が合致していることではなくセマンティクスが一致していることが重要です。

標準例外

用意されている例外の中で、最もよく利用される例外は次のようなものです。

  • IllegalArgumentException
  • IllegalStateException
  • NullPointerException
  • IndexOutOfBoundsException
  • ConcurrentModificationExcetpion
  • UnsupportedOperationException

IllegalArgumentException

これはメソッドに不適切な値の引数を渡した場合にスローされる例外です。

たとえば、処理を繰り返し回数を与えるメソッドに負の値を与えた場合などにスローされる例外です。

IllegalStateException

メソッドのレシーバーとなるオブジェクトの状態が不正な場合にスローされる例外です。

たとえば、初期化が必要なオブジェクトに対して、初期化せずにメソッドを呼び出した場合などにスローするべき例外です。

NullPointerException

null が禁止されている引数に対して、null が与えられた場合にスローされる例外です。

通常、不正な引数の場合は IllegalArgumentException がスローされます。 ただし、null が禁止されている引数に null が与えられた場合には NullPointerException をスローするのが慣例です。

IndexOutOfBoundsException

配列やリストのインデックスを表す引数に範囲外の値が与えられた場合にスローされる例外です。

通常、不正な引数が与えられた場合には IllegalArgumentException がスローされます。 しかし、インデックスを表す引数に範囲外の値が与えられた場合には慣例として IndexOutOfBoundsException をスローします。

ConcurrentModificationExcetpion

特定のスレッドからのみ変更されるべきオブジェクトが並行で変更されようとしたときにスローされる例外です。

UnsupportedOperationException

オブジェクトがそのメソッドを実装していない場合などに呼び出されるメソッドです。

通常、オブジェクトはメソッドを正しく実装しているため、この例外を利用する頻度は低いです。 インタフェースを実装したクラスで特定の操作をサポートしてない場合などに利用できます。

【Effective Java】項目59:チェックされる例外を不必要に使用するのを避ける

チェックされる例外はプログラマに例外の処理を強制させることができるので、プログラムの信頼性を大きく向上させることができます。 ただし、これを過剰に利用すると API が使いにくいものになってしまいます。

次の条件がすべて満たされる場合のみ、チェックされる例外を利用するべきです。

  • API を適切に利用したとしても例外の発生を防ぐことができない場合
  • 例外の補足を強制をすることで有益な回復処理が行える場合

この二つが満たされない場合、チェックされない例外が適切です。

もし、プログラマが次の二つの例外処理しか書けないのであればチェックされない例外が適切でしょう。

try {
   object.method();
} catch (TheCeckedExcetion e) {
   throw new AssertionError(); // 決して起こらない
}
try {
   object.method();
} catch (TheCheckedException e) {
   e.printStackTrace(); // どうしようもないのでエラーを出力して終了
   System.exit(1);
}

CloneNotSupportedException はこのテストに合格しません。

Cloneable を実装しないオブジェクトに対して clone() を呼び出すと CloneNotSupportedException がスローされますが、これはチェックされる例外なので例外処理が強制されます。 しかし、この例外は基本的に回復できません。

そのため、本来はチェックされない例外であるべきでした。

チェックメソッド

チェックされる例外をチェックされない例外に変更するための一つのテクニックはチェックメソッドを作ることです。

try {
    obj.action(args);
} catch(TheCheckedException e) {
    // 例外状態の処理
}

このようなコードがある場合、次のようにチェックメソッドを作成して利用するようにします。

if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
   // 例外状態の処理
}

後者のほうが、よいコードになり API の柔軟性も向上します。

さらに、こうしておけばプログラマが「action は必ず成功する。失敗したらこのスレッドが終了してもよい」と思っているならば、単に

obj.action(args);

のように書くことができます。

これは項目58で説明したチェックメソッドと同じものになります。

チェックメソッドは項目58で述べたようにように同期化が必要な場合には利用できません。 呼び出しの間に他のスレッドが状態を変更してしまう可能性があるからです。

【Effective Java】項目58:回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する

例外の種類

Java には次のような種類の例外があります。

  • チェックされる例外(checked exception)
  • チェックされない例外(unchecked exception)
    • 実行時例外(runtime exception)
    • エラー(error)

チェックされる例外

チェックされる例外がスローされると、プログラマは必ず次のうちのどれかの処理をする必要があります。

  • catch 節で適切に処理する
  • さらに外側のメソッドに伝播させる

この処理はコンパイラによって静的に検査されます。 これらの処理が行われていないとコンパイルエラーとなります。

チェックされる例外はメソッドの利用者にこのような処理を強制することになります。 これを強制する価値があり、これらの処理によってシステムが回復可能な場合のみ、チェックされる例外を利用するべきです。

チェックされる例外を不必要に使うと メソッドの利用者に不必要な負荷をかけてしまいます。

実行時例外

実行時例外はチェックされない例外です。 すなわちチェックされる例外で強制された「catch 節で適切に処理する」ことや「外側に伝播させる」ことが強制されません。 実行時例外がキャッチされない場合、カレントスレッドは停止します。

実行時例外はメソッドの呼び出しが「事前条件違反」の場合に利用するべきです。 たとえば、ArrayIndexOutOfBoundsException は配列のインデックスが正しくないという実行時例外です。 これは事前条件に違反している好例です。

実際、ほとんどの場合は実行時例外を利用するでしょう。 チェックされる例外を使用する機会は少ないはずです。

実行時例外を実装するためには RuntimeExcpetion を継承したクラスを作成します。

エラー

エラーはチェックされない例外の一種です。 Error クラスのサブクラスとして実装されています。

JVM が実行できないような資源不足や致命的なエラーを示すために利用されているためです。 そのため、チェックされない例外の実装としてエラーは利用するべきではありません。

その他

なお、Exception, RuntimeException、Error のサブクラスではない例外を定義することができます。 これらはチェックされる例外として扱われます。

ただし、こういうことをする必要は一切ありません。

『CODE COMPLETE (上)』を読んで

CODE COMPLETE 上巻を読み終えた。

Code Complete 第2版 上 完全なプログラミングを目指して

Code Complete 第2版 上 完全なプログラミングを目指して

平日のお昼休みに10ページずつ読んで三ヶ月弱。最初は、一部ごとにメモでもブログにまとめようと思ってたけどやめてしまった。その方が理解は深まるだろうけどあまりに時間がかかりすぎるので…。

内容

CODE COMPLETE で紹介されるテクニックはすべて「コードの複雑度を下げることができるかどうか」という視点で評価されている。 たびたび議論になる「goto 文」も複雑度を低減できる一部の局面では使うべきだろうという姿勢。 「コードの複雑度」はコーディングの効率にも影響している。 コードの複雑度をコントロールできるプログラマーが優秀なプログラマーとなる。

複雑度を下げるために個別のテクニックがたくさーん紹介されているけどここでは割愛。 有名なものからマイナーなものまでたくさんのテクニックが紹介されています。

変化

自分としてはもともとシンプルなコードが好きだけど、この本でその意識がより強化された。

ただ、シンプルなコードにしようとはしてたけど、結構いきあたりばったりなコーディングをしていたなぁと反省した。

コーディング自体もシンプルにやれたらもっといいプログラマになれる。 シンプルに、というと少し抽象的だけど要するに考える対象をなるべく絞って、集中し、深く理解するという感じだろうか。

  • 一つのクラスを書くくらいのレベルでも設計する(パプリックメソッドをリストアップしたり、他のクラスとの関係をざっとノートに書いてみる)
  • コードを書く前にコメントで各ステップを書いてみる
  • 闇雲に修正→コンパイル→動作確認→動かない→闇雲に修正、というループを繰り返さない
  • 自分の書いたコードを見直して理解しなおす
  • うまくいかないときはコーディングに執着しすぎない。休憩する。

こういうところを少し気をつけてみたら、なんとなーく、前よりもコーディングの出力が上がったと思う コーディングしてる最中は、以前よりタイプする時間が減っているので足取りが遅くなったように感じるけど、終わってみると「思ったより時間がかからなかったな」という感じ。

下巻もけっこうなボリュームだけど読むのが楽しみだな〜。

【Effective Java】項目57:例外的状態にだけ例外を使用する

例外は例外的な状況に対してのみ利用するべきです。 正常なパスで例外を利用するべきではありません。

よいパフォーマンスを得ようとして、次のようなコードを書く人達がいます。

try {
    int i = 0;
    while (true) 
        range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}

このコードは「range 配列をイテレートする」という機能を持っています。 ただし、このコードを見ても何を意図しているのかよく分かりませんし、明らかに異常なコードです。

このコードは、次の理由によりパフォーマンス上の利点も得られません。

  • 例外は通常は使用されないことが想定されています。そのため、例外は多くの JVM 実装でコードの実行を遅くします。
  • try-catch ブロック内にコードを書くと、JVM 実装の最適化が排除されます
  • 配列をループするために標準イディオムを使えば、配列の境界チェックは最適化されます

実際に測定すると、例外に基づいたイディオムは通常のイテレートイディオムと比較して2倍程度遅いです。

例外と API 設計

よい API は正常なパスにおいて例外を使用することをクライアントに強制しません。

ある状態でしか呼び出してはいけないメソッドを持つクラスでは、呼び出してよいかどうかをチェックするメソッドを持っています。 例えば Iterator クラスでは next() を呼び出してよいかをチェックする hasNext() を持っています。

一方、チェックメソッドを持たず、呼び出しが成功したかどうかを区別する戻り値を返すというメソッドがあります。 不適切な状態でメソッドが呼ばれた場合には null などの値が返ります。

オブジェクトが外部からの同期なしで並行アクセスされる場合、この「戻り値方式」を使う必要があります。 なぜなら、対象のメソッドとチェックメソッドの間で状態が変更されるかもしれないからです。

しかし、一般的にはチェックメソッド方式の方がよいとされています。 その方が可読性が高いですし、チェックメソッドを呼び忘れた場合には例外が発生すらため、バグが明らかになりやすいからです。

【Effective Java】項目56:一般的に受け入れられている命名規約を守る

Java には命名規約(naming convention)があります。 命名規約は2種類に分類されていて、活字的(typographical)と文法的(grammatical)に分けられています。

活字的命名規約はパッケージ、クラス、インタフェース、メソッド、フィールド、型変数を扱っていて、ほとんどの場合、絶対に守るべき規則です。 たとえば「パッケージ名は小文字であるべき」、「クラス名の最初は大文字で始まるべきである」など、命名規約の基本的な項目です。

活字的命名規則の具体例は以下のようになります。

識別子
パッケージ com.google.inject, org.joda.time.format
クラス、インタフェース Timer, FutureTask, LinkedHashMap, HttpServlet
メソッド、フィールド remove, ensureCapacity, getCrc
定数フィールド VALUES, NEGATIVE_INFINITY
ローカル変数 i, xref, houseNumber
型パラメータ T, E, K, V, X, T1, T2

文法的命名規約はより柔軟な規約で、場合によっては守らなくてもよい場合があります。 例えば「インタフェースは able や ible で終わるべき」や「メソッドは動詞/あるいは動詞句で命名される」などです。

文法的命名規約に関連して、特徴的なメソッド命名規則がいくつかあります。

  • オブジェクトの型を変換するメソッドは toType となります(例:toString, toArray)
  • レシーバーオブジェクトの型と異なる型の View を返すメソッドは asType(例:asList)
  • 同じ値をもつ基本データ型を返すメソッドは typeValue です(例:intValue)
  • static ファクトリーメソッドは valueOf, of, getInstance, newInstance, getType, newType です

感想

命名規則がすべて列挙されているわけではないし、特におもしろい事も書かれていなかったので、簡単にまとめて終わり。

次回からは「第9章:例外」。

(c) The King's Museum