Рубрики
Без рубрики

Обработка исключений в потоках Java

Потоковый API и лямбда-интерфейс значительно улучшили Java с версии 8. С этого момента мы начинаем… Помеченный java, качество кода.

Потоковый API и лямбда-интерфейс значительно улучшили Java с версии 8. С этого момента мы могли бы работать с более функциональным синтаксическим стилем. Теперь, после нескольких лет работы с этими конструкциями кода, одна из самых больших проблем, которая остается, заключается в том, как работать с проверяемыми исключениями внутри лямбды.

Как вы все, наверное, знаете, невозможно вызвать метод, который выдает проверенное исключение из лямбды напрямую. Каким-то образом нам нужно перехватить исключение, чтобы код скомпилировался. Естественно, мы можем выполнить простую попытку перехвата внутри лямбды и обернуть исключение в RuntimeException , как показано в первом примере, но я думаю, что мы все можем согласиться с тем, что это не лучший путь.

myList.stream()
  .map(item -> {
    try {
      return doSomething(item);
    } catch (MyException e) {
      throw new RuntimeException(e);
    }
  })
  .forEach(System.out::println);

Большинство из нас знают, что блочные лямбды неуклюжи и менее удобочитаемы. На мой взгляд, их следует избегать, насколько это возможно. Если нам нужно выполнить больше одной строки, мы можем извлечь тело функции в отдельный метод и просто вызвать новый метод. Лучший и более читаемый способ решить эту проблему – обернуть вызов в простой старый метод, который выполняет попытку перехвата и вызывает этот метод из вашей лямбды.

myList.stream()
  .map(this::trySomething)
  .forEach(System.out::println);
private Item trySomething(Item item) {
  try {
    return doSomething(item);
  } catch (MyException e) {
    throw new RuntimeException(e);
  }
}

Это решение, по крайней мере, немного более читабельно, и мы разделяем наши проблемы. Если вы действительно хотите поймать исключение и сделать что-то конкретное, а не просто обернуть исключение в RuntimeException , это может быть возможным и удобным решением для вас.

Исключение времени выполнения

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

Я могу немного рассказать об этой практике, потому что лично я не вижу большой ценности в проверенных исключениях в целом, но это совершенно другое обсуждение, которое я не собираюсь здесь начинать. Если вы хотите обернуть каждый вызов в лямбду, у которой есть отметка в Исключение RuntimeException , вы увидите, что повторяете один и тот же шаблон. Чтобы не переписывать один и тот же код снова и снова, почему бы не абстрагировать его в служебную функцию? Таким образом, вам нужно написать его только один раз и вызывать каждый раз, когда он вам понадобится.

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

@FunctionalInterface
public interface CheckedFunction {
    R apply(T t) throws Exception;
}

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

public static  Function wrap(CheckedFunction checkedFunction) {
  return t -> {
    try {
      return checkedFunction.apply(t);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  };
}

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

myList.stream()
       .map(wrap(item -> doSomething(item)))
       .forEach(System.out::println);

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

Любой

При работе с потоками мы, вероятно, не хотим прекращать обработку потока в случае возникновения исключения. Если ваш поток содержит очень большое количество элементов, которые необходимо обработать, хотите ли вы, чтобы этот поток завершался, когда, например, второй элемент выдает исключение? Скорее всего, нет.

Давайте изменим наш образ мышления. Почему бы не рассматривать “исключительную ситуацию” в такой же степени как возможный результат, как мы бы рассматривали “успешный” результат. Давайте рассмотрим это как данные, продолжая обрабатывать поток, а затем решим, что с ним делать. Мы можем это сделать, но чтобы сделать это возможным, нам нужно ввести новый тип — Любой тип.

Любой тип является распространенным типом в функциональных языках и (пока) не является частью Java. Аналогично необязательному типу в Java, ан Либо является универсальной оболочкой с двумя возможностями. Это может быть либо левый, либо Правильно, но никогда не одновременно. Как левый, так и правый могут быть любых типов. Например, если у нас есть значение Либо, это значение может содержать либо что-то типа String, либо целое число, Либо<Строка,Целое число> .

Если мы используем этот принцип для обработки исключений, мы можем сказать, что наш Любой тип содержит либо исключение, либо значение. Для удобства, как правило, левое значение является исключением, а правое – успешным значением. Вы можете запомнить это, думая о правом не только как о правой стороне, но и как о синониме “хорошо”, “хорошо” и т. Д.

Ниже вы увидите базовую реализацию Либо введите. В этом случае я использовал Необязательный тип, когда мы пытаемся получить левое или правое, потому что мы:

public class Either {
    private final L left;
    private final R right;
    private Either(L left, R right) {
        this.left = left;
        this.right = right;
    }
    public static  Either Left( L value) {
        return new Either(value, null);
    }
    public static  Either Right( R value) {
        return new Either(null, value);
    }
    public Optional getLeft() {
        return Optional.ofNullable(left);
    }
    public Optional getRight() {
        return Optional.ofNullable(right);
    }
    public boolean isLeft() {
        return left != null;
    }
    public boolean isRight() {
        return right != null;
    }
    public  Optional mapLeft(Function mapper) {
        if (isLeft()) {
            return Optional.of(mapper.apply(left));
        }
        return Optional.empty();
    }
    public  Optional mapRight(Function mapper) {
        if (isRight()) {
            return Optional.of(mapper.apply(right));
        }
        return Optional.empty();
    }
    public String toString() {
        if (isLeft()) {
            return "Left(" + left +")";
        }
        return "Right(" + right +")";
    }
}

Теперь вы можете заставить свои собственные функции возвращать Либо вместо того , чтобы создавать исключение | . Но это не поможет вам, если вы хотите использовать существующие методы, которые генерируют проверенное исключение внутри лямбды, верно? Поэтому мы должны добавить крошечную служебную функцию к любому типу, который я описал выше.

public static  Function lift(CheckedFunction function) {
  return t -> {
    try {
      return Either.Right(function.apply(t));
    } catch (Exception ex) {
      return Either.Left(ex);
    }
  };
}

Добавив этот метод статического подъема в Либо , теперь мы можем просто “поднять” функцию, которая выдает проверенное исключение, и позволить ей возвращать Либо . Если мы возьмем исходную проблему, то теперь мы получим поток Либо вместо возможного Исключение RuntimeException , которое может взорвать весь мой Поток .

myList.stream()
       .map(Either.lift(item -> doSomething(item)))
       .forEach(System.out::println);

Это просто означает, что мы вернули себе контроль. Используя функцию фильтра в API потока, мы можем просто отфильтровать оставленные экземпляры и, например, зарегистрировать их. Вы также можете отфильтровать нужные экземпляры и просто игнорировать исключительные случаи. В любом случае, вы снова контролируете ситуацию, и ваш поток не завершится мгновенно, когда возможно Возникает исключение RuntimeException .

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

public static  Function liftWithValue(CheckedFunction function) {
  return t -> {
    try {
      return Either.Right(function.apply(t));
    } catch (Exception ex) {
      return Either.Left(Pair.of(ex,t));
    }
  };
}

Вы видите, что в этом функция lift со значением внутри типа Pair используется для сопряжения как исключения, так и исходного значения слева от Либо . Теперь у нас есть вся информация, которая нам может понадобиться, если что-то пойдет не так, вместо того, чтобы иметь только исключение | .

Используемый здесь тип Pair является еще одним универсальным типом, который можно найти в библиотеке коллекций Eclipse, или вы можете просто реализовать свой собственный. В любом случае, это просто тип, который может содержать два значения.

public class Pair {
    public final F fst;
    public final S snd;
    private Pair(F fst, S snd) {
        this.fst = fst;
        this.snd = snd;
    }
    public static  Pair of(F fst, S snd) {
        return new Pair<>(fst,snd);
    }
}

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

Попробуй

Люди, которые, возможно, работали, например, с Scala, могут использовать Попробуйте вместо тот Либо для обработки исключений. Тип Try – это то, что очень похоже на Либо введите. У него, опять же, два случая: “успех” или “неудача”. Ошибка может содержать только исключение типа, в то время как успех может содержать любой тип, который вы хотите. Итак, Try – это не что иное, как конкретная реализация Либо , где левый тип (сбой) исправлен до типа Исключение .

public class Try {
    private final Exception failure;
    private final R succes;
    public Try(Exception failure, R succes) {
        this.failure = failure;
        this.succes = succes;
    }
}

Некоторые люди убеждены, что его проще использовать, но я думаю, что, поскольку мы можем содержать только Исключение в части сбоя, у нас та же проблема, что и в первой части Либо раздел. Лично мне нравится гибкость Либо введите больше. В любом случае, в обоих случаях, если вы используете Попробуйте или Либо , вы решаете первоначальную проблему обработки исключений и не позволяете вашему потоку завершаться из-за исключения RuntimeException.

Библиотеки

Оба Либо и Попробуйте очень легко реализовать самостоятельно. С другой стороны, вы также можете ознакомиться с доступными функциональными библиотеками. Например, в VAVR (ранее известном как Javaslang) есть реализации как для типов, так и для вспомогательных функций. Я действительно советую вам взглянуть на него, потому что в нем содержится гораздо больше, чем только эти два типа. Однако вы должны задать себе вопрос о том, нужна ли вам эта большая библиотека в качестве зависимости только для обработки исключений, когда вы можете реализовать ее самостоятельно с помощью всего нескольких строк кода.

Вывод

Когда вы хотите использовать метод, который выдает проверенное исключение , вам нужно сделать что-то дополнительное, если вы хотите вызвать его в лямбде. Упаковка его в исключение RuntimeException может быть решением, позволяющим заставить его работать. Если вы предпочитаете использовать этот метод, я настоятельно рекомендую вам создать простой инструмент-оболочку и использовать его повторно, чтобы вас не беспокоил try/catch каждый раз.

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

Оригинал: “https://dev.to/brianverm/exception-handling-in-java-streams-2mjh”