「例外をむやみにキャッチして握り潰してはいけない」 これはよく知られている鉄則です。自ら対処できないのであれば、 例外をキャッチせずに上位にそのまま伝搬させるのが良い設計であるとされています。
例外を握り潰してしまっている例public void myBusinessLogic() {
try {
doSomething();
} catch(IOException e) {
e.printStackTrace();
}
}
この鉄則は広く浸透しており、 上記のようなコードを書いている人はもういません。多くの開発者は、 処理できない例外をそのまま伝搬させるコードを書きます。
例外をキャッチせずにそのまま上位に伝搬させる例public void myBusinessLogic() throws IOException {
doSomething();
}
この鉄則は 「呼び出される側のコード」 について言及したものです。呼び出される側のライブラリならそれで十分でしょう。しかし、 伝搬していった例外はいつか誰かがキャッチしなければなりません。その役回りは、 ほとんどの場合、 アプリケーションが務めることになります。
ライブラリでは投げっぱなしにできた例外も、 アプリケーションではそうはいきません。投げっぱなしにされた例外をアプリケーションはどのようにハンドリングすれば良いのでしょうか?
Java
アプリケーションは自ら対処できない例外も必ずキャッチすべし
コンソールアプリケーションであれば例外をキャッチしないという選択肢もあります。main メソッドの外まで伝搬した例外は Java ランタイムによってスタックトレースが出力されるので、 ユーザーはターミナルに表示されたスタックトレースを見てエラーが発生したことを察することができます。長い呪文のように意味不明のスタックトレースであってもそれくらいのことは感じ取れるものです。
GUI アプリケーションの場合はそうはいきません。キャッチされなかった例外はターミナルに出力されることもなく、 瞬く間にアプリケーションウィンドウは消え去ってしまいます。ユーザーは何が起こったのか理解できないでしょう。(操作を誤って自分でウィンドウを閉じてしまったと思うかもしれません。)
GUI アプリケーションにおいては、 たとえ自ら対処できない回復不能な例外であっても、 必ずそれをキャッチし、 最期の力を振り絞ってエラーが発生したことをユーザーに伝えてからアプリケーションを終了させることが重要になります。
この目的は、 エラー状態を回復させて処理を継続することではなく、 ユーザーにエラーを提示したり、 スタックトレースをログファイルに記録することにあります。ですから、 個々の例外発生箇所に応じた対処 (try~catch
) ではなく、 アプリケーション全体で広域的に共通の例外処理ができると都合が良いです。
JavaFX で例外を広域的にキャッチする
Java には、 キャッチされなかった例外 (未処理例外) によってスレッドが終了しようとしたときに呼び出される Uncaught
このハンドラーは特定のスレッドに対して設定することも、 すべてのスレッドに対して設定することもできます。特定のスレッドに対してハンドラーを設定する場合は、 対象となる Thread インスタンスの set
この未処理例外ハンドラーの仕組みが Java
イベントのディスパッチ中、 アニメーション ・ タイムラインの実行中、 またはその他のコード中に Java
FX アプリケーション ・ スレッドで発生したすべての未処理例外は、 スレッドの uncaught exception handler に転送されます。
Java
public class Sample extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
//FXアプリケーションスレッドでキャッチされなかった例外を処理します。
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
showException(e);
});
}
protected void showException(Throwable e) {
e.printStackTrace();
Alert dialog = new Alert(Alert.AlertType.WARNING, e.getMessage());
dialog.setTitle(e.getClass().getSimpleName());
dialog.setHeaderText(null);
dialog.showAndWait();
}
}
start
メソッドは JavaThread
に対して Uncaughtshow
を定義し発生した例外をダイアログで表示するようにしています。
サンプルコードにもう少し肉付けをして実際に動くコードを完成させましょう。
Sample.javaimport javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
//FXアプリケーションスレッドでキャッチされなかった例外を処理します。
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
showException(e);
});
Button button = new Button("Click Me!");
button.setPrefSize(240, 160);
button.setOnAction(event -> {
myBusinessLogic();
});
Scene scene = new Scene(button);
primaryStage.setScene(scene);
primaryStage.show();
}
public void myBusinessLogic() {
throw new RuntimeException("oops!");
}
protected void showException(Throwable e) {
e.printStackTrace();
Alert dialog = new Alert(Alert.AlertType.WARNING, e.getMessage());
dialog.setTitle(e.getClass().getSimpleName());
dialog.setHeaderText(null);
dialog.showAndWait();
}
}
1 つのボタンを配置しました。ボタンをクリックすると my
メソッドが実行され、 my
メソッドは Runtime
をスローします。
スローされた Runtime
は未処理例外ハンドラーに転送されます。未処理例外ハンドラーでは show
メソッドを呼び出してダイアログを表示します。
未処理例外ハンドラーを使わない場合、 以下のように個別に例外をキャッチしてダイアログを表示する必要があります。
button.setOnAction(event -> {
try {
myBusinessLogic();
} catch(Exception e) {
showException(e);
}
});
これが try~catch
なしで書けるのはスマートですよね。コードが簡潔になると全体の見通しも良くなります。
button.setOnAction(event -> {
myBusinessLogic();
});
ここまでは上手い具合にいっています。
未処理例外ハンドラーと検査例外は仲が悪い
Uncaught
下記のような書き方ができるのは、 my
メソッドがスローするのが Runtime
(実行時例外) だからです。
button.setOnAction(event -> {
myBusinessLogic();
});
my
が実行時例外ではなく検査例外 (たとえば IOException
) をスローするように変更してみます。
public void myBusinessLogic() throws IOException {
throw new IOException("oops!");
}
すると、 検査例外である IOException
をキャッチして、 実行時例外に変換してから再スローしなければならなくなります。
button.setOnAction(event -> {
try {
myBusinessLogic();
} catch(Exception e) {
throw new RuntimeException(e);
}
});
このような書き方をしなければいけない理由は、 set
メソッドに指定する Event
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
try {
myBusinessLogic();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
});
Eventhandle
メソッドに throws Exception
が付いていないため、 Javathrows Exception
が付いていてくれたら良かったのですが…。残念です。
検査例外を実行時例外に変換するラッパーを作る
Javathrows
句が付いていないのですから、 どうしようもないのです。
「実行時例外しかスローできない」 という Javatry~catch
で検査例外を実行時例外に変換して再スローするという冗長なコード記述を削減する手立てはまだ残されています。
検査例外を実行時例外に変換する関数型インターフェース ・ ラッパーを作るのです。
検査例外を投げっぱなしにできない原因は Eventthrows
句がないことでした。それなら、 throws
句の付いた関数型インターフェースを自分で定義してしまえば良いのです。
Java
EventHandler.java@FunctionalInterface
public interface EventHandler<T extends Event> extends EventListener {
void handle(T event);
}
これを真似て throws Exception
を持つ関数型インターフェースを自分で作ります。
@FunctionalInterface
public interface Silent<T extends Event> {
void handle(T event) throws Exception;
}
自作の関数型インターフェースの名前は Silent
としました。Silent
は Eventthrows Exception
を付けられなくなってしまいます。
この Silent
インターフェースは Java
JavaFXのイベントハンドラーとして自作のインターフェースを指定することはできない// このコードはエラーになりコンパイルできません。
button.setOnAction(new Silent<ActionEvent>() {
@Override
public void handle(ActionEvent event) throws Exception {
}
});
Java
そこで、 自作の関数型インターフェース Silent
を Javawrap
メソッド作ります。wrap
メソッドは Silent
インスタンスを引数として取り、 Eventhandle
が呼び出されると、 それはそのまま Silent
インターフェースの handle
呼び出しに委譲されます。Silent
インターフェースの handle
メソッドは検査例外をそのままスローする可能性があるので、 それを try~catch
で吸収して実行時例外 (Runtime
) に変換して再スローしています。これなら、 Java
static <T extends Event> EventHandler<T> wrap(Silent<T> handler) {
return event -> {
try {
handler.handle(event);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
wrap
メソッドを使うと Java
button.setOnAction(wrap(event -> {
myBusinessLogic(); //検査例外をスローするコードをそのまま書けます。
}));
どうですか? 当初のコードに近いだいぶ簡潔なコードになりましたよね。wrap(...)
で包むだけで検査例外をスローするコードを try~catch
なしでそのまま書けるようになりました。もちろん、 例外は握り潰されることなく未処理例外ハンドラーに転送されます。
完成したコード
これが完成したコードです。
Sample.javaimport javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import java.io.IOException;
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
//FXアプリケーションスレッドでキャッチされなかった例外を処理します。
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
while(e instanceof Silent.WrappedException) {
e = e.getCause();
}
showException(e);
});
Button button = new Button("Click Me!");
button.setPrefSize(240, 160);
button.setOnAction(wrap(event -> {
myBusinessLogic();
}));
Scene scene = new Scene(button);
primaryStage.setScene(scene);
primaryStage.show();
}
public void myBusinessLogic() throws IOException {
throw new IOException("oops!");
}
protected void showException(Throwable e) {
e.printStackTrace();
Alert dialog = new Alert(Alert.AlertType.WARNING, e.getMessage());
dialog.setTitle(e.getClass().getSimpleName());
dialog.setHeaderText(null);
dialog.showAndWait();
}
protected <T extends Event> EventHandler<T> wrap(Silent<T> handler) {
return Silent.wrap(handler);
}
@FunctionalInterface
public interface Silent<T extends Event> {
void handle(T event) throws Exception;
static <T extends Event> EventHandler<T> wrap(Silent<T> handler) {
return event -> {
try {
handler.handle(event);
} catch (Exception e) {
throw new Silent.WrappedException(e);
}
};
}
class WrappedException extends RuntimeException {
public WrappedException(Throwable cause) {
super(cause);
}
}
}
}
サンプルコードを含む Gradle プロジェクト一式を下記のリンクからダウンロードできます。
本文での解説から少し手直しをしています。
Runtime
ではなく、 自作の Silent
で検査例外を包むようにしました。Silent
は Runtime
を継承しただけの例外クラスで特別なことは何もしません。汎用的に使われる Runtime
と区別するために別の例外クラスとしました。
こうすることで、 未処理例外ハンドラーで元の例外を取り出しやすくなります。未処理例外ハンドラーでは例外クラスが Silent
の場合、 内包されている原因例外を取り出すようにしています。これなら、 元の例外がきちんとダイアログに表示されます。
//FXアプリケーションスレッドでキャッチされなかった例外を処理します。
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
while(e instanceof Silent.WrappedException) {
e = e.getCause();
}
showException(e);
});
wrap
メソッドと Wrapped
例外クラスはどこに定義しても構わないのですが、 とりあえず Silent
インターフェースに内包させました。Silent
インターフェースの部分だけをコピペで使い回せます。
Javatry~catch
だらけになってしまって悩んでいる方、 ぜひ試してみてくださいね。
おまけ
Java
これは Windows 版 Java
このバグに対処するには、 Javatry~catch
を書くということです。
このバグに対するワークアラウンドを本記事で紹介した wrap
メソッドに組み込むことができます。
static <T extends Event> EventHandler<T> wrap(Silent<T> handler) {
return event -> {
try {
handler.handle(event);
} catch (Exception e) {
throw new Silent.WrappedException(e);
}
};
}
この wrap
メソッドを以下のように変更します。
ドラッグ関連イベントハンドラーで例外が飲み込まれてしまうバグへの対策static <T extends Event> EventHandler<T> wrap(Silent<T> handler) {
return event -> {
try {
handler.handle(event);
} catch (Exception e) {
// ここから
Thread.UncaughtExceptionHandler ueh
= Thread.currentThread().getUncaughtExceptionHandler();
if(ueh != null) {
ueh.uncaughtException(Thread.currentThread(), e);
return;
}
// ここまで
throw new Silent.WrappedException(e);
}
};
}
現在のスレッド (つまり Javauncaught
メソッドを直接呼び出す) のです。
例外を再スローすると (Java
Windows 環境で Java