Обработка исключений в Java в функциональном стиле. Часть 2.
В предыдущей статье была рассмотрена функциональная обработка исключений с помощью интерфейса Try<T>
. Статья вызвала определенный интерес читателей и была отмечена в "Сезоне Java".
В данной статье автор продолжит тему и рассмотрит простую и "грамотную" (literate) обработку исключений при помощи функций высшего порядка без использования каких либо внешних зависимостей и сторонних библиотек.
Решение
Для начала перепишем пример из предыдущей статьи преобразования URL из строкового представления к объектам URL c использованием Optional.
public List<URL> urlList(String[] urls) {
return Stream.of(urls) //Stream<String>
.map(s -> {
try {
return Optional.of(new URL(s));
} catch (MalformedURLException me) {
return Optional.empty();
}
}) //Stream<Optional<>URL>
.flatMap(Optional::stream) //Stream<URL>, filters empty optionals
.toList();
}
Общая схема понятна, однако не хотелось бы писать подобный boilerplate код каждый раз когда мы встречается с функцией выбрасывающей проверяемые исключения.
И тут нам помогут следующие функции:
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
}
/**
* Higher-order function to convert partial function T=>R to total function T=>Optional<R>
* @param <T> function input parameter type
* @param <R> function result type
* @param func partial function T=>R that may throw checked exception
* @return total function T => Optional<R>
*/
static <T, R> Function<T, Optional<R>> toOptional(CheckedFunction<T, R> func) {
return param -> {
try {
return Optional.ofNullable(func.apply(param));
} catch (RuntimeException err) {
throw err; //All runtime exceptions are treated as errors/bugs
} catch (Exception e) {
return Optional.empty();
}
};
}
Дадим некоторые пояснения. CheckedFunction<T, R>
это функция которая может выбросить исключение при преобразовании T => R. Подобные функции в терминах функционального программирования называются частичными (partial) функциями, потому что значение функции не определено при некоторых входных значениях параметра.
Функция toOptional(...)
преобразует частичную (partial) функцию T => R в полную (total) функцию T => Optional. Подобного рода функции, которые принимают параметром и/или возвращают другую функцию, в терминах функционального программирования называются функциями высшего порядка (higher-order function).
С использованием новой функции код примет следующий опрятный вид:
public List<URL> urlList(String[] urls) {
return Stream.of(urls) //Stream<String>
.map(toOptional(URL::new)) //Stream<Optional<URL>>
.flatMap(Optional::stream) //Stream<URL>, filters empty optionals
.toList(); //List<URL>
}
И теперь её можно применять везде где возможны проверяемые (checked) исключения.
List<Number> intList(String [] numbers) {
NumberFormat format = NumberFormat.getInstance();
return Stream.of(numbers) //Stream<String>
.map(toOptional(format::parse)) //Checked ParseException may happen here
.flatMap(Optional::stream) //Stream<Number>
.toList(); //List<Number>
}
Улучшаем обработку исключений
При использовании Optional<T>
пропадает информация о самом исключении. В крайнем случае исключение можно залогировать в теле функции toOptional
, но мы найдем лучшее решение.
Нам нужен любой контейнер, который может содержать значение типа T либо само исключение. В терминах функционального программирования таким контейнером является Either<Exception, T>
, но к сожаления класса Either<L,R>
(как и класса Try<T>
) нет в составе стандартной библиотеки Java.
Вы можете использовать любой подходящий контейнер, которым Вы обладаете. Я же в целях краткости буду использовать следующий
//Require Java 14+
record Result<T>(T result, Exception exception) {
public boolean failed() {return exception != null;}
public Stream<T> stream() {return failed() ? Stream.empty() : Stream.of(result);}
}
Теперь наша функция высшего порядка получит имя toResult
и будет выглядеть так:
static <T, R> Function<T, Result<R>> toResult(CheckedFunction<T, R> func) {
return param -> {
try {
return new Result<>(func.apply(param), null);
} catch (RuntimeException err) {
throw err;
} catch (Exception e) {
return new Result<>(null, e);
}
};
}
А вот и применение новой функции toResult()
List<Number> intListWithResult(String [] numbers) {
NumberFormat format = NumberFormat.getInstance();
return Stream.of(numbers) //Stream<String>
.map(toResult(format::parse)) //Stream<Result<Number>>, ParseException may happen
.peek(this::handleErr) //Stream<Result<Number>>
.flatMap(Result::stream) //Stream<Number>
.toList(); //List<Number>
}
void handleErr(Result r) {
if (r.failed()) {
System.out.println(r.exception());
}
}
Теперь возможное проверяемое исключение сохраняется в контейнере Result и его можно обработать в потоке.
Выводы
Для простой и "грамотной" (literate) обработки проверяемых (checked) исключений в функциональном стиле без использования внешних зависимостей необходимо
Выбрать подходящий контейнер для хранения результата. В простейшем случае это может быть
Optional<T>
. Лучше использовать контейнер который может хранить значение результата или перехваченное исключение.
Написать функцию высшего порядка которая преобразует частичную функцию
T => R
, которая может выбросить исключение, в полную функциюT => YourContainer<R>
и применять ее в случае необходимости.
Ссылки
На github-e
JavaDoc
Source Code
Junit tests
Автор — Сергей А. Копылов
e-mail skopylov@gmail.com