Generic исключения в лямбда-функциях

в 6:26, , рубрики: exception, generic, java, stream api, обработка исключений, функциональные интерфейсы

Как известно из функциональных интерфейсов в Stream API нельзя выбрасывать контролируемые исключения. Если по каким-то причинам это необходимо (например, работа с файлами, базами данных или по сети), приходится оборачивать их в RuntimeException. Это неплохо работает если ошибки игнорируются, но если их необходимо обрабатывать, то код получается громоздкий и трудночитаемый. Я заинтересовался можно ли объявлять интерфейсы и методы с generic исключениями и неожиданно для себя узнал, что можно.

Зададим такой функциональный интерфейс, от стандартного интерфейса Function<A, B> он отличается только наличием третьего generic-типа для бросаемого исключения.

public interface ThrowableFunction<A, B, T extends Throwable>{
	B apply(A a) throws T;
}

И объявим простенький метод, который преобразует коллекцию используя этот интерфейс, у этого метода также объявлен generic-тип для бросаемого исключения (совпадающий с типом исключения которое может выбросить функциональный интерфейс).

public static <A, B, T extends Throwable> Collection<B> map(Collection<A> source, ThrowableFunction<A, B, T> function) throws T {
	Collection<B> result = new ArrayList<>();
	for (A a : source) {
		result.add(function.apply(a));
	}
	return result;
}

Посмотрим, как будет выглядит обработка исключений с ними в разных случаях.

Одно исключение

Попробуем преобразовать коллекцию используя лямбда функцию выбрасывающую обрабатываемое исключение, за счет generic-типа оно будет корректно передано в место вызова метода map. При этом тип исключения будет сохранён.

public Collection<byte[]> singleException(Collection<String> filenames) throws IOException
{
	return map(filenames, f -> Files.readAllBytes(new File(f).toPath());
}

Два исключения в одной лямбда-функции

Если мы используем функцию, выбрасывающую несколько обрабатываемых исключений, то они будет приведены к наиболее общему типу, что не очень хорошо (но, по-моему, не хуже завертывания исключений в RuntimeException).

public static byte[] waitAndRead(String filename, long time) throws InterruptedException, IOException {
	Thread.sleep(time);
	return Files.readAllBytes(new File(filename).toPath());
}
public Collection<byte[]> joinedExceptions(Collection<String> filenames) throws Exception
{
	return map(filenames, f -> waitAndRead(f, 1000L));
}

Исключения в разных лямбда-функциях

Однако если мы запишем цепочку лямбда функций каждая из которых выбрасывает не более одного исключения, то разумеется они не будут объединены и могут быть корректно обработаны по отдельности.

private <T> T wait(T t, long time) throws InterruptedException {
	Thread.sleep(time);
	return t;
}
private byte[] read(String filename) throws IOException {
	return Files.readAllBytes(new File(filename).toPath());
}
public Collection<byte[]> separatedExceptions(Collection<String> filenames) throws InterruptedException {
	try {
		return map(map(filenames, f -> wait(f, 1000L)), f -> read(f));
	} catch (IOException e) {
		return Collections.emptyList();
	}
}

Как видно в этом примере в IOException мы перехватываем и возвращаем пустую коллекцию, а InterruptedException передаём выше.

Лямбда-функция без исключений

И наконец посмотрим, как поведет себя функция которая не выбрасывает контролируемых исключений, не потребует ли она обрабатывать исключение которого нет?

public Collection<Boolean> noExceptions(Collection<String> filenames) 
{
	return Mapper.map(filenames, f -> new File(f).exists());
}

Всё работает замечательно и нет необходимости обрабатывать исключения. Интересно что при этом generic-тип исключения раскрылся в RuntimeException автоматически, что в принципе логично, но немного неожиданно.

Недостатки

Главным минусом описанного выше подхода является несовместимость с Stream API из-за невозможности использовать интерфейсы с generic исключениями вместо стандартных. Потенциально можно написать ThrowableStream API по аналогии StreamEx или расширить StreamEx, но это потребует написания большого объёма тривиального кода. Вторым минусом является, то что объявить больше одного generic исключения нельзя.

Кстати использовать исключения в классах с generic типами можно и на более ранних версиях Java (проверил на 1.7), но там это неудобно и поэтому довольно бессмысленно.

Ссылка на git с исходным кодом и junit тестом в котором проверяются ситуации аналогичные описанным в статье.
github.com/igormich/ThrowableLambdas

Автор: Игорь Михайлов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js