クレイジーなfinallyをどうにかしたい

概要

finallyのネストをフラットにする。

動機

以下の理由で、finallyの中でtry-catch-finallyを書かなくてはならないことがあります。

finally ブロックの中で呼び出されるメソッドは例外をスローすることがある。そのような例外をキャッチして処理しないと、try ブロック全体が途中で終了してしまう。try ブロックの中でスローされた例外は失われ、リカバリのための処理を行うメソッドは例外の発生原因となる問題に対処できなくなってしまうのである。

http://www.jpcert.or.jp/java-rules/err05-j.html

しかし、finallyが3重になったくらいからクレイジーになってきます。

 finally {
  try {
    if (rs != null) {rs.close();}
  } catch (SQLException e) {
    // ハンドラに処理を移す
  } finally {
    try {
      if (stmt != null) {stmt.close();}
    } catch (SQLException e) {
        // ハンドラに処理を移す
    } finally {
      try {
        if (conn != null) {conn.close();}
      } catch (SQLException e) {
        // ハンドラに処理を移す
      }
    }
  }
http://www.jpcert.or.jp/java-rules/fio04-j.html

Java7のAutoCloseableを実装すればわりとシンプルになりますが。

対策

読みづらいのはネストのせいなのでフラットにします。
カギは実行を遅延する関数オブジェクトです。

  1. finallyで実行したい、例外を発生しうるコードブロックをすべて別々の関数オブジェクトに包みます。(ここではProcクラス)
  2. ブロックを実行したい順にキューに入れます。(ここではappendメソッド)
  3. ループまたは再帰でキューに入った関数オブジェクトを順次実行し、例外はすべて捕捉しておきます。(executeおよびexecute2)
  4. 例外は後から適切に処理します。(ここではすべてprintStackTraceするだけ)

コード

package test;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class NestFinallyTest {

    public static void main(final String[] args) {
        List<Throwable> errors = new NestFinallyTest().append(new Proc() {

            @Override
            public void exec() throws Throwable {
                System.out.println("hoge");
                throw new NullPointerException();
            }
        }).append(new Proc() {

            @Override
            public void exec() throws Throwable {
                System.out.println("fuga");
                throw new IllegalStateException();
            }
        }).append(new Proc() {

            @Override
            public void exec() throws Throwable {
                System.out.println("piyo");
                throw new IllegalArgumentException();
            }
        })
        // 3つ関数を登録してから実行
        .execute2();
        
        // 例外は複数発生しうる
        for (Throwable throwable : errors) {
            throwable.printStackTrace();
        }
    }
    
    // 実行を遅延するための関数オブジェクト
    /* package private */ interface Proc {
        void exec() throws Throwable;
    }
    
    // 関数を貯めておくキュー
    private final Queue<Proc> procs;
    
    public NestFinallyTest() {
        this.procs = new LinkedList<Proc>();
    }
    
    /**
     * 関数を登録する
     * @param proc
     * @return this
     */
    public NestFinallyTest append(final Proc proc) {
        this.procs.add(proc);
        return this;
    }
    
    /**
     * 関数を順に実行し、発生した例外をすべて返す。
     */
    public List<Throwable> execute() {
        List<Throwable> throwables = new ArrayList<Throwable>();
        Proc proc = this.procs.poll();
        for (; proc != null; proc = this.procs.poll()) {
            try {
                proc.exec();
            } catch (Throwable e) {
                throwables.add(e);
            } finally {
                continue;
            }
        }
        
        return throwables;
    }
    
    /**
     * 再帰バージョン
     */
    public List<Throwable> execute2() {
        return execute2Aux(new ArrayList<Throwable>());
    }
    
    private List<Throwable> execute2Aux(final List<Throwable> throwables) {
        Proc proc = this.procs.poll();
        if(proc == null) {
            return throwables;
        }
        try {
            proc.exec();
        } catch (Throwable e) {
            throwables.add(e);
        } finally {
            return execute2Aux(throwables);
        }
        
    }
    
    
}

以上、finallyネストがフラットになりました。

実行結果

hoge
fuga
piyo
java.lang.NullPointerException
	at test.NestFinallyTest$1.exec(NestFinallyTest.java:16)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:96)
	at test.NestFinallyTest.execute2(NestFinallyTest.java:87)
	at test.NestFinallyTest.main(NestFinallyTest.java:34)
java.lang.IllegalStateException
	at test.NestFinallyTest$2.exec(NestFinallyTest.java:23)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:96)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:100)
	at test.NestFinallyTest.execute2(NestFinallyTest.java:87)
	at test.NestFinallyTest.main(NestFinallyTest.java:34)
java.lang.IllegalArgumentException
	at test.NestFinallyTest$3.exec(NestFinallyTest.java:30)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:96)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:100)
	at test.NestFinallyTest.execute2Aux(NestFinallyTest.java:100)
	at test.NestFinallyTest.execute2(NestFinallyTest.java:87)
	at test.NestFinallyTest.main(NestFinallyTest.java:34)

例外を捕捉して次のコードブロックに進んでいることがわかります。
再帰にすると再帰の段数で何個目のブロックで例外が発生したか分かりますね。

関連?

  • ネストが深いとCODING HORROR

> breader.close();
> freader.close();
> bwriter.close();
> fwriter.close();

try 〜 finally で入れ子にすべき。
普通に入れ子にするとネスト深すぎて CODING HORROR ですが (w

http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=24576&forum=12