ほげにっき

hogedigoの日記

Java7 AutoCloseableを使ってスマートにリソース解放する

Javaでは昔からリソースの解放がやたら面倒くさかった。
デストラクタはないしfinalizeはいつ呼ばれるかわからないので、try-finallyで明示的に解放メソッドを呼ぶ必要がある。finally節ではさらにnullチェックとか入ってひたすらややこしいコードになる。

InputStream in = null;
OutputStream out = null;
try {
    in = new FileInputStream("...");
    out = new FileOutputStream("...");
    
    // 例えばストリーム間のコピーとか
    StreamUtils.copy(in, out);
} catch {
    (snip)
} finally {
    if (out != null) {
        try {
            out.close();
        } catch (IOException e) {
            // closeもまた検査例外だからcatchしなきゃ
            logger.warn("close failed.", e);
        }
    }
    if (in != null) {
        try {
            in.close();
        } catch (IOException e) {
            // closeもまた検査例外だからcatchしなきゃ
            logger.warn("close failed.", e);
        }
    }            
}


ちなみに今まではClosingという自前ユーティリティを使って、コードを簡略化していた。
Closingはざっくりとこんなカンジ↓

public static class Closing {

    private List<Closeable> closeables = new ArrayList<Closeable>();
	
    public <T extends Closeable> T later(T closeable) {
        // リソースをため込んでおく。
        closeables.add(closeable);
        return closeable;
    }
	
    public void atLast() {
        // 逆順に解放する
        CloseUtils.close(closeables);
        closeables.clear();
    }
}

laterが引数をそのまま戻しているのがミソ。
こんなカンジに書ける↓

Closing closing = new Closing();
try {
    
    // 例えばストリーム間のコピーとか
    StreamUtils.copy(
        closing.later(new FileInputStream("...")),
        closing.later(new FileOutputStream("...")));
} catch {
    (snip)
} finally {
    closing.atLast();     
}

手前味噌だがなかなか便利だった(^^;


前置きが長くなったが本題に戻る。
Java7でAutoCloseableというインタフェースとtry-with-resourcesとう構文が追加された。
これでリソース解放はかなり簡略に書ける様になった。

try (InputStream in = new FileInputStream("...");
        OutputStream out = new FileOutputStream("...")) {
    
    // 例えばストリーム間のコピーとか
    StreamUtils.copy(in, out);    
}

これだけ。なんて便利。
AutoCloseableインタフェースを実装しているオブジェクトはtry節のスコープが外れる際に自動でcloseメソッドが呼び出される。従来からあるCloseableインタフェースはAutoCloseableを継承しているので、Closebaleなクラスは全てtry-with-resources構文で使用することが出来る。


便利なのは記述の簡略化だけではない。

An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents, an exception can be thrown from the try block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects. If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed method from the exception thrown by the try block.

http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html#suppressed-exceptions

従来finally節のcloseで発生した例外は(検査例外なのでまたcatchしなければならないが)、try節で発生したもっと重要な例外をつぶしてしまうので再throwが難しかった。なので大概はclose時の例外は仕様上容認して、エラーログを出力するのみに留めたりした。
Java7のtry-with-resources構文では、AutoCloseableのclose時に発生した例外もきちんとスローされる。try節で例外が発生していた場合はもちろんそれが優先してスローされるが、その場合でも、close時例外が全てsuppressed exceptionsとして含まれておりThrowable#getSuppressedメソッドで取得することが出来る。その辺りについては別のエントリで追記する。→書いた


結局何が書きたかったというと、今までホクホク顔で使っていた自前ユーティリティが無用になってしまい少し寂しいということ(^_^;
まあJavaが標準で便利になるのは喜ばしいことだ。

一応自前ユーティリティもAutoCloseableにすることもできるけど・・・

try (Closing closing = new Closing()) {
    
    // 例えばストリーム間のコピーとか
    StreamUtils.copy(
        closing.later(new FileInputStream("...")),
        closing.later(new FileOutputStream("...")));
} catch {
    (snip)
}

あまりメリットないかな・・・

Java7 try-with-resourcesのsuppressed exceptionsを検証

前エントリでちょこっと書いたJava7 try-with-resourcesのsuppressed exceptionsを検証する。

public class Test implements AutoCloseable{
	
	private int index;
	
	public Test(int index) {
		super();
		this.index = index;
	}

	public static void main(String[] args) throws IOException, SQLException {
		try (Test test = new Test(1);
				Test test2 = new Test(2)) {
			
			test.throwSQLException();
		} catch (IOException e) {
			System.err.println("IOException occurred.");
			throw e;
		} catch (SQLException e) {
			System.err.println("SQLException occurred.");
			throw e;
		}
	}
	
	public void throwSQLException() throws SQLException {
		System.err.println("throwSQLException:"+index);
		throw new SQLException("sql error:"+index);
	}

	@Override
	public void close() throws IOException {
		System.err.println("close:"+index);
		throw new IOException("close error:"+index);		
	}
}

まず面白かったのがAutoCloseable#closeでスローされた例外もtry-with-resourcesのcatchで補足できること。AutoClosebale#closeはthrows Exceptionと定義されているのだが、サブクラスではthrowsをもっと具象的な例外に定義しなおすことが出来る。Java7コンパイラはtryで宣言されている変数の型もちゃんとチェックしていて、closeが投げる例外(上記コードではIOException)が検査例外の場合はcatchするかthrowsしないとコンパイルエラーとなる。


上記コードを実行してみた。

throwSQLException:1
close:2
close:1
SQLException occurred.
Exception in thread "main" java.sql.SQLException: sql error:1
	at Test.throwSQLException(Test.java:30)
	at Test.main(Test.java:18)
	Suppressed: java.io.IOException: close error:2
		at Test.close(Test.java:36)
		at Test.main(Test.java:19)
	Suppressed: java.io.IOException: close error:1
		at Test.close(Test.java:36)
		at Test.main(Test.java:19)
  1. まずthrowSQLExceptionが実行されSQLExceptionがスローされるが、この時点ではまだcatchされない。
  2. 次にTest#closeがtry中宣言の逆順に呼ばれている。それぞれのclose内ではIOExceptionがスローされているが、これらはサプレス(抑制?)されて先にスローされたSQLExceptionに格納されているので、最後までcatchで補足されない。
  3. 最後にSQLExceptionがcatchされる。

スタックトレースを見るとちゃんとサプレスされたIOExceptionが2つ入っているのが分かる。これらはThrowable#getSuppressedメソッドで取得することも出来る。


次にtry{...}中で例外がスローされなかった場合も見てみよう。throwSQLException呼び出しをコメントアウトする。ついでにTestインスタンスを一つ増やした。

public class Test implements AutoCloseable{
	
	private int index;
	
	public Test(int index) {
		super();
		this.index = index;
	}

	public static void main(String[] args) throws IOException, SQLException {
		try (Test test = new Test(1);
				Test test2 = new Test(2);
				Test test3 = new Test(3)) {
			
//			test.throwSQLException();
		} catch (IOException e) {
			System.err.println("IOException occurred.");
			throw e;
		}
	}
	
	public void throwSQLException() throws SQLException {
		System.err.println("throwSQLException:"+index);
		throw new SQLException("sql error:"+index);
	}

	@Override
	public void close() throws IOException {
		System.err.println("close:"+index);
		throw new IOException("close error:"+index);		
	}
}

結果↓

close:3
close:2
close:1
IOException occurred.
Exception in thread "main" java.io.IOException: close error:3
	at Test.close(Test.java:34)
	at Test.main(Test.java:20)
	Suppressed: java.io.IOException: close error:2
		... 2 more
	Suppressed: java.io.IOException: close error:1
		... 2 more

closeが逆順に呼ばれるまでは一緒。最初に発生したIOExceptionがcatchされた。catchされなかった例外は全てcatchされた例外にsuppressed exceptionsとして格納されている。

結論

これはJavaにとっては地味だけど結構革命的な仕様追加ではなかろうか。
ようやくJavaプログラマ積年のモヤモヤが一つ解消された気がする。

国王杯準々決勝レアルマドリードvsバルセロナ

やりました!バルサ逆転勝利。


Cロナウドが先取点を取ったときはヤラレタ感がただよいましたが、後半はもうバルサペースでレアルはいつものダーティーチームに逆戻り。ぺぺのメッシの手を踏むくだりとか、ホントひどかった。他リーグの選手もtwitterで批判している模様。


イニエスタ、シャビは相変わらずすごかった。あれだけのプレッシャーの中でボールコントロールを失わず空間を制圧してる。もう脱帽。
メッシは今日はそれほどインパクトなかったが、一番激しくプレスを受けていたにも関わらず要所でボールに絡み、決定機を作った(2点目アシスト)のはさすが。


モウリーニョはせっかくリーガ独走しているのに、バルサに勝てないというだけでおそらくかなり苦境に立たされているのではないかと想像する。でもここで他の監督に変えるわけにもいかないよな。バルサ戦以外は好調なんだから。