🌶️

Lombok @SneakyThrowの挙動

に公開

lombokの@SneakyThrowの挙動について

はじめに

Springなどでよくつかわれる便利なlombokライブラリの@SneakyThrowについて、
どのような挙動をするのか解説します。
@SneakyThrowは検査例外をcatchしなくてもよくするために使われるアノテーションです。

@SneakyThrowとは

@SneakyThrowを使うと例外をスローするメソッドを呼び出す際に、例外をcatchしなくてもよくなります。

    @SneakyThrows
    public void throwException() {
        throw new Exception();
        //このメソッドを呼び出しても、例外をcatchしなくてもよくなる!
    }

上記のメソッドを呼び出しても、呼び出しもとでは例外をcatchしなくてもよくなります。
通常、検査例外がthrowされる場合はメソッド内、もしくは呼び出しもとでcatchする必要があり、
例外をcatchしない場合はコンパイルエラーとなります。
@SneakyThrowはコンパイル時にソースを変換してこのコンパイルエラーを回避できる状態にしてくれます。

コンパイルしたソースの実行時には、検査例外がthrowされてしまうため(上記コードではException)、
呼び出し元で例外をcatchしないと、実行時には処理が中断されてしまいます。

裏側で何が起こっているのか?

ここからが今回の解説の本題となります、コンパイル時に変換されて生成されるソースを確認してみます。

    public void throwException() {
        try {
            throw new Exception();
        } catch (final java.lang.Throwable $ex) {
            throw lombok.Lombok.sneakyThrow($ex);
        }
    }

例外がthrowされる部分で実際にはtry-catch文が生成されており、
例外がthrowされると、catch文の中でlombok.Lombok.sneakyThrow($ex)が実行されます。

このlombok.Lombok.sneakyThrow($ex)は以下のようなコードになります。

    public static RuntimeException sneakyThrow(Throwable t) {
        if (t == null) throw new NullPointerException("t");
        return Lombok.<RuntimeException>sneakyThrow0(t);
    }
    
    @SuppressWarnings("unchecked")
    private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T {
        throw (T)t;
    }

lombok.Lombok.sneakyThrow($ex)では、引数に渡された例外をそのままthrowしています。
ただし、例外をthrowする際に、型をジェネリクスのT型に変換してthrowしています。
これは一見すると、RuntimeException型に変換してthrowしているように見えますが、
実際には変換しているわけではなく、引数に渡された例外をそのままthrowしているだけです。

Javaのジェネリクスの型情報はコンパイル時に消去されるという仕様のため、
実行時には<RunTimeException>(T)、などの記載はなくなり、
引数に渡された例外をそのままthrowすることになります。

まとめ

以上を踏まえると、@SneakyThrowはコンパイル時に検査例外を非検査例外であるかのように
コンパイラーを騙して、コンパイルを通すアノテーションとなっています。
実際には、例外がそのままthrowされるため絶対に例外がthrowされない場面で使用するか、
例外をthrowされても問題ない用に呼び出し元のコードを書く必要があります。

ラムダ関数やStreamAPIなどで、例外をthrowするメソッドを使用する際に
@SneakyThrowを使用すると便利な場面もあるため、安全に使うためにも仕組みを理解しておく必要があります。
この記事が参考になれば幸いです。

Discussion