RAT Java勉強会2004 第05回 Javaの例外処理
問題が発生した際は問題が大きくなる前に状況を把握し、復帰に努めなければなりません。
Javaの例外処理を使うと、この部分のプログラムを構造的に書くことができます。
今回は、Javaで例外処理を行う際の基本的な道具を紹介します。
例外処理とは
Section titled “例外処理とは”処理の把握 (1)
Section titled “処理の把握 (1)”- 次の項を一瞬だけ読んで、内容を把握してください。
- 目安は5秒
コーヒーを淹れる処理 (1)
Section titled “コーヒーを淹れる処理 (1)”- CafeShopでは次の手順でコーヒーを淹れる
- 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを謝る。残っていればコーヒーメーカーにコーヒー豆をセットする。
- お湯が残っているかチェックし、残っていなければコーヒーメーカーからコーヒー豆を取り出して、客に5分だけ待ってもらう。残っていればコーヒーメーカーにお湯をセットする。
- コーヒーメーカーが動くかチェックし、動かなければ客にコーヒーメーカーが壊れたことを謝り、修理会社を呼ぶ。動くならばコーヒーメーカーのスイッチを入れる。
コーヒーを淹れる処理 (2)
Section titled “コーヒーを淹れる処理 (2)”- コーヒーの淹れ方を説明してください
処理の把握 (2)
Section titled “処理の把握 (2)”-
日本語を書き換えてみましょう
-
次の項を一瞬だけ読んで、内容を把握してください。
- 目安は5秒
コーヒーを淹れる処理 (3)
Section titled “コーヒーを淹れる処理 (3)”-
コーヒーの淹れ方を説明してください
-
倉庫からコーヒー豆を取ってコーヒーメーカーにセットし、ポットからお湯を出してコーヒーメーカーに入れて、コーヒーメーカーのスイッチを入れる
-
ただし
- コーヒー豆が切れていれば
- 客にコーヒー豆が切れたことを謝る
- お湯が切れていれば
- コーヒーメーカーからコーヒー豆を取り出し
- 客に5分だけ待ってもらう
- コーヒーメーカーが動かなければ
- コーヒー豆とお湯を取り出し
- 客にコーヒーメーカーが壊れたことを謝り
- 修理会社を呼ぶ
- コーヒー豆が切れていれば
処理の把握 (3)
Section titled “処理の把握 (3)”-
どちらが分かりやすいか?
-
2つ目の例は、正常処理と例外処理を分離しただけ
コーヒーを淹れる処理 (4)
Section titled “コーヒーを淹れる処理 (4)”- 実はこれだけの処理
- 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを謝る。残っていればコーヒーメーカーにコーヒー豆をセットする。
- ポットにお湯が残っているかチェックし、残っていなければコーヒーメーカーからコーヒー豆を取り出して、客に5分だけ待ってもらう。残っていればコーヒーメーカーにお湯をセットする。
- コーヒーメーカーが動くかチェックし、動かなければコーヒー豆とお湯を取り出し、客にコーヒーメーカーが壊れたことを謝り、修理会社を呼ぶ。動くならばコーヒーメーカーのスイッチを入れる。
例外処理を使わない場合のプログラム
Section titled “例外処理を使わない場合のプログラム”if (! warehouse.hasBeans()) { apologizeTo(customer, "豆切れ"); return null;}else { coffeeMaker.setBeans(warehouse.getBeans());}if (! pot.hasHotWater()) { coffeeMaker.setAsideBeans(); makeWait(customer, 5); return null;}else { coffeeMaker.setHotWater(pot.getHotWater());}if (! coffeeMaker.isAvailable()) { coffeeMaker.setAsideBeans(); coffeeMaker.setAsideHotWater(); apologizeTo(customer, "機器故障"); support.repare(coffeeMaker); return null;}else { coffeeMaker.switchOn(); return coffeeMaker.getCoffee();}例外を使ったプログラム
Section titled “例外を使ったプログラム”try { coffeeMaker.setBeans(warehouse.getBeans()); coffeeMaker.setHotWater(pot.getHotWater()); coffeeMaker.switchOn(); return coffeeMaker.getCoffee();}catch (WarehouseEmptyException e) { apologizeTo(customer, "豆切れ"); return null;}catch (PotEmptyException e) { coffeeMaker.setAsideBeans(); makeWait(customer, 5); return null;}catch (CoffeeMakerBrokenException e) { coffeeMaker.setAsideBeans(); coffeeMaker.setAsideHotWater(); apologizeTo(customer, "機器故障"); support.repare(coffeeMaker); return null;}正常処理と例外処理
Section titled “正常処理と例外処理”- 例外処理は、あくまで例外である
- 正常処理と例外処理を混在させると読みにくい
- 正常処理はまとめて書く
- 例外処理は、例外の種類ごとにまとめて書く
例外のスロー
Section titled “例外のスロー”最低な例外処理
Section titled “最低な例外処理”public File getReadableFile(String filename) { File file = new File(filename); if (! file.exists()) { System.err.println(filename + "が見つかりません"); System.exit(1); } if (! file.canRead()) { System.err.println(filename + "が読めません"); System.exit(1); } return file;}悪い例外処理
Section titled “悪い例外処理”public File getReadableFile(String filename) { File file = new File(filename); if (! file.exists()) { System.err.println(filename + "が見つかりません"); return null; } if (! file.canRead()) { System.err.println(filename + "が読めません"); return null; } return file;}悪くない例外処理
Section titled “悪くない例外処理”public File getReadableFile(String filename) throws Exception { File file = new File(filename); if (! file.exists()) { throw new Exception(filename + "が見つかりません"); } if (! file.canRead()) { throw new Exception(filename + "が読めません"); } return file;}より良い例外処理
Section titled “より良い例外処理”public File getReadableFile(String filename) throws FileNotFoundException, IOException { File file = new File(filename); if (! file.exists()) { throw new FileNotFoundException(filename + "が見つかりません"); } if (! file.canRead()) { throw new IOException(filename + "が読めません"); } return file;}良い例外処理と悪い例外処理
Section titled “良い例外処理と悪い例外処理”- 最悪の例外処理
- 間違えた瞬間にシステムが終了する
- 呼び出し側は何が悪いのか分からない
- 悪い例外処理
- 間違えていたら不正な値を返す
- 間違えていると分からずに進めてしまう場合がある
- 例外の情報が分かりにくい
- 悪くない例外処理
- 間違えたら例外を投げる
- 呼び出し側に例外処理を強制できる
- 一般的な例外で、呼び出し側で例外に対応しにくい
- より良い例外処理
- 間違えたら種類にあった例外を投げる
- 種類毎に例外処理を記述しやすい
例外をスローする場面
Section titled “例外をスローする場面”- 次のような場面では例外を使うべき
- メソッドが正常な値を返せない場合
- 例外処理を呼び出し側に任せたい場合
throw文
Section titled “throw文”- 例外を投げるには、throw文でThrowableのインスタンスをスローする
- Exception, Errorなど (Exception is-a Throwable)
- メソッドの外側に例外を投げるには、メソッドの宣言時に throws で種類を指定する
public void raise() throws Exception { throw new Exception();}エラーアトミック
Section titled “エラーアトミック”- 例外が発生した場合、オブジェクトの状態が不正になることをできるだけ避ける
- 例外から復帰しやすくなる
例外発生時のロールバック (1)
Section titled “例外発生時のロールバック (1)”- 次の文章をプログラムにしてみましょう
“エレベーターの許容重量は200kgである。50kgのAliceと60kgのBobと65kgのCeliaが乗った。そこに128kgのDavidが乗ろうとしたら重量オーバーになってしまい乗れなかった”
例外発生時のロールバック (2)
Section titled “例外発生時のロールバック (2)”- 重量オーバーの例外を作る
- OverweightException extends Exception
class OverweightException extends Exception { public OverweightException(String msg) { super(msg); }}例外発生時のロールバック (3)
Section titled “例外発生時のロールバック (3)”- エレベーターを表すクラスを作る
class Elevator { // 乗り込んでいる人を表す private Set passengers;
// 人が乗り込む処理 // 重量オーバーで例外発生 public void stepInto(Human human) throws OverweightException { ... }
// 重量オーバーか検出するヘルパーメソッド private boolean isOverweight() { ... }}例外発生時のロールバック (4)
Section titled “例外発生時のロールバック (4)”- 例外発生時に状態が不正になる例
public void stepInto(Human human) throws OverweightException { // とりあえず乗ろうとしている人を乗せる passengers.add(human); // 現在乗っている人で重量超過していないか調べる if (this.isOverweight()) { // 重量超過なら例外をスロー throw new OverweightException(human.getName()+"は乗れません"); }}例外発生時のロールバック (5)
Section titled “例外発生時のロールバック (5)”- とりあえず乗ろうとしている人を乗せる
- 現在乗っている人で合計重量を調べる
- 重量超過なら例外をスロー
- 重量超過時にも人を乗せる
- 例外発生時に乗せた人を降ろしていない
- エレベーターの状態が不正になっている
例外発生時のロールバック (6)
Section titled “例外発生時のロールバック (6)”- 例外発生時にロールバックする例
public void stepInto(Human human) throws OverweightException { // とりあえず乗ろうとしている人を乗せる passengers.add(human); // 現在乗っている人で重量超過していないか調べる if (this.isOverweight()) { // 重量超過なので乗せない passengers.remove(human); // 例外をスロー throw new OverweightException(human.getName()+"は乗れません"); }}例外ハンドラ
Section titled “例外ハンドラ”- tryブロックで発生したExceptionをcatchブロックで補足できる
try { num = Integer.parseInt(str);}catch (NumberFormatException nfe) { System.out.println(nfe.toString()); num = -1;}複数の例外ハンドラ
Section titled “複数の例外ハンドラ”- tryブロックは複数のcatchブロックを持つことができる
try { a = Integer.parseInt(stra); b = Integer.parseInt(strb); answer = a / b;}catch (NumberFormatException nfe) { // 例外処理}catch (ArithmeticException ae) { // 例外処理}- メソッド内で例外を処理せずに、呼び出し元に例外処理を任せられる
- throwsで通過する例外を指定する
public String getInputLine() throws IOException { BufferedReader r = new BufferedReader( new InputStreamReader( System.in)); return r.readLine();}例外の階層化
Section titled “例外の階層化”- 例外は、より上位の例外として扱える
- NumberFormatException is-a Excetpion
- ArithmeticException is-a Exception
try { a = Integer.parseInt(stra); b = Integer.parseInt(strb); answer = a / b;}catch (Exception nfe) { // 例外処理}例外の処理順序
Section titled “例外の処理順序”- 例外ハンドラは、上から順に探される
try { a = Integer.parseInt(stra); b = Integer.parseInt(strb); answer = a / b;}catch (Exception nfe) { // 例外処理}catch (NumberFormatException nfe) { // x ここには来られない}- 例外の発生を前提とした制御を書かない
try { int i = 0; while (true) { total += array[i++]; }}catch (ArrayIndexOutOfBoundsException e) {}for (int i = 0; i < array.length; i++) { total += array[i];}チェックされない例外
Section titled “チェックされない例外”-
java.lang.RuntimeExceptionを継承した例外はチェックされない例外になる
- 暗黙のうちにthrowsに追加されるイメージ
-
チェックされない例外は次のような場面で使用
- プログラミングエラー
- 不正な引数でメソッド呼び出したなど
- catchしても回復できない例外
- キャッチしたところで、どちらにしろプログラムを終了させる必要がある
- プログラミングエラー
-
例
- NoSuchElementException
- NullPointerException
- ClassCastException
ErrorとRuntimeException
Section titled “ErrorとRuntimeException”-
ErrorもRuntimeExceptionも共にチェックされない例外
- Errorはバーチャルマシン内部で起きた例外
- RuntimeExceptionは実行時にプログラムで起きた例外
-
慣例的にErrorはサブクラス化しない
- チェックされない例外はRuntimeExceptionを使う
用意されている例外
Section titled “用意されている例外”- RuntimeExceptionを継承した例外で、良く使われるものはすでに用意されています
- java.lang.NullPointerException
- パラメータに不正にnullを渡した場合
- java.lang.IndexOutOfBoundsException
- パラメータに渡した値が範囲外であった場合
- java.lang.IllegalArgumentException
- 不正なパラメータを渡した場合
- java.lang.IllegalStateException
- オブジェクトの状態が不正な場合 (初期化されていないなど)
- java.lang.UnsupportedOperationException
- サポートしていないメソッドを呼び出した場合
- java.util.NoSuchElementException
- 指定した要素が見つからなかった場合
- java.lang.NullPointerException
用意されている例外を使用する利点
Section titled “用意されている例外を使用する利点”- コーディングの負担が減る
- わざわざ新しい例外を作らない
- 説明が楽
- 「IndexOutOfBoundsExceptionが投げられる可能性がある」というだけで、何が悪いか予想が付く
- 一貫性が高くなる
- プロジェクト毎に投げられる例外の型が違うと、覚えるのが大変
- どのクラスがどのプロジェクトにあって、どの例外を投げるかなどは覚えたくない
- プロジェクト毎に投げられる例外の型が違うと、覚えるのが大変
finally clauseとリソースの開放
Section titled “finally clauseとリソースの開放”ゴミ処理の失敗 (1)
Section titled “ゴミ処理の失敗 (1)”- “r.readLine()“の位置で例外が発生した場合を考えてみましょう
public String getHead(String filename) throws IOException { BufferedReader r = new BufferedReader( new FileReader(filename)); String line = r.readLine(); r.close(); return line;}ゴミ処理の失敗 (2)
Section titled “ゴミ処理の失敗 (2)”-
r.readLine()で例外が発生すると…
- r.close()を実行する前にメソッドを終了
-
ファイルが閉じられずに終了している
- オブジェクトの状態が不正
Catchを用いたゴミ処理
Section titled “Catchを用いたゴミ処理”- catch節を使えばr.close()を埋め込むことができる
- あまり見た目が美しくない?
public String getHead(String filename) throws IOException { BufferedReader r = new BufferedReader( new FileReader(filename)); String line; try { line = r.readLine(); r.close(); } catch (IOException e) { r.close(); throw e; // 再度スロー } return line;}finally clause
Section titled “finally clause”- finallyは、処理が正常もしくは例外で終了する最後に必ず実行される
public String getHead(String filename) throws IOException { BufferedReader r = new BufferedReader( new FileReader(filename)); try { return r.readLine(); } finally { r.close(); }}finallyの挙動
Section titled “finallyの挙動”- try節が正常に終了
- finally節を実行
- try節の中で例外が発生
- 例外をキャッチ
- 対応するcatch節を実行
- finally節を実行
- 例外をキャッチできず
- finally節を実行
- 例外をもう一度スロー
- 例外をキャッチ
finallyを使う場面
Section titled “finallyを使う場面”-
以下の場面では大抵はfinallyが有効です
-
ある処理で例外が発生してもしなくても、その処理が終わったらこれをしなければならない
例外を使ったプログラミング
Section titled “例外を使ったプログラミング”- 次の文章をプログラムにしてみましょう
“エレベーターの許容重量は200kgである。あるエレベーターに50kgのAliceと60kgのBobと65kgのCeliaが乗った。そこに128kgのDavidが乗ろうとしたら重量オーバーになってしまい乗れなかった。そのため、Davidはもう1つの空のエレベーターに乗った。“
オブジェクトの抽出
Section titled “オブジェクトの抽出”- エレベーター
- Alice
- Bob
- Celia
- David
オブジェクトのカテゴリ化
Section titled “オブジェクトのカテゴリ化”-
エレベーター (Elevator)
-
人間 (Human)
- Alice
- Bob
- Celia
- David
-
is-a関係ではなく、has-a関係で表す
-
重量オーバー (OverweightException)
- extends Exception
-
ElevatorServiceException などの上位例外を作っても良い
-
オブジェクト毎に状態を抽出
-
Human
- 名前 (String name)
- 体重 (double weight)
-
Elevator
- 乗客 (Set
passengers)
- 乗客 (Set
振る舞いの抽出
Section titled “振る舞いの抽出”-
オブジェクトの抽出
-
人間
- 名前を取得 (String getName())
- 体重を取得 (double getWeight())
-
エレベーター
- ~に乗る (void stepInto(Human))
- 重量オーバーの例外を投げる (throws OverweightException)
- ~に乗る (void stepInto(Human))
クラスの記述 (Human)
Section titled “クラスの記述 (Human)”public class Human { // 名前 private String name; // 体重 private double weight; // コンストラクタ public Human(String name, double weight) { this.name = name; this.weight = weight; } // getter of name public String getName() { return this.name; } // getter of weight public weight getWeight() { return this.weight; }}クラスの記述 (Elevator)
Section titled “クラスの記述 (Elevator)”public class Elevator { // 乗客 private Set passengers; // コンストラクタ public Elevator() { this.passengers = new HashSet(); } // ~に乗る public void stepInto(Human human) throws OverweightException { ... } // 総重量をチェックするヘルパーメソッド private boolean isOverweight() { ... }}メソッドの記述 (Elevator#stepInto(Human))
Section titled “メソッドの記述 (Elevator#stepInto(Human))”public void stepInto(Human human) throws OverweightException { // とりあえず乗せる passengers.add(human); // 総重量のチェック if (this.isOverweight()) { // とりあえず乗った人を降ろす passengers.remove(human); // 例外のスロー throw new OverweightException(human.getName() + "は乗れません"); }}メソッドの記述 (Elevator#isOverweight())
Section titled “メソッドの記述 (Elevator#isOverweight())”private boolean isOverweight() { // 総重量 double total = 0.0; // それぞれの人について… for (Iterator i = passenger.iterator; i.hasNext();) { // 乗客をHuman型に変換 Human human = (Human) i.next(); // 体重を総重量に加算 total += human.getWeight(); } // 許容重量の200kgを超えているか return (total > 200.0);}クラスの記述 (OverweightException)
Section titled “クラスの記述 (OverweightException)”class OverweightException extends Exception { public OverweightException(String msg) { super(msg); }}class ElevatorWorld { public static void main(String[] args) { // 世界に必要な物体を創造 Human alice = new Human("Alice", 50); Human bob = new Human("Bob", 60); Human celia = new Human("Celia", 65); Human david = new Human("David", 128); Elevator elevator1 = new Elevator(); Elevator elevator2 = new Elevator();
// 50kgのAliceと60kgのBobと65kgのCeliaが乗った。 elevator1.stepInto(alice); elevator1.stepInto(bob); elevator1.stepInto(celia);
try { // 128kgのDavidが乗ろうとしたら elevator1.stepInto(david); return; } catch (OverweightException e) { // 重量オーバーになってしまい乗れなかった System.out.println(e.toString()); }
try { // Davidはもう1つの空のエレベーターに乗った elevator2.stepInto(david); } catch (OverweightException e) { // 重量オーバーになってしまい乗れなかった System.out.println(e.toString()); } }}課題1 - コーヒーメーカーの後始末
Section titled “課題1 - コーヒーメーカーの後始末”- 次の文章をプログラムに変換しなさい
“喫茶店でコーヒーを飲もうとしたら、コーヒーメーカーが壊れているらしく、コーヒーを飲めなかった”
- ただし、喫茶店では次の手順でコーヒーを淹れる
- 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを伝え、コーヒーメーカーをきれいにする。残っていればコーヒーメーカーにコーヒー豆をセットする。
- お湯が残っているかチェックし、残っていなければ客にお湯が切れていることを伝え、コーヒーメーカーをきれいにする。残っていればコーヒーメーカーにお湯をセットする。
- コーヒーメーカーが動くかチェックし、動かなければ客にコーヒーメーカーが壊れたことを伝え、コーヒーメーカーをきれいにする。動くならばコーヒーメーカーのスイッチを入れ、コーヒーを出した後にコーヒーメーカーをきれいにする。
- ただし、喫茶店では次の手順でコーヒーを淹れる