#概要
try-with-resource文とDecoratorパターンは相性が悪い。Fluent Interfaceとも。前回作成した AutoCloseable な Tuple を用いることでトラブルの発生を軽減できる。
#はじめに
良く知られているようにJava7から導入された try-with-resource 文はリソースの解放漏れを防ぐのに多大な効果がある。しかしまた、これも良く知られている話だが Decorator パターンや Fluent Interface として設計されたクラス類と try-with-resource 文は、あまり相性が良くない。以下で順に説明していこう。
#Decoratorパターン、Fuluent Interface とリソースリーク
良く知られている話で恐縮だが、下記にtry-with-resource文とDecoratorパターンを不適切に扱ってしまうことによりリソースリークするコードを示す。
public static class ClassA<B extends AutoCloseable> implements AutoCloseable {
private final boolean throwException;
private final B inner;
public ClassA(boolean throwException_, B inner_) {inner = inner_; throwException = throwException_;}
@Override public void close() throws Exception {
if (throwException) throw new RuntimeException();
inner.close();
System.out.println("ClassA closed successfully");
}
}
public static class ClassB<C extends AutoCloseable> implements AutoCloseable {
private final boolean throwException;
private final C inner;
public ClassB(boolean throwException_, C inner_) {inner = inner_; throwException = throwException_;}
@Override public void close() throws Exception {
if (throwException) throw new RuntimeException();
inner.close();
System.out.println("ClassB closed successfully");
}
}
public static class ClassC implements AutoCloseable {
private final boolean throwException;
public ClassC(boolean throwException_) {throwException = throwException_;}
@Override public void close() throws Exception {
if (throwException) throw new RuntimeException();
System.out.println("ClassC closed successfully");
}
}
public static void main(String[] args) throws Exception {
System.out.println("bad case");
try (ClassA<ClassB<ClassC>> a = new ClassA<>(false, new ClassB<ClassC>(true, new ClassC(false)));) {
// ClassC を new し、次に ClassB を new するところでコンストラクタ中で例外が発生する
System.out.println("try with resource internal");
} catch (RuntimeException e) {}
System.out.println("try with resource end");
System.out.println();
}
上記を実行するとコンソールに下記のような出力があらわれる。
bad case
try with resource internal
try with resource end
ClassC は new されたにも関わらず適切に close が呼び出されていない。ClassC closed successfully と出力されていないことからそれがわかる。try-with-resource文はその中で「定義された変数が保持するオブジェクトのみ」close を実行してくれるのだ。オブジェクトを生成する過程で生みだされる無数の中間オブジェクトの面倒までは見てくれない。もちろん本来DecoratorパターンではDecorator側が内側のオブジェクトのclose まで面倒を見ないといけないのであり、この場合、ClassBは何が何でもClassCのcloseを行なわなくてはならなかったのであるが、今回の例ではそのようにコーディングされていない。現実のコードでもメモリ不足その他の有象無象のエラーに対して確実に動作させるのは困難を伴なう場合が多い。
ではどうするか。一般的な対処方法としては「中間オブジェクトまで全部変数に保持させる」というやりかたが採用されていることが多い。以下のようなコードだ。
public static void main(String[] args) throws Exception {
System.out.println("normal case");
try (
ClassC c = new ClassC(false);
ClassB<ClassC> b = new ClassB<>(true, c);
ClassA<ClassB<ClassC>> a = new ClassA<>(false, b);
) {
System.out.println("try with resource internal");
} catch (RuntimeException e) {}
System.out.println("try with resource end");
System.out.println();
}
上記を実行すると以下のようになる。
normal case
try with resource internal
ClassC closed successfully
try with resource end
きちんと ClassC が close されたことがわかる。しかし、これでは後で使いもしない中間オブジェクトを全て数えあげて変数定義せねばならず、不要に名前空間を汚してしまうことになる。多数のコーディングの指南書(例えばリーダブルコード)でも指摘されている通り、変数のスコープは可能な限り小さく、変数の数は可能な限り少なくするのが望ましい。
そこで前回作成した AutoCloseable な Tuple の登場だ。下記のコードを見て欲しい。
public static void main(String[] args) throws Exception {
System.out.println("best case");
try (
Tuple3AC<ClassC, ClassB<ClassC>, ClassA<ClassB<ClassC>>> tpl = new Tuple3AC<>(
new ClassC(false),
c -> new ClassB<>(true, c),
b -> new ClassA<>(false, b)
);
) {
System.out.println("try with resource internal");
} catch (RuntimeException e) {}
System.out.println("try with resource end");
System.out.println();
}
アイデアは極めて単純で中間オブジェクトを全て AutoCloseable な Tuple に保持させてしまおうというものだ。結局のところ、実質としては上記コードと一緒ではあるのだが何より名前空間を汚すのは最終変数を使うために必要な Tuple 一つだけなので「変数は必要最小限に」という一般的なコーディング指針にも合致する。
Decoratorパターンと同様のリソースリークは Fluent Interface 的な設計のクラスについてもあてはまる。下記はJDBCを使用する際にやってしまいがちな典型的なコードだ。
public static Map<String, String> makeMapFromTable(Connection conn) throws SQLException {
Map<String, String> returnValue = new HashMap<>();
try (ResultSet rs = conn.createStatement().executeQuery("select * from memberMaster")) {
while (rs.next()) returnValue.put(rs.getString("memberCode"), rs.getString("memberName"));
} // 中間オブジェクト Statement のインスタンスが close されない!
return returnValue;
}
これを
public static class ResultSetWrapper extends Tuple2AC<Statement, ResultSet> {
public ResultSetWrapper(Connection conn, String sql) throws SQLException {
super(conn.createStatement(), toUnchecked((Statement s) -> s.executeQuery(sql)));
}
// close メソッドからは SQLException しか投げられないはず。
// しかし文法上の制限から、それ以外の例外についても記述せねばならないため、
// 仮に非チェック例外でラップして throw するコードを入れてある
@Override public void close() throws SQLException {
try {
super.close();
} catch (SQLException e) {
throw e;
} catch (Throwable th) {
throw new RuntimeException(th);
}
}
}
// 以下はラムダ式中で例外がthrowされるケースを try catch でくるむことにより
// 非チェック例外しか throw しないラムダ式に変換する便利メソッド。
// 他からは static import して使ったりする。
public static interface FunctionWithThrowable<A, B> {public B apply(A a) throws Throwable;}
public static <A, B> Function<A, B> toUnchecked(FunctionWithThrowable<A, B> src) {
return new Function<A, B>() {
@Override public B apply(A t) {
try {
return src.apply(t);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
};
}
というような準備をしておくことで下記のように書けることになる。
public static Map<String, String> makeMapFromTable2(Connection conn) throws SQLException {
Map<String, String> returnValue = new HashMap<>();
try (ResultSetWrapper rs = new ResultSetWrapper(conn, "select * from memberMaster")) {
while (rs.get().next()) returnValue.put(rs.get().getString("memberCode"), rs.get().getString("memberName"));
}
return returnValue;
}
これで先程までの見掛けとさほど変らない、しかし Statement のインスタンスの close 漏れの発生しないコードを書くことができるようになった、という訳だ。
今回のコードを応用するにあたって注意点が一つある。先に説明したようにDecoratorパターンではDecorator側(外側)が close された際、内側で保持しているインスタンスの close を外側のインスタンスが呼びだす実装が一般的だ。すなわち、正常終了した場合には内側のインスタンスの close メソッドは以下のように都合2回、呼びだされることになる。
- 外側のインスタンス → 内側のインスタンス という経路
- try-with-resource文 → TupleAC → 内側のインスタンス という経路
これはまずい?? いや、さほどまずくない。以下のURLを見ればわかる通り、Closeable の close は厳密に冪等である必要があるし、AutoCloseable の close も冪等であるように実装されることが強く推奨されている。自分で作成する close メソッドがそのように実装されてさえいれば複数回呼びだされても何も問題が生じない。
- Closeable の説明
- AutoCloseable の説明
- 冪等とは
- REST API における冪等