[Javaの小枝] try ~ finally と return のもう一つの闇


はじめに

Java の try ~ finally と return の良く知られた闇として「finally 句にて値の return もしくは例外のスローをするといろいろとまずい」というものがある。検索すれば詳細な情報はすぐ出てくると思うので、どうしてまずいのかだけを簡潔に述べておくと try 句での return や例外を finally 句で完全に上書いてしまえる、という理由による。すると最初に発生した例外を無視して別の例外をスローできたり、全く何もなかったかのような正常な return を行なえる状態になってしまうのだ。

もう一つの闇

しかしもう一つ、 try ~ finally と return には闇が見え隠れしていることを指摘しておく。実は finally 句ではなく try 句で return していても闇コードになることがあるのだ。こちらについては、あまり情報を見掛けないように思うので以下で述べておくことにする。以下の良く似た二つのメソッドを見て欲しい。(闇コードであることを強調するため、若干不自然な記述をして無理に似せてあるが許して欲しい)

Sample.java
    public static String tryReturnTest1() {
        String s = new String("foo");
        try {
            return s;
        } finally {
            s = s.concat("baa"); // ①
        }
    }

    public static StringBuilder tryReturnTest2() {
        StringBuilder s = new StringBuilder("foo");
        try {
            return s;
        } finally {
            s = s.append("baa"); // ②
        }
    }

    public static void main(String[] args) {
        System.out.println(tryReturnTest1()); // foo だけが表示される
        System.out.println(tryReturnTest2()); // foobaa と表示される
    }

tryReturnTest1 メソッド内では①のコードは返り値に影響を与えていない(fooと表示)のに対して tryReturnTest2の②は影響を与えている(foobaaと表示)ことがわかる。つまり評価は以下のどちらの様式でもないことが明らかだ。

Sample1.java
        // 下記のような評価順ではどちらも foo が返るはず
        String s = new String("foo");
        return s;
        s = s.concat("baa");

        StringBuilder s = new StringBuilder("foo");
        return s;
        s = s.append("baa");
Sample2.java
        // 下記のような評価順ではどちらも foobaa が返るはず
        String s = new String("foo");
        s = s.concat("baa");
        return s;

        StringBuilder s = new StringBuilder("foo");
        s = s.append("baa");
        return s;

最後の質問。以下のコードにおいて、tryReturnTest3 で返る値が想像つくであろうか?

Sample.java
    public static int tryReturnTest3() {
        int s = 2;
        try {
            return s;
        } finally {
            s = s + 3;
        }
    }

    public static void main(String[] args) {
        System.out.println(tryReturnTest3()); // ??
    }

答は…

2となる。

この良く似た3つのメソッドがそれぞれに返す値を理解するには return 文と finally の評価順序をきちんと知っておく必要がある。手短にすませたいのでいきなり正解を書くと

  1. return文の式の評価 (return s;sの評価)
  2. finally句の評価
  3. 値のリターン(1.で評価したもの)

という具合になる。return文の式の評価と、その評価結果のリターンの「間に」finally句が入るのがポイントだ。

tryReturnTest1 においては、式の評価の段階で返り値のインスタンスの参照の評価が終わり、その次にfinally句に入る。変数sで読みとることの出来る参照はfinally句内で変更されるが、String が immutable な設計であるため最初の評価結果である参照と finally 句実行後の変数sの参照は異なったものとなっている。言い換えればs = s.concat("baa");の左辺のsの参照と右辺のsの参照が異なっているということだ。そして最後に値のリターンが行なわれ(右辺のsの参照に対応する) foo のみがリターンされる、という具合だ。

tryReturnTest2 についても同様に理解できる。StringBuilder は String と異なり mutable であり、finally 句での s = s.append("baa"); とした際の左辺のsの参照と右辺のsの参照は同一である。すなわちリターン文の式の評価結果と同一の参照である。結果、返り値は finally 句の影響を受けて foobaa となる、という訳だ。

ここまで説明したら変数が primitive の場合にどうなっているのかは理解できるだろう。immutable なオブジェクトと同様の結果、すなわち String の方の例と同じになる。

最後に

この return と finally のイディオムは「闇」認定してしまったものの実はそれなりに便利であり「ある値を返した後、~の操作を実行する」などと言う場合、返す値を保存しておく一時変数を使わないでコーディングできたりする。例えば「ある配列のとあるインデックスに値を入れ、元々入っていた値を返すメソッド」というものを考えた時、以下のように記述できる。

Sample.java
public static int replaceElementOfArrayAndGetOld(int[] array, int index, int newValue) {
        try {
                return array[index]; // return an old value first, then...
        } finally {
                array[index] = newValue; // replace the old value with a new one
        }
}

try finally を使わなければ以下のようになる。

Sample.java
public static int replaceElementOfArrayAndGetOld(int[] array, int index, int newValue) {
        int oldValue = array[index];
        array[index] = newValue;
        return oldValue;
}

try~finallyの方が行数は長くなってしまっているが「変数が少ない方が良いコードだ」という指針に立てば上のコードの方が良い、という見方も可能ではある。しかし、何が起こるコードなのか初見では意図を読み取り間違う可能性が高い、という意味では、避けた方が良いだろう。使う場合にはソースにコメントを付けて意図を明確にし、動作についてドキュメント化してからの方が良さそうだ。