finally の return が結果を上書きする問題
finallyでreturnを書いてはいけない理由と、try-with-resourcesによる安全なリソース解放を扱います。
なぜエラーが出ないのか
出力: 2
(エラーなし)- javacは何も報告しません。文法として正しいためです
出力: 2- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 public class Main { 2 static int f() { 3 try { return 1; } 4 finally { return 2; } ^ 5 } 6 public static void main(String[] args) { 7 System.out.println(f()); 8 } 9 }
finally内のreturnはtry内のreturnを上書きします。finallyでreturnを書かないのが鉄則です。
直し方: finally を catch (Exception e) にします。
パターン2
1 import java.io.*; 2 3 public class Main { 4 public static void main(String[] args) throws IOException { 5 try (String r = new BufferedReader(new StringReader("a"))) { ^ 6 System.out.println(r.readLine()); 7 } 8 } 9 }
try-with-resourcesの変数はAutoCloseableを実装した型で宣言します。ブロックを抜けると自動でcloseされます。
直し方: String を BufferedReader にします。
パターン3
1 public class Main { 2 public static void main(String[] args) { 3 try { 4 System.out.println("try"); 5 } { ^ 6 System.out.println("always"); 7 } 8 } 9 }
tryは単独では書けません。catchかfinally、またはリソース宣言のいずれかが必要です。
直し方: (空) を finally にします。
パターン4
1 public class Main { 2 static void f() { 3 try { 4 throw new RuntimeException("try-error"); 5 } finally { 6 throw new RuntimeException("finally-error"); ^ 7 } 8 } 9 public static void main(String[] args) { 10 f(); 11 } 12 }
finallyの中で例外を投げると、tryで発生した元の例外は握りつぶされ、finallyの例外だけが伝わります。
直し方: throw new RuntimeException("finally-error"); を System.out.println("cleanup"); にします。
パターン5
1 public class Main { 2 static void risky() throws java.io.IOException, InterruptedException { 3 throw new java.io.IOException("x"); 4 } 5 public static void main(String[] args) { 6 try { 7 risky(); 8 } catch (java.io.IOException , InterruptedException e) { ^ 9 System.out.println("caught"); 10 } 11 } 12 }
複数の型を1つのcatchでまとめて捕まえるマルチキャッチは | 区切りです。カンマでは書けません。
直し方: , を | にします。
よくある誤解
finallyは必ず実行されると言われますが、System.exit() を呼んだ場合は実行されません。
実務での勘所
finallyで例外や戻り値を上書きしてしまう問題は、リソース解放の文脈で特に起きやすいため、Java 7以降はtry-with-resourcesという専用の構文が用意されました。この構文ではclose()の中で新たに例外が発生しても、元の例外を上書きして消してしまうのではなく、addSuppressedという仕組みで元の例外に「抑制された例外」として記録され、両方の情報が残ります。対象がCloseableを実装しているなら、自分でfinallyに書くよりtry-with-resourcesに任せるほうがこの種の情報消失を避けられます。