Обработка исключений в Java в функциональном стиле
В данной статье автор предоставит информацию о собственной библиотеке для обработки исключений (Exception) в функциональном стиле.
Предпосылки
В Java начиная с версии 8 появились новые возможности в виде функциональных интерфейсов и потоков (Stream API). Эти возможности позволяют писать код в новом функциональном стиле без явных циклов, временных переменных, условий ветвления и проч. Я уверен что этот стиль программирования станет со временем основным для большинства Java программистов.
Однако применение функционального стиля на практике осложняется тем, что все стандартные функциональные интерфейсы из пакета java.util.function
не объявляют проверяемых исключений (являются checked exception unaware).
Рассмотрим простой пример преобразования URL из строкового представления к объектам URL.
public List urlListTraditional(String[] urls) {
return Stream.of(urls)
.map(URL::new) //MalformedURLException here
.collect(Collectors.toList());
}
К сожалению данный код не будет компилироваться из-за того, что конструктор URL может выбросить MalformedURLException
. Правильный код будет выглядеть следующим образом
public List urlListTraditional(String[] urls) {
return Stream.of(urls)
.map(s -> {
try {
return new URL(s);
} catch (MalformedURLException me) {
return null;
}
}).filter(Objects::nonNull)
.collect(Collectors.toList());
}
Мы должны явно обработать MalformedURLException
, вернуть null
из лямбды, а затем отфильтровать нулевые значения в потоке. Увы, такой код на практике нивелирует все преимущества функционального подхода.
При использовании функционального подхода к обработке исключений, код может выглядеть гораздо приятней.
public List urlListWithTry(String[] urls) {
return Stream.of(urls)
.map(s -> Try.of(() -> new URL(s)))
.flatMap(Try::stream)
.collect(Collectors.toList());
}
Итак, по порядку про Try
Интерфейс Try
Интерфейс Try
представляет собой некоторое вычисление, которое может завершиться успешно с результатом типа T или неуспешно с исключением. Try
очень похож на Java Optional
, который может иметь результат типа T или не иметь результата вообще (иметь null значение).
Объекты Try
создаются с помощью статического фабричного метода Try.of(...)
который принимает параметром поставщика (supplier) значения типа T, который может выбросить любое исключение Exception.
Try url = Try.of(() -> new URL("foo"));
Каждый объект Try
находится в одном из двух состояний — успеха или неудачи, что можно узнать вызывая методы Try#isSuccess()
или Try#isFailure()
.
Для логирования исключений подойдет метод Try#onFailure(Consumer
, для обработки успешных значений — Try#.onSuccess(Consumer
.
Многие методы Try
возвращают также объект Try, что позволяет соединять вызовы методов через точку (method chaining). Вот пример как можно открыть InputStream от строкового представления URL в несколько строк без явного использования try/catch
.
Optional input =
Try.success(urlString) //Factory method to create success Try from value
.filter(Objects::nonNull) //Filter null strings
.map(URL::new) //Creating URL from string, may throw an Exception
.map(URL::openStream) //Open URL input stream, , may throw an Exception
.onFailure(e -> logError(e)) //Log possible error
.optional(); //Convert to Java Optional
Интеграция Try с Java Optional и Stream
Try
легко превращается в Optional
при помощи метода Try#optional()
, так что в случае неуспешного Try вернется Optional.empty
.
Я намеренно сделал Try
API восьма похожим на Java Optional
API. Методы Try#filter(Predicate
и Try#map(Function
имеют аналогичную семантику соответствующих методов из Optional. Так что если Вы знакомы с Optional
, то Вы легко будете работать с Try
.
Try
легко превращается в Stream
при помощи метода Try#stream()
точно так же, как это сделано для Optional#stream()
. Успешный Try превращается в поток (stream) из одного элемента типа T, неуспешный Try — в пустой поток.
Фильтровать успешные попытки в потоке можно двумя способами — первый традиционный с использованием Try#filter()
...
.filter(Try::isSuccess)
.map(Try::get)
...
Второй короче — при помощи Try#stream()
...
.flatMap(Try::stream)
...
будет фильтровать в потоке неуспешные попытки и возвращать поток успешных значений.
Восстановление после сбоев (Recovering from failures)
Try
имеет встроенные средства recover(...)
для восстановления после сбоев если вы имеете несколько стратегий для получения результата T. Предположим у Вас есть несколько стратегий:
public T planA();
public T planB();
public T planC();
Задействовать все три стратегии/плана одновременно в коде можно следующим образом
Try.of(this::planA)
.recover(this::planB)
.recover(this::planC)
.onFailure(...)
.map(...)
...
В данном случае сработает только первый успешный план (или ни один из них). Например, если план А не сработал, но сработал план Б, то план С не будет выполняться.
Работа с ресурсами (Try with resources)
Try
имплементирует AutoCloseable интерфейс, а следовательно Try
можно использовать внутри try-with-resource
блока. Допустим нам надо открыть сокет, записать несколько байт в выходной поток сокета и затем закрыть сокет. Соответствующий код с использованием Try
будет выглядеть следующим образом.
try (var s = Try.of(() -> new Socket("host", 8888))) {
s.map(Socket::getOutputStream)
.onSuccess(out -> out.write(new byte[] {1,2,3}))
.onFailure(e -> System.out.println(e));
}
Сокет будет закрыт при выходе за последнюю фигурную скобку.
Выводы
Try
позволяет обрабатывать исключения в функциональном стиле без явного использования конструкций try/catch/finally
и поможет сделать Ваш код более коротким, выразительным, легким для понимания и сопровождения.
Надеюсь Вы получите удовольствие от использования Try
Ссылки
Автор — Сергей А. Копылов
e-mail skopylov@gmail.com
Последнее место работы
Communications Solutions CMS IUM R&D Lab
Hewlett Packard Enterprise
Ведущий специалист
Код библиотеки на github
Try JavaDoc
Еще одна функциональная библиотека для Java