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

  1. Непроверенные исключения
  2. Использование блока try / catch по сравнению с извлеченным методом
  3. Работа с проверенными исключениями
  4. Заметка
  5. Используя извлеченный метод подхода
  6. Обобщая извлеченный метод
  7. Осложнения и некоторая помощь сообщества Java
Поезд ночью (источник: biacamentil )

При создании языка Java было принято несколько решений, которые по-прежнему влияют на то, как мы пишем код сегодня. Одним из них было добавление проверенных исключений к языку, к которым компилятор требует, чтобы вы готовились с помощью блока try / catch или условия throws во время компиляции.

Если вы перешли на Java 8, вы знаете, что переход к концепциям функционального программирования, таких как лямбда-выражения , ссылки на методы , а также потоки Такое ощущение, что они полностью переделали язык. Вместо того, чтобы перебирать коллекцию значений, мы преобразуем потоки, используя конвейер. Вместо условий «если» мы используем предикаты в фильтре. Вместо того, чтобы подробно описывать, как выполнить задачу, мы используем более декларативный подход и указываем, что мы хотим сделать в коде, и используем значительно улучшенную стандартную библиотеку.

Разработчики, переходящие на Java 8, довольно легко принимают новый подход, и полученный код имеет тенденцию быть короче и легче следовать, если только вам не приходится иметь дело с исключениями. Здесь вы увидите три основных способа обработки исключений в потоковом конвейере, и каждый подход будет включать несколько примеров.

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

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

Пример 1. Лямбда-выражение, которое может генерировать непроверенное исключение

public List <Integer> scale (List <Integer> values, Integer factor) {return values.stream () .map (n -> n / factor) .collect (Collectors.toList ()); }

Метод масштабирования принимает список целых чисел и коэффициент для масштабирования. Метод stream () (новый метод по умолчанию, добавленный к Коллекция интерфейс) преобразует коллекцию в поток <Integer>. Метод map использует Function, один из новых функциональных интерфейсов в пакете java.util.function, который принимает каждое значение n и делит его на коэффициент. Затем метод сбора используется для преобразования результатов обратно в список.

Аргумент функции метода map столкнется с проблемой, если заданный коэффициент равен нулю. В этом случае целочисленное деление вызовет исключение ArithmeticException.

(Помимо этого: в качестве эксперимента попробуйте разделить число на двойное значение 0.0 вместо целочисленного значения 0. Вы можете быть удивлены или напуганы, обнаружив, что исключение не выдается. В этом случае возвращаемое значение равно Бесконечности (серьезно) и любые последующие вычисления будут продолжаться, повреждая значения по пути. Верьте или нет, это считается правильным поведением в соответствии с Спецификация IEEE 754 для обработки вычислений с плавающей запятой в двоичном компьютере.)

Поскольку ArithmeticException является непроверенным исключением, вам не нужно готовиться к нему, добавляя в код блок try / catch или предложение throws. Другими словами, этот код в порядке, по крайней мере, в том, что касается компилятора.

Что бы вы сделали, если бы хотели разобраться с брошенным исключением?

Использование блока try / catch по сравнению с извлеченным методом

Самый простой ответ - встроить блок try / catch внутри конвейера, как показано в примере 2.

Пример 2. Лямбда-выражение с помощью try / catch

public List <Integer> scale (List <Integer> values, Integer factor) {return values.stream () .map (n -> {try {return n / factor;} catch (ArithmeticException e) {e.printStackTrace (); }}) .collect (Collectors.toList ()); }

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

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

Пример 3. Извлечение лямбды в метод

частное целочисленное деление (целочисленное значение, целочисленный коэффициент) {try {возвращаемое значение / коэффициент; } catch (ArithmeticException e) {e.printStackTrace (); }} public List <Integer> scale (List <Integer> values, Integer factor) {return values.stream () .map (n -> div (n, factor)) .collect (Collectors.toList ()); }

Разработчики писали код наподобие метода разделения в течение многих лет. Это выглядит знакомо, легко понять, и (что еще лучше) вы можете написать контрольные примеры, чтобы убедиться, что все работает так, как вы ожидаете. Лучше всего то, что метод масштабирования теперь упрощен, так что промежуточная операция карты - только одна строка.

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

Работа с проверенными исключениями

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

  1. Добавьте блок try / catch в лямбда-выражение
  2. Создайте извлеченный метод , как в непроверенном примере
  3. Напишите метод- обертку, который перехватывает отмеченные исключения и сбрасывает их как непроверенные.
Заметка

Полный пример, включая тесты для всех трех подходов кодирования и демонстрацию нескольких геокодированных адресов, можно найти в репозитории GitHub по адресу https://github.com/kousen/java-geocoder ,

Чтобы привести конкретный пример, рассмотрите возможность определения широты и долготы адреса с помощью общедоступного геокодера, такого как один предоставлен Google как RESTful веб-сервис. Геокодер берет информацию об адресе, такую ​​как улица, город и штат, и преобразует ее в географические координаты, такие как широта и долгота. Чтобы использовать геокодер Google, вам необходимо кодировать компоненты по указанному адресу, передавать его в Google с использованием правильного URL-адреса и извлекать результаты из ответа JSON или XML.

Java уже включает в себя класс java.net.URLEncoder. Этот класс включает метод статического кодирования, который преобразует строки в их закодированные в форме URL эквиваленты, в которых пробелы становятся плюсами, апострофы становятся% 27s и т. Д. (Подробности см. В Javadocs для URLEncoder). Проблема заключается в том, что метод encode объявляет, что он генерирует исключение UnsupportedEncodingException, которое является проверенным исключением.

Учитывая строки, представляющие адрес, метод encodeAddress в Примере 4 показывает, как вы можете кодировать каждый из них.

Пример 4. URL, кодирующий коллекцию строк (ПРИМЕЧАНИЕ: НЕ СОБИРАЕТСЯ)

public String encodeAddress (String ... values) {return Arrays.stream (values) .map (s -> URLEncoder.encode (s, "UTF-8"))) .collect (Collectors.joining (",")) ; }

Аргумент String varargs рассматривается как массив внутри метода, поэтому метод Arrays.stream () создает поток <String> из этих значений. Затем, как и прежде, операция map с функцией кодирует каждое значение, а результаты собираются обратно в строку. К сожалению, код в Примере 4 не компилируется, потому что отмеченный UnsupportedEncodingException не обрабатывается во время компиляции.

Возможно, вы захотите просто объявить, что метод encodeAddress выдает правильное исключение, как в примере 5.

Пример 5. Объявление исключения (ТАКЖЕ НЕ СООТВЕТСТВУЕТ)

public String encodeAddress (String ... values) throws UnsupportedEncodingException {return Arrays.stream (values) .map (s -> URLEncoder.encode (s, "UTF-8"))) .collect (Collectors.joining (",") )); }

Это тоже не компилируется, потому что метод encodeAddress не тот, который нуждается в предложении throws. Чтобы убедиться в этом, рассмотрим реализацию аргумента функции для отображения как анонимного внутреннего класса, а не как лямбда-выражения, как показано в примере 6.

Пример 6. Кодирование URL с использованием try / catch - анонимная версия внутреннего класса

public String encodeAddressAnonInnerClass (String ... values) {return Arrays.stream (values) .map (new Function <String, String> () {@Override public String apply (String s) {try {return URLEncoder.encode (s, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace (); return "";}}}) .collect (Collectors.joining (",")); }

Как показывает этот пример, место для размещения предложения throws будет в методе apply, а в лямбда-версии вы даже не увидите сигнатуру этого метода. Более того, интерфейс Function (который объявляет метод apply) взят из библиотеки, и вы не можете просто изменить методы, объявленные в интерфейсах библиотеки. Итак, вы вернулись к внедрению блока try / catch внутри метода apply, как в примере 7.

Пример 7. Кодирование URL с использованием try / catch - лямбда-версия

public String encodedAddressUsingTryCatch (String ... address) {return Arrays.stream (address) .map (s -> {try {return URLEncoder.encode (s, "UTF-8");} catch (UnsupportedEncodingException e) {выбросить новый RuntimeException (e);}}) .collect (Collectors.joining (",")); }

Используя извлеченный метод подхода

Вместо того, чтобы встраивать try / catch в лямбду, используйте вместо этого извлеченный метод, описанный с примером масштаба, приведенным выше, как показано в примере 8.

Пример 8. Делегирование кодирования URL извлеченному методу

private String encodeString (String s) {try {return URLEncoder.encode (s, "UTF-8"); } catch (UnsupportedEncodingException e) {e.printStackTrace (); } return s; } public String encodedAddressUsingExtractedMethod (String ... address) {return Arrays.stream (address) .map (this :: encodeString) .collect (Collectors.joining (",")); }

Здесь извлеченный метод называется encodeString, и он содержит необходимый блок try / catch. Это работает, и код конвейера еще проще, потому что он использует ссылку на метод для указания на метод encodeString. В этом случае извлеченный метод перехватывает проверенное исключение и сбрасывает его как непроверенное.

Этот подход неплох, и на самом деле довольно распространен, но он требует, чтобы вы каждый раз писали извлеченный метод. Разве нет способа обобщить это?

Обобщая извлеченный метод

Обобщенный способ обработки всех проверенных исключений не идеален, но методика стоит знать. Проблема в том, что метод map принимает функцию, а метод apply в функции не объявляет никаких исключений. Что если вы создали другой функциональный интерфейс, похожий на Function, чей метод apply объявил об исключении? См. Пример 9 для такого интерфейса.

Пример 9. Функциональный интерфейс, основанный на функции, которая выдает исключение

@FunctionalInterface открытый интерфейс FunctionWithException <T, R, E расширяет исключение> {R apply (T t) выбрасывает E; }

Общие параметры T и R в FunctionWithException аналогичны их аналогам в Function, но добавленный параметр E расширяет Exception. Метод apply теперь принимает T, возвращает R и объявляет, что может бросить E.

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

Пример 10. Метод-обертка для обработки исключений

закрытый <T, R, E расширяет Exception> Function <T, R> обертка (FunctionWithException <T, R, E> fe) {return arg -> {try {return fe.apply (arg); } catch (Exception e) {throw new RuntimeException (e); }}; }

Аргументом метода-обертки является любое FunctionWithException. Реализация встраивает блок try / catch, который перехватывает любое исключение и перебрасывает его как исключение без проверки. Тип возвращаемого значения - функция java.util.function. Функция, которая является обязательным аргументом для метода карты. Пример 11 демонстрирует, как использовать метод оболочки.

Пример 11. Использование универсального метода-обертки

public String encodedAddressUsingWrapper (String ... address) {return Arrays.stream (address) .map (wrapper (s -> URLEncoder.encode (s, "UTF-8"))) .collect (Collectors.joining ("," )); }

В этом случае лямбда-выражение вызывает метод URLEncode.encode со строковым аргументом и возвращает закодированную строку. Конечно, это может вызвать исключение UnsupportedEncodingException. Метод-обертка перехватывает это (или любое другое) исключение и перебрасывает его как исключение без проверки, в конечном итоге возвращая экземпляр функции, который требуется методу карты.

Осложнения и некоторая помощь сообщества Java

Недостатком этого подхода является то, что в дополнение к FunctionWithException вам, вероятно, также понадобятся функциональные интерфейсы для потребителей с исключениями, поставщиков с исключениями и предикатов с исключениями, не говоря уже о примитивном типе и двоичных вариациях. Это звучит как большой объем работы, поэтому, вероятно, лучше оставить ее разработчикам библиотеки. Конечно, именно это сейчас и происходит в сообществе. Библиотека VAVR Например, имеет методы, аналогичные только что описанным.

Именно такие сложности объясняют, почему большинство популярных фреймворков Java (например, весна а также зимовать , чтобы перечислить только пару примеров) поймать все отмеченные исключения и сбросить их как непроверенные.

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

Что бы вы сделали, если бы хотели разобраться с брошенным исключением?
Разве нет способа обобщить это?
Что если вы создали другой функциональный интерфейс, похожий на Function, чей метод apply объявил об исключении?