Help us understand the problem. What is going on with this article?

【社内勉強会】Javaの例外処理(2017/04/26)

More than 1 year has passed since last update.

【社内勉強会】Javaの例外処理(2017/04/26)

by yuji38kwmt
1 / 34

0. はじめに


対象者

  • Javaを使っている人

伝えたいこと

  • 検査例外と実行時例外の違い
  • 例外のアンチパターン

API作成者、API使用者の観点から、例外に関する規則を説明する。
API(Application Program Interface)は、publicメソッドを想定。


参考図書

Effective Java 第2版

参考図書.jpg

Java6時代に書かれた本で少し古いが、良書。
明瞭性、簡潔性を重要視した規則が書かれている。

  • このスライドでは、本書の文章や図を引用している。
  • 「項目~」はこの本の項目番号を表す。
  • 本書では「チェックされる例外」という言葉が使われているが、本スライドでは「検査例外」という言葉を用いる。

目次

  1. 例外の概要
  2. API使用者のための例外に関する規則
  3. API作成者のための例外に関する規則
  4. 付録

1. 例外の概要


例外とは?

例外処理(れいがいしょり)とは、プログラムがある処理を実行している途中で、なんらかの異常が発生した場合に、現在の処理を中断(中止)して、別の処理を行うこと。その際に発生した異常のことを例外と呼ぶ。

Wikipedia 例外処理 引用


検査例外と実行時例外

  • 検査例外(Checked Exception):呼び出し元で検査(catch or throw)しなくてはいけない例外。実行時例外でない例外。

  • 実行時例外(Rentime Exception):呼び出し元で検査しなくてもよい例外。java.lang.RuntimeExceptionを継承する。

Exceptionの型構造
java.lang.Exception
 -  IOException
 -  SQLException
 -  RuntimeException
     - NullPointerException
     - IllegalArgumentException

例外処理の構文(スローするメソッドを呼ぶ)

java.io.FileReaderのコンストラクタを使う場合。

throwsはそのメソッドがスローするExceptionを定義する。

public FileReader(File file) throws FileNotFoundException

Exceptionをスローするメソッドを呼ぶ場合の対応は

  • Exceptionをキャッチする
try {
  FileReader fr = new FileReader(file);  
} catch(FileNotFoundException e) {
  //...
}
  • Exceptionをスローする メソッド宣言にthrowsを使う。
private void read(File file) throws FileNotFoundException {
    FileReader fr = new FileReader(file);  
}

例外処理の構文(Exceptionを明示的にスローする)

throw予約語を使う。

private void read(File file) {
  if (file == null) {
     throw new NullPointerException("file is null");
  }
}

検査例外はメソッドにthrowsを宣言しなくてはいけない。
実行時例外(NullPointerExceptionなど)は不要。
検査例外と実行時例外の説明は後述。


例外処理の構文(マルチキャッチ)

Java7から利用可能。

Java6以前の場合

catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

Java7以降の場合

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/language/catch-multiple.html 参考


2. API使用者のための例外に関する規則


例外を無視しない(項目65)

無視は最悪。ダメ絶対。

try {
   ...
} catch(SomeException e) {}

空のcatchブロックでは、例外の目的が達成されません。その目的とは「例外的状態を処理させることを強制する」ことです。

個人的見解

  • 譲歩1:無視している理由をコメントで記述
  • 譲歩2: Exceptionの内容を出力。log.error("~失敗", e);
  • 譲歩3:キャッチせずにとりあえずスロー

例外的状態にだけ例外を使用する(項目57)

何が問題か?
ループの終了判定に例外を使っている。

int[] array = {1,2,3,4,5};
try {
    int i=0;
    while(true) {
        System.out.println(array[i++]);
        //その他の処理
    }
} catch(ArrayIndexOutOfBoundsException e) {}
  • 明白でない。ループしたいならforループの方が明白。
  • 想定していないバグが隠れてしまう。「その他の処理」でArrayIndexOutOfBoundsExceptionが発生したら?

例外は、その名が示す通り、例外的条件に対してのみ使用すべきです。通常の制御フローに対しては、決して使用すべきではありません。


[個人的見解] 適切な例外でキャッチする

IOExceptionでキャッチすればよいのに、java.lang.Exceptionでキャッチしている。

File file = new File("/tmp/sample.txt");
try {
  FileReader fr = new FileReader(file);  
} catch(Exception e) { //IOExceptionが適切
  //...
}

問題点

  • どのExceptionをキャッチしたいのか分からない。
  • 本来上位メソッドに伝搬してほしい、RuntimeExceptionまでキャッチしてしまう。

java.lang.Exceptionでキャッチしてもよい場合

フレームワークの処理など。
どこかでExceptionの伝搬を止めないと、システム(たとえばTomcat)がダウンしてしまう。

以下のソースは、Terasolunaフレームワーク内でビジネスロジックを呼んでいる個所。

jp.terasoluna.fw.ex.aop.log.BLogicLogInterceptor.java
 public Object invoke(MethodInvocation invocation) throws Throwable {
       //...
        try {
           //ビジネスロジックの実行
            result = invocation.proceed();
        } catch (Throwable e) {
            if (checkException(e)) {
                logger.error(e); //【exception出力】
                throw e;
            } else {
                logger.debug(e); //【exception出力】
                throw e;
            }
        }
}        

Throwablejava.lang.Exceptionの1つ上位。


[個人的見解] tryブロックの範囲は広すぎず

Exceptionをスローしない処理は、可能ならばtryブロックに記述しない方がよい。

try {
  //ファイル以外の処理(Exceptionはスローされない)
  initialize();
  //ファイル関係の処理
  File file = new File("/tmp/sample.txt");
  FileReader fr = new FileReader(file);  
} catch(IOException e) { 
  //...
}

問題点

  • どのメソッドがExceptionをスローするのか分かりにくい。

3. API作成者のための例外に関する規則


プログラミングエラーには実行時例外を使用する(項目58)

実行時例外のほとんどは事前条件違反を示しています。事前条件違反は、API仕様が決めている契約をAPIのクライアントが単に守っていないということです。

よく見る実行時例外

  • NumberFormatExceptionInteger.parseInt()に渡す引数をチェックすれば発生しない
  • NullPointerException:nullチェックすれば発生しない
  • ArrayIndexOutOfRangeException:配列サイズをチェックすれば発生しない

http://qiita.com/yuba/items/d41290eca726559cd743 参考

API使用者が回避できる例外、API使用者の責任。


回復可能な状態にはチェックされる例外(検査例外)を使用する(項目58)

呼び出し側が適切に回復できるような状況に対してはチェックされる例外を使用しないということです。

「回復可能な状態」がよくわからない。

http://qiita.com/yuba/items/d41290eca726559cd743 によると、「検査例外は、呼び出し側の責任でない異常系」とのこと。

よく見る検査例外

  • IOException: 入出力処理の失敗、または割り込みの発生
  • SQLException: DB障害など、呼び出し側で防げない

回避できないので、API使用者に「異常系を考える」よう強制している。


そもそもIOExceptionは本当に回避できないのか

FileReaderコンストラクタに存在しないファイルパスを渡すと、FileNotFoundException発生する。
ファイルの存在確認しても、その直後にファイルが削除される可能性はあるので、完全に回避することはできない。

File file = new File("/tmp/sample.txt");
if (file.exists() ){ 
  //ここでファイルが削除されれば、次の行でExceptionが発生する
  FileReader fr = new FileReader(file);
}  

私の見解

「API使用者の責任でない」ならば、「API作成者の責任」というわけでもなさそう。
たぶん、誰も責任は取れない。

API作成者は「異常系が発生しても、これ私の責任ではないよ」という意味を込めて、検査例外を投げているように感じた。


API使用者の責任でも検査例外の場合がある?

java.text.DateFormat#parseメソッドは検査例外ParseExceptionをスローします。

ParseException - 指定された文字列の先頭が解析できない場合。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
try {
    sdf.parse("abc"); //PrseExceptionスロー
} catch (ParseException e) {
    e.printStackTrace();
}

Integer.parseIntは実行時例外NumberFormatExceptionをスローする。
SimpleDateFormatはスレッドセーフでないので、検査例外をスローするのかもしれない。


[個人的見解] java.lang.Exceptionをスローしない

/** ファイル読み込み */
List<String> read(File file) throws Exception {
  List<String> lines = new ArrayList<>();
  FileReader fr = new FileReader(file);
  //....
  return lines;
}  

問題点

  • 例外の内容が抽象的すぎる。「異常」としか言っていない。

4. 付録


例外処理機構が存在しないC言語

エラーを戻り値や出力引数で表す。

/**
 * 0: 成功
 * -1: 引数エラー
 * -2: ファイル関係のエラー
 * /
int read(char *file) {
  //...
}

int sub(char *file) {
  int rtn = read(file);
  return rtn;
}

int main() {
  int rtn = sub(file);
}

問題点

  • エラーを上位に伝搬するには、下位APIの戻り値を上位に返す処理を書く必要がある
  • API使用者にエラーチェックを強制することができない

検査例外の賛否

メジャーな言語で検査例外が実装されているのはJavaのみ。
どちらかという検査例外は否定的。

http://qiita.com/Kokudori/items/0fe9181d8eec8d933c98


finally句とreturn

try-catch-finally句の中で、returnをする場合、finally句を設定するとreturnする前にかならずfinally句の中が実行される。

source-try.java
public class Try {
    public static void main(String[] args) {
        String i=TryCatch();
        System.out.println(i);
    }

    public static String TryCatch(){
        System.out.println("スタート!");
        String str = null;//nullにする
        try{
            str.toString();     //NullPointerException
            System.out.println("ほげほげ");
            return "Try句からのリターンです。";
        }catch(Exception e){
            System.out.println("ふがふが");
            return "Catch句からのリターンです。";
        }finally{
            System.out.println("finally句が実行されました。");
        }
    }
}
System.out
スタート!
ふがふが
finally句が実行されました。
Catch句からのリターンです。

http://qiita.com/eijenson/items/7e9e112e69b37f72353c より引用


finally句の中でbreak、continue、return、System.exitは禁止

これらは、javacでコンパイルした場合にはエラーや警告の対象にはなりません。しかし、Eclipseを用いると、break、continue、returnについて「finallyブロックは正常に完了しません。」という警告が表示されます。

http://www.atmarkit.co.jp/ait/articles/0611/22/news145.html より引用

public class Try {
    public static void main(String[] args) {
        String i=TryCatch();
        System.out.println(i);
    }

    public static String TryCatch(){
        System.out.println("スタート!");
        String str = null;//nullにする
        try{
            str.toString();     //NullPointerException
            System.out.println("ほげほげ");
            return "Try句からのリターンです。";
        }catch(Exception e){
            System.out.println("ふがふが");
            return "Catch句からのリターンです。";
        }finally{
            System.out.println("finally句が実行されました。");
            return "例外を握りつぶしますね";//リターンをする
        }
    }
}
System.out
スタート!
ふがふが
finally句が実行されました。
例外を握りつぶしますね

try-with-resources

Java7から使える。close不要な構文

Java SE 7 より前では、try 文が正常に終了したか突然終了したかにかかわらずリソースが確実に閉じられるようにするために、finally ブロックを使用できます。

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
  BufferedReader br = new BufferedReader(new FileReader(path));
  try {
    return br.readLine();
  } finally {
    if (br != null) br.close();
  }
}

try-with-resources文を使った場合

static String readFirstLineFromFile(String path) throws IOException {
  try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
  }
}

この例で、try-with-resources 文で宣言されているリソースは BufferedReader です。宣言文は try キーワードの直後の括弧内にあります。Java SE 7 以降では、クラス BufferedReader はインタフェース java.lang.AutoCloseable を実装しています。BufferedReader インスタンスは try-with-resource 文で宣言されているため、try 文が正常に終了したか、(メソッド BufferedReader.readLine が IOException をスローした結果として) 突然終了したかにかかわらず、このインスタンスは閉じられます。

http://docs.oracle.com/javase/jp/7/technotes/guides/language/try-with-resources.html


「プログラマが知るべき97のこと」で例外に関する内容

プログラマが知るべき97のこと

プログラマが知るべき.jpg


参考サイト

Javaにおける例外処理のベスト・プラクティス

Javaの検査例外は、呼び出し側の責任でない異常系

エラーハンドリングの歴史

例外設計における大罪


おわり

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away