Throwing exceptions in java lambda

How to Handle Checked Exceptions With Lambda Expression

Join the DZone community and get the full member experience.

Lambda expressions are all about how we write code. It’s about how we can write more concise, smaller, and less boilerplate code. However, this aforementioned statement may not seem to be true in the case of exceptions in Java.

In Java, we can only handle exceptions through the try-catch block, and this hasn’t changed for the lambda expression.

Let’s say we’re going to develop a simple web crawler. The crawler will take a list of URLs in a string as an argument and save the content of the text file in a text file. Let’s do this.

import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.UUID; public class WebCrawler < public static void main(String[] args) < ListurlsToCrawl = List.of(“https://masterdevskills.com"); WebCrawler webCrawler = new WebCrawler(); webCrawler.crawl(urlsToCrawl); > public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(urlToCrawl ->new URL(urlToCrawl)) .forEach(url -> save(url)); > private void save(URL url) throws IOException < String uuid = UUID.randomUUID().toString(); InputStream inputStream = url.openConnection().getInputStream(); Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING); >>

The above code is simple and intuitive. We used Stream and the first map method converts string URL to java.net.URL object and then passed it to the forEach method, which saves it in a text file. We have used a lambda expression in the crawl method. However, the above code won’t compile. The reason is that we didn’t handle the checked exceptions. The constructor of java.net.URL class throws the MalformedURLException checked exception. And, our private save method also throws checked exceptions, which is the IOException .

Читайте также:  Php pear or zend

Let’s handle the exceptions.

public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(urlToCrawl -> < try < return new URL(urlToCrawl); >catch (MalformedURLException e) < e.printStackTrace(); >return null; >) .forEach(url -> < try < save(url); >catch (IOException e) < e.printStackTrace(); >>); >

Lambda expressions are supposed to be concise, smaller, and crisp, but none of these apply to the aforementioned code. So here, we have a problem.

Let’s rewrite the whole program and make our lambda crisp.

import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Objects; import java.util.UUID; public class WebCrawler < public static void main(String[] args) < ListurlsToCrawl = List.of("https://masterdevskills.com"); WebCrawler webCrawler = new WebCrawler(); webCrawler.crawl(urlsToCrawl); > public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(this::createURL) .forEach(this::save); >private URL createURL(String urlToCrawl) < try < return new URL(urlToCrawl); >catch (MalformedURLException e) < e.printStackTrace(); >return null; > private void save(URL url) < try < String uuid = UUID.randomUUID().toString(); InputStream inputStream = url.openConnection().getInputStream(); Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING); >catch (IOException e) < e.printStackTrace(); >> >

Now look carefully at the crawl() method. We replaced the lambda expression with the method reference. It’s now more concise, smaller, and crisp. However, we were not able to solve the problem of handling the exception, we just moved it to a different place.

We have another problem here, we handle the exception in the method with the try-catch block in places but did not delegate the exception up the stack of the method call where we actually called the crawl() method.

We can solve this problem by re-throwing the checked exception using RuntimeException , which will work since we don’t have to handle runtime exception if we don’t want to and our lambda expression will remain concise.

public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(this::createURL) .forEach(this::save); >private URL createURL(String urlToCrawl) < try < return new URL(urlToCrawl); >catch (MalformedURLException e) < throw new RuntimeException(e); >> private void save(URL url) < try < String uuid = UUID.randomUUID().toString(); InputStream inputStream = url.openConnection().getInputStream(); Files.copy(inputStream, Paths.get(uuid + ".txt"), StandardCopyOption.REPLACE_EXISTING); >catch (IOException e) < throw new RuntimeException(e); >>

The solution seems to work, but the amount of boilerplate code didn’t reduce. Let’s work on that now.

The map() method of stream takes a functional interface. We can write a similar functional interface, which would use a checked exception. Let’s do that.

@FunctionalInterface public interface ThrowingFunction

This functional interface has three generic types, including one that extends a Throwable . Since Java 8, an interface can have static methods, let’s write one here.

@FunctionalInterface public interface ThrowingFunction  < R apply(T t) throws E; static Function unchecked(ThrowingFunction f) < return t -> < try < return f.apply(t); >catch (Throwable e) < throw new RuntimeException(e); >>; > >

The above, unchecked method takes a ThrowingFunction and handles the exception, which, in turn, throws a RuntimeException and returns a Function .

Let’s use in our lambda expression:

public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(ThrowingFunction.unchecked(urlToCrawl ->new URL(urlToCrawl))) .forEach(this::save); >

In the map method, the ThrowingFunction.unchecked() handles the exception inside it and returns a Function and map method that uses it. This solves no more boilerplate around, and we can easily reuse this new ThrowingFunction functional interface anywhere we want.

Now, let’s take care of the forEach method of the stream API. It takes a Consumer . Here, we can also have a new ThrowingConsumer similar to the previous one.

public interface ThrowingConsumer  < void accept(T t) throws E; static Consumer unchecked(ThrowingConsumer consumer) < return (t) -> < try < consumer.accept(t); >catch (Throwable e) < throw new RuntimeException(e); >>; > >
public void crawl(List urlsToCrawl) < urlsToCrawl.stream() .map(ThrowingFunction.unchecked(urlToCrawl ->new URL(urlToCrawl))) .forEach(ThrowingConsumer.unchecked(url -> save(url))); > private void save(URL url) throws IOException

Now in our code, there is no try-catch block, no boilerplate code. We can use method reference to make it crisper.

public void crawl(List urlsToCrawl)

In conclusion, it’s still debatable whether or not we need a checked exception. However, plenty of software projects have been delivered without checked exceptions till date. Having said that, the decisions that developers made when the language was being created impact our way of writing code today, which we cannot ignore. Java 8 changed our way of writing code. For that, we can just ignore the debate and use the above techniques when we need to deal with checked exceptions in the lambda expression.

Published at DZone with permission of A N M Bazlur Rahman , DZone MVB . See the original article here.

Opinions expressed by DZone contributors are their own.

Источник

Исключения в лямбда-выражениях Java

В Java есть проверяемые исключения (checked exceptions), которые нужно указывать в сигнатуре метода, и непроверяемые (unchecked exceptions) — не требующие этого. По сравнению с более ранними подходами концепция проверяемых исключений казалась революционной. Но с течением времени стало понятно, что эта идея не прижилась, и Java остался единственным распространенным языком программирования с проверяемыми исключениями. Например, в Kotlin все исключение непроверяемые.

Да и новые фичи Java не используют проверяемые исключения: в сигнатуре стандартных функциональных интерфейсов исключения не указаны. Использование лямбда-выражений в легаси коде часто приводит к громоздкому коду. Особенно это заметно в Stream API.

В этом посте я хотел бы рассмотреть варианты решения этих проблем.

Пример проблемного кода

Рассмотрим следующий пример кода:

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(it -> new ForNamer().apply(it)) // 1 .forEach(System.out::println);
  1. Этот код не компилируется — возникает ошибка о необходимости обработки проверяемого исключения ClassNotFoundException

Для решения этой проблемы добавим try/catch.

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(it -> < try < return Class.forName(it); >catch (ClassNotFoundException e) < throw new RuntimeException(e); >>) .forEach(System.out::println);

Но добавление try/catch совсем не улучшает читаемость кода.

Инкапсулируем try/catch в класс

Для улучшения кода сделаем небольшой рефакторинг и создадим новый класс. IntelliJ IDEA даже предлагает использовать record:

var forNamer = new ForNamer(); // 1 Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(forNamer::apply) // 2 .forEach(System.out::println); record ForNamer() implements Function> < @Override public Classapply(String string) < try < return Class.forName(string); >catch (ClassNotFoundException e) < return null; >> >

Попробуем использовать Lombok

Project Lombok — это процессор аннотаций, который генерирует дополнительный байт-код во время компиляции. Мы можем получить нужный результат, используя всего лишь одну аннотацию, без написания лишнего кода.

Project Lombok — библиотека Java, которая интегрируется с вашим IDE и инструментами сборки, улучшая вашу Java. Больше нет необходимости писать очередной геттер или метод equals. Используя одну аннотацию, вы можете получить полнофункциональный builder, автоматизировать логирование и сделать многое другое.

Например Lombok-аннотация @SneakyThrow позволяет использовать проверяемые исключения без объявления их в сигнатуре метода. Но в настоящее время она не работает для Stream API. На GitHub есть соответствующий issue.

Commons Lang спешит на помощь

Apache Commons Lang — проект с давней историей. В свое время он был весьма популярен, предлагая утилиты, которые могли бы быть частью Java API, но не были. Гораздо удобнее использовать готовое решение, чем заново изобретать DateUtils и StringUtils в каждом проекте. При написании этого поста я обнаружил, что проект по прежнему жив, и там есть на что посмотреть. Например, Failable API.

Этот API состоит из двух частей:

Код, наконец, становится таким, каким мы ожидали увидеть его с самого начала:

Stream stream = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList"); Failable.stream(stream) .map(Class::forName) // 1 .forEach(System.out::println);

Исправление ошибок компиляции недостаточно

Предыдущий код бросает в рантайме ClassNotFoundException , обернутый в UndeclaredThrowableException . Все компилируется без ошибок, но мы не можем определить нужное поведение:

  • Выбросить первое исключение.
  • Проигнорировать исключения.
  • Собрать классы и исключения, чтобы обработать их на последнем этапе конвейера.
  • Сделать что-то другое.

Для этого мы можем использовать силу Vavr. Vavr — это библиотека, которая привносит мощь функционального программирования в язык Java:

Vavr — это функциональная библиотека для Java. Она помогает уменьшить объем кода и повысить ошибкоустойчивость. Первый шаг к функциональному программированию — начать думать иммутабельными значениями. Vavr предоставляет иммутабельные коллекции и соответствующие функции с управляющими структурами для работы с ними.

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

Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(CheckedFunction1.liftTry(Class::forName)) // 1 .map(Try::toEither) // 2 .forEach(e -> < if (e.isLeft()) < // 3 System.out.println("not found:" + e.getLeft().getMessage()); >else < System.out.println("class:" + e.get().getName()); >>);
  1. Оборачиваем вызов в Vavr Try .
  2. Преобразуем Try в Either, чтобы сохранить исключение. Если не нужно, то можно использовать Optional.
  3. Действуем в зависимости от того, содержит ли Either исключение (left) или ожидаемый результат (right).

Мы все еще остаемся в мире Java Stream. Все работает, как и ожидается, но forEach выглядит не очень красиво.

Vavr предоставляет собственный класс Stream , который имитирует Stream из Java Stream API, но с дополнительной функциональностью. Давайте воспользуемся им, чтобы переписать пайплайн:

var result = Stream.of("java.lang.String", "ch.frankel.blog.Dummy", "java.util.ArrayList") .map(CheckedFunction1.liftTry(Class::forName)) .map(Try::toEither) .partition(Either::isLeft) // 1 .map1(left -> left.map(Either::getLeft)) // 2 .map2(right -> right.map(Either::get)); // 3 result._1().forEach(it -> System.out.println("not found: " + it.getMessage())); // 4 result._2().forEach(it -> System.out.println("class: " + it.getName())); // 4
  1. Разделяем Stream из Either на кортеж из двух Stream .
  2. Преобразуем левый Stream из Either в Stream из Throwable .
  3. Преобразуем правый Stream из Either в Stream из Class .
  4. Далее делаем все, что нам нужно.

Выводы

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

Java Stream API плохо работает с проверяемыми исключениями. Код, использующий проверяемые исключения совместно со Stream API выглядит не очень хорошо. Для улучшения читаемости кода, ради чего мы и используем Stream API, можно использовать Apache Commons Lang.

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

Исходный код примеров можно найти на GitHub.

Скоро состоится открытый урок «Архитектурные концепции построения систем обмена сообщений», на котором рассмотрим стили интеграции (File Transfer, RPI, Shared Database, Messaging), а также основные концепции обмена сообщениями. Регистрируйтесь по ссылке.

Источник

Оцените статью