Pull to refresh

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

Reading time 5 min
Views 8.5K
UPD: Добавлен пример с ленивыми вычислениями поверх стандартных стримов.

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

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

public interface FunctionWithExceptions<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, FunctionWithExceptions<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), но там это неудобно и поэтому довольно бессмысленно.

UPD:

Ленивые вычисления


Для поддержки ленивых вычислений создаём такую обертку вокруг стандартного стрима (для примера только два метода, остальные реализуются аналогичным образом).

public class ErStream<S, T extends Throwable> {
	private final Stream<S> mainStream;
	public ErStream(Stream<S> mainStream) {
		this.mainStream = mainStream;
	}
	public static <S, R, T extends Throwable, T1 extends T, T2 extends T> ErStream<R, T> map(ErStream<S, T1> erStream, FunctionWithExceptions<S, R, T2> function) {
		Function<S, R> f = uncheck(function);
		return new ErStream<>(erStream.mainStream.map(f));
	}
	public static <S, T extends Throwable> Optional<S> findAny(ErStream<S, T> erStream) throws T {
		return erStream.mainStream.findAny();
	}
	//Взято у Djaler
	private static <S, R> Function<S, R> uncheck(FunctionWithExceptions<S, R, ?> function) {
		return t -> {
			try {
				return function.apply(t);
			} catch (Throwable exception) {
				throwAsUnchecked(exception);
				return null;
			}
		};
	}
	//Взято у Djaler
	@SuppressWarnings("unchecked")
	private static <E extends Throwable> void throwAsUnchecked(Throwable exception) throws E {
		throw (E) exception;
	}

И используем следующим образом (для компактности все примеры в одном блоке кода):

private static byte[] read(String filename) throws EOFException {
	return null;
}
private static String search(String filename) throws FileNotFoundException {
	return filename;
}
public static void main(String[] args) {
	List<String> list = Arrays.asList("1.txt");
	//autogenerated code with wrong generic types
	//ErStream<Object, Throwable> temp = map(new ErStream<>(list.stream()), s -> search(s));
	//store single exception
	ErStream<String, FileNotFoundException> temp = map(new ErStream<>(list.stream()), s -> search(s));
	try {//exception will be thrown here
		findAny(temp);
	} catch (FileNotFoundException e) {/*NOTHING*/}
	temp = map(new ErStream<>(list.stream()), s -> search(s));
	try {//most general exception will be thrown here
		findAny(map(temp,s -> read(s)));
	} catch (IOException e) {/*NOTHING*/}
	//without exception, nothing thrown
	findAny(map(new ErStream<>(list.stream()), i -> i + 1));
}

В данном примере ошибка выбрасывается из терминальной операции и ErStream может быть сохранен в переменную (к сожалению, у Eclipse 4.6.3 вместо правильных generic-типов выводит наиболее общие типы <Object, Throwable> и их приходится указывать самому) или передан в другую функцию. Так же выводить общий тип для объединения нескольких исключений корректно получилось только в статических методах. Так же при сохранении ErStream не генерирующего ошибку в локальную переменную generic-тип ошибки будет RuntimeException что при объединении её с другими ошибками выведет общий тип как Exception (при записи функций в цепочку такой проблемы нет).

Ссылка на git с исходным кодом и junit тестом в котором проверяются ситуации аналогичные описанным в статье.
Tags:
Hubs:
+11
Comments 7
Comments Comments 7

Articles