0. はじめに
対象者
- Javaを使っている人
伝えたいこと
- 検査例外と実行時例外の違い
- 例外のアンチパターン
API作成者、API使用者の観点から、例外に関する規則を説明する。
API(Application Program Interface)は、publicメソッドを想定。
参考図書
Java6時代に書かれた本で少し古いが、良書。
明瞭性、簡潔性を重要視した規則が書かれている。
- このスライドでは、本書の文章や図を引用している。
- 「項目~」はこの本の項目番号を表す。
- 本書では「チェックされる例外」という言葉が使われているが、本スライドでは「検査例外」という言葉を用いる。
目次
- 例外の概要
- API使用者のための例外に関する規則
- API作成者のための例外に関する規則
- 付録
1. 例外の概要
例外とは?
例外処理(れいがいしょり)とは、プログラムがある処理を実行している途中で、なんらかの異常が発生した場合に、現在の処理を中断(中止)して、別の処理を行うこと。その際に発生した異常のことを例外と呼ぶ。
検査例外と実行時例外
-
検査例外(Checked Exception):呼び出し元で検査(catch or throw)しなくてはいけない例外。実行時例外でない例外。
-
実行時例外(Rentime Exception):呼び出し元で検査しなくてもよい例外。
java.lang.RuntimeException
を継承する。
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フレームワーク内でビジネスロジックを呼んでいる個所。
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;
}
}
}
※Throwable
はjava.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のクライアントが単に守っていないということです。
よく見る実行時例外
- NumberFormatException
Integer.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のみ。
どちらかという検査例外は否定的。
finally句とreturn
try-catch-finally句の中で、returnをする場合、finally句を設定するとreturnする前にかならずfinally句の中が実行される。
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句が実行されました。");
}
}
}
スタート!
ふがふが
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 "例外を握りつぶしますね";//リターンをする
}
}
}
スタート!
ふがふが
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 をスローした結果として) 突然終了したかにかかわらず、このインスタンスは閉じられます。
「プログラマが知るべき97のこと」で例外に関する内容
参考サイト
[Javaにおける例外処理のベスト・プラクティス]
(https://moneyforward.com/engineers_blog/2014/08/22/java%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E4%BE%8B%E5%A4%96%E5%87%A6%E7%90%86%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%BB%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9/)