📝

Stream/Optionalで例外をthrowするとコンパイルエラーになる(回避策)

2022/09/27に公開約3,700字

はじめに

ラムダ式の検査例外を非検査化する方法を調べた際のメモです

前提

Javaのラムダ式にて検査例外(Exceptionを継承した例外)を処理した場合

	private void func1() throws java.io.IOException {
		java.util.List.of(1, 2, 3)
				// 例外が実際に発生した場合の想定
				.forEach(e -> {
					throw new java.io.IOException();
				});

		java.util.Optional.ofNullable("val")
				// 例外が実際に発生した場合の想定
				.map(val -> {
					throw new java.io.IOException();
				});
	}

IOExceptionを上位にthrowしたため、ラムダ内部のIOExceptionも上位のクラス内部で処理したいところですが、コンパイルエラーとしてビルドに失敗します。

実装

上記を回避するためには検査例外をビルドに通すか内部で都度例外発生時に処理をしなければなりません。
そのため、java-sneaky-throwsという方法で例外の発生を隠蔽してしまうという実装方法です。

import java.util.function.Consumer;
import java.util.function.Function;

public class SneakyThrowTest {

	@org.junit.jupiter.api.Test
	public void test() throws java.io.IOException {
		try {
			func1();
		} catch (java.io.IOException e) {
			System.out.println("func1 is thow IOException");
		}
		try {
			func2();
		} catch (java.io.IOException e) {
			System.out.println("func2 is thow IOException");
		}
	}

	/**
	 * 検査対象例外付きの関数型(ラムダ式)実装例
	 * @throws java.io.IOException 検査例外化する場合はThrowに追加
	 */
	private void func1() throws java.io.IOException {
		// 正常系
		java.util.List.of(1, 2, 3).stream()
				// 例外ラップ関数使用時のFunction実装
				.map(SneakyThrowUtil.rethrowFunc(val -> {
					return "val" + val;
				}))
				// 例外ラップ関数使用時のConsumer実装
				.forEach(SneakyThrowUtil.rethrowCons(System.out::println));

		// 異常系
		java.util.List.of(1, 2, 3)
				// 例外が実際に発生した場合の想定
				.forEach(SneakyThrowUtil.rethrowCons(e -> {
					throw new java.io.IOException();
				}));
	}

	/**
	 * 検査対象例外付きの関数型(ラムダ式)実装例
	 * @throws java.io.IOException 検査例外化する場合はThrowに追加
	 */
	private void func2() throws java.io.IOException {
		// 正常系
		java.util.Optional.ofNullable("val")
				// 例外ラップ関数使用時のFunction実装
				.map(SneakyThrowUtil.rethrowFunc(val -> {
					return val + 1;
				}))
				// 例外ラップ関数使用時のConsumer実装
				.ifPresent(SneakyThrowUtil.rethrowCons(System.out::println));

		// 異常系
		java.util.Optional.ofNullable("val")
				// 例外が実際に発生した場合の想定
				.map(SneakyThrowUtil.rethrowFunc(val -> {
					throw new java.io.IOException();
				}));
	}
}

/**
 * 関数型処理の非検査例外用Utilty
 */
final class SneakyThrowUtil {

	/**
	 * Constructor
	 */
	private SneakyThrowUtil() {
	}

	/**
	 * sneakyThrowを行う拡張interface
	 */
	public interface ThrowingConsumer<T> extends Consumer<T> {

		@Override
		default void accept(final T e) {
			try {
				accept0(e);
			} catch (Throwable ex) {
				SneakyThrowUtil.sneakyThrow(ex);
			}
		}

		// 例外をThows可能なaccept0実装
		void accept0(T e) throws Throwable;
	}

	/**
	 * sneakyThrowを行う拡張interface
	 */
	public interface ThrowingFunction<T, R> extends Function<T, R> {
		@Override
		default R apply(T e) {
			try {
				return apply0(e);
			} catch (Throwable ex) {
				SneakyThrowUtil.sneakyThrow(ex);
			}

			// 必ず処理又はThrowされるため到達不能
			return null;
		}

		// 例外をThows可能なapply実装
		R apply0(T t) throws Throwable;
	}

	/**
	 * 関数型メソッドを非検査例外化する関数
	 */
	public static <T> Consumer<T> rethrowCons(final ThrowingConsumer<T> consumer) {
		return consumer;
	}

	/**
	 * 関数型メソッドを非検査例外化する関数
	 */
	public static <T, R> Function<T, R> rethrowFunc(final ThrowingFunction<T, R> func) {
		return func;
	}

	/**
	 * 非検査例外としてcompileを通すsneaky-throws処理
	 * @see {@linkplain <a href="https://www.baeldung.com/java-sneaky-throws">www.baeldung.com</a>}
	 */
	@SuppressWarnings("unchecked")
	private static <E extends Throwable> void sneakyThrow(Throwable ex) throws E {
		throw (E) ex;
	}
}

上記のやり方でラムダ内での例外発生を無視をし、
func1(),func2()を実行した際に上位で期待した例外をcatchすることができます。

終わりに

非検査例外が検査例外となった際などの応急処置としては有用だと思います。

「検査例外を無理やり非検査例外化する」処理を入れて対応するのはいかがなものかという気はしますが、
対象の処理内で発生する例外を上位にthrowする、またはtry-catchにて発生する可能性がある例外をきちんと処理する等の実装者のモラル的なところにもなるかと思いますので、ある程度メンバーがJavaに慣れている環境や一人親方での実装の際は検討してもいいと思います。

Discussion

ログインするとコメントできます