Обработка исключений в Java в функциональном стиле

84ebf5a63802edad5cd008e4ddfc657d

В данной статье автор предоставит информацию о собственной библиотеке для обработки исключений (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

© Habrahabr.ru