Skip to content

RAT Java勉強会2004 第05回 Javaの例外処理

This content is not available in your language yet.

問題が発生した際は問題が大きくなる前に状況を把握し、復帰に努めなければなりません。
Javaの例外処理を使うと、この部分のプログラムを構造的に書くことができます。
今回は、Javaで例外処理を行う際の基本的な道具を紹介します。

  • 次の項を一瞬だけ読んで、内容を把握してください。
    • 目安は5秒
  • CafeShopでは次の手順でコーヒーを淹れる
    • 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを謝る。残っていればコーヒーメーカーにコーヒー豆をセットする。
    • お湯が残っているかチェックし、残っていなければコーヒーメーカーからコーヒー豆を取り出して、客に5分だけ待ってもらう。残っていればコーヒーメーカーにお湯をセットする。
    • コーヒーメーカーが動くかチェックし、動かなければ客にコーヒーメーカーが壊れたことを謝り、修理会社を呼ぶ。動くならばコーヒーメーカーのスイッチを入れる。
  • コーヒーの淹れ方を説明してください
  • 日本語を書き換えてみましょう

  • 次の項を一瞬だけ読んで、内容を把握してください。

    • 目安は5秒
  • コーヒーの淹れ方を説明してください

  • 倉庫からコーヒー豆を取ってコーヒーメーカーにセットし、ポットからお湯を出してコーヒーメーカーに入れて、コーヒーメーカーのスイッチを入れる

  • ただし

    • コーヒー豆が切れていれば
      • 客にコーヒー豆が切れたことを謝る
    • お湯が切れていれば
      • コーヒーメーカーからコーヒー豆を取り出し
      • 客に5分だけ待ってもらう
    • コーヒーメーカーが動かなければ
      • コーヒー豆とお湯を取り出し
      • 客にコーヒーメーカーが壊れたことを謝り
      • 修理会社を呼ぶ
  • どちらが分かりやすいか?

  • 2つ目の例は、正常処理と例外処理を分離しただけ

  • 実はこれだけの処理
    • 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを謝る。残っていればコーヒーメーカーにコーヒー豆をセットする。
    • ポットにお湯が残っているかチェックし、残っていなければコーヒーメーカーからコーヒー豆を取り出して、客に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();
}
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;
}
  • 例外処理は、あくまで例外である
  • 正常処理と例外処理を混在させると読みにくい
    • 正常処理はまとめて書く
    • 例外処理は、例外の種類ごとにまとめて書く
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;
}
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;
}
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;
}
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;
}
  • 最悪の例外処理
    • 間違えた瞬間にシステムが終了する
    • 呼び出し側は何が悪いのか分からない
  • 悪い例外処理
    • 間違えていたら不正な値を返す
    • 間違えていると分からずに進めてしまう場合がある
    • 例外の情報が分かりにくい
  • 悪くない例外処理
    • 間違えたら例外を投げる
    • 呼び出し側に例外処理を強制できる
    • 一般的な例外で、呼び出し側で例外に対応しにくい
  • より良い例外処理
    • 間違えたら種類にあった例外を投げる
    • 種類毎に例外処理を記述しやすい
  • 次のような場面では例外を使うべき
    • メソッドが正常な値を返せない場合
    • 例外処理を呼び出し側に任せたい場合
  • 例外を投げるには、throw文でThrowableのインスタンスをスローする
    • Exception, Errorなど (Exception is-a Throwable)
  • メソッドの外側に例外を投げるには、メソッドの宣言時に throws で種類を指定する
public void raise() throws Exception {
throw new Exception();
}
  • 例外が発生した場合、オブジェクトの状態が不正になることをできるだけ避ける
    • 例外から復帰しやすくなる
  • 次の文章をプログラムにしてみましょう

“エレベーターの許容重量は200kgである。50kgのAliceと60kgのBobと65kgのCeliaが乗った。そこに128kgのDavidが乗ろうとしたら重量オーバーになってしまい乗れなかった”

  • 重量オーバーの例外を作る
    • OverweightException extends Exception
class OverweightException extends Exception {
public OverweightException(String msg) {
super(msg);
}
}
  • エレベーターを表すクラスを作る
class Elevator {
// 乗り込んでいる人を表す
private Set passengers;
// 人が乗り込む処理
// 重量オーバーで例外発生
public void stepInto(Human human) throws OverweightException {
...
}
// 重量オーバーか検出するヘルパーメソッド
private boolean isOverweight() {
...
}
}
  • 例外発生時に状態が不正になる例
public void stepInto(Human human) throws OverweightException {
// とりあえず乗ろうとしている人を乗せる
passengers.add(human);
// 現在乗っている人で重量超過していないか調べる
if (this.isOverweight()) {
// 重量超過なら例外をスロー
throw new OverweightException(human.getName()+"は乗れません");
}
}
  • とりあえず乗ろうとしている人を乗せる
  • 現在乗っている人で合計重量を調べる
    • 重量超過なら例外をスロー
  • 重量超過時にも人を乗せる
  • 例外発生時に乗せた人を降ろしていない
    • エレベーターの状態が不正になっている
  • 例外発生時にロールバックする例
public void stepInto(Human human) throws OverweightException {
// とりあえず乗ろうとしている人を乗せる
passengers.add(human);
// 現在乗っている人で重量超過していないか調べる
if (this.isOverweight()) {
// 重量超過なので乗せない
passengers.remove(human);
// 例外をスロー
throw new OverweightException(human.getName()+"は乗れません");
}
}
  • tryブロックで発生したExceptionをcatchブロックで補足できる
try {
num = Integer.parseInt(str);
}
catch (NumberFormatException nfe) {
System.out.println(nfe.toString());
num = -1;
}
  • 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();
}
  • 例外は、より上位の例外として扱える
    • NumberFormatException is-a Excetpion
    • ArithmeticException is-a Exception
try {
a = Integer.parseInt(stra);
b = Integer.parseInt(strb);
answer = a / b;
}
catch (Exception nfe) {
// 例外処理
}
  • 例外ハンドラは、上から順に探される
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];
}
  • java.lang.RuntimeExceptionを継承した例外はチェックされない例外になる

    • 暗黙のうちにthrowsに追加されるイメージ
  • チェックされない例外は次のような場面で使用

    • プログラミングエラー
      • 不正な引数でメソッド呼び出したなど
    • catchしても回復できない例外
      • キャッチしたところで、どちらにしろプログラムを終了させる必要がある
    • NoSuchElementException
    • NullPointerException
    • ClassCastException
  • ErrorもRuntimeExceptionも共にチェックされない例外

    • Errorはバーチャルマシン内部で起きた例外
    • RuntimeExceptionは実行時にプログラムで起きた例外
  • 慣例的にErrorはサブクラス化しない

    • チェックされない例外はRuntimeExceptionを使う
  • RuntimeExceptionを継承した例外で、良く使われるものはすでに用意されています
    • java.lang.NullPointerException
      • パラメータに不正にnullを渡した場合
    • java.lang.IndexOutOfBoundsException
      • パラメータに渡した値が範囲外であった場合
    • java.lang.IllegalArgumentException
      • 不正なパラメータを渡した場合
    • java.lang.IllegalStateException
      • オブジェクトの状態が不正な場合 (初期化されていないなど)
    • java.lang.UnsupportedOperationException
      • サポートしていないメソッドを呼び出した場合
    • java.util.NoSuchElementException
      • 指定した要素が見つからなかった場合

用意されている例外を使用する利点

Section titled “用意されている例外を使用する利点”
  • コーディングの負担が減る
    • わざわざ新しい例外を作らない
  • 説明が楽
    • 「IndexOutOfBoundsExceptionが投げられる可能性がある」というだけで、何が悪いか予想が付く
  • 一貫性が高くなる
    • プロジェクト毎に投げられる例外の型が違うと、覚えるのが大変
      • どのクラスがどのプロジェクトにあって、どの例外を投げるかなどは覚えたくない
  • “r.readLine()“の位置で例外が発生した場合を考えてみましょう
public String getHead(String filename)
throws IOException {
BufferedReader r = new BufferedReader(
new FileReader(filename));
String line = r.readLine();
r.close();
return line;
}
  • r.readLine()で例外が発生すると…

    • r.close()を実行する前にメソッドを終了
  • ファイルが閉じられずに終了している

    • オブジェクトの状態が不正
  • 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は、処理が正常もしくは例外で終了する最後に必ず実行される
public String getHead(String filename)
throws IOException {
BufferedReader r = new BufferedReader(
new FileReader(filename));
try {
return r.readLine();
}
finally {
r.close();
}
}
  • try節が正常に終了
    • finally節を実行
  • try節の中で例外が発生
    • 例外をキャッチ
      • 対応するcatch節を実行
      • finally節を実行
    • 例外をキャッチできず
      • finally節を実行
      • 例外をもう一度スロー
  • 以下の場面では大抵はfinallyが有効です

  • ある処理で例外が発生してもしなくても、その処理が終わったらこれをしなければならない

  • 次の文章をプログラムにしてみましょう

“エレベーターの許容重量は200kgである。あるエレベーターに50kgのAliceと60kgのBobと65kgのCeliaが乗った。そこに128kgのDavidが乗ろうとしたら重量オーバーになってしまい乗れなかった。そのため、Davidはもう1つの空のエレベーターに乗った。“

  • エレベーター
  • Alice
  • Bob
  • Celia
  • David
  • エレベーター (Elevator)

  • 人間 (Human)

    • Alice
    • Bob
    • Celia
    • David
  • is-a関係ではなく、has-a関係で表す

  • 重量オーバー (OverweightException)

    • extends Exception
  • ElevatorServiceException などの上位例外を作っても良い

  • オブジェクト毎に状態を抽出

  • Human

    • 名前 (String name)
    • 体重 (double weight)
  • Elevator

    • 乗客 (Set passengers)
  • オブジェクトの抽出

  • 人間

    • 名前を取得 (String getName())
    • 体重を取得 (double getWeight())
  • エレベーター

    • ~に乗る (void stepInto(Human))
      • 重量オーバーの例外を投げる (throws OverweightException)
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; }
}
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);
}
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 - コーヒーメーカーの後始末”
  • 次の文章をプログラムに変換しなさい “喫茶店でコーヒーを飲もうとしたら、コーヒーメーカーが壊れているらしく、コーヒーを飲めなかった”
    • ただし、喫茶店では次の手順でコーヒーを淹れる
      • 倉庫にコーヒー豆が残っているかチェックし、残っていなければ客にコーヒー豆が切れたことを伝え、コーヒーメーカーをきれいにする。残っていればコーヒーメーカーにコーヒー豆をセットする。
      • お湯が残っているかチェックし、残っていなければ客にお湯が切れていることを伝え、コーヒーメーカーをきれいにする。残っていればコーヒーメーカーにお湯をセットする。
      • コーヒーメーカーが動くかチェックし、動かなければ客にコーヒーメーカーが壊れたことを伝え、コーヒーメーカーをきれいにする。動くならばコーヒーメーカーのスイッチを入れ、コーヒーを出した後にコーヒーメーカーをきれいにする。