Обходной путь для проверенных исключений Java

49

Я высоко ценю новые функции Java 8, касающиеся лямбд и интерфейсов методов по умолчанию. Тем не менее, я все еще скучаю с проверенными исключениями. Например, если я просто хочу перечислить все видимые поля объекта, я хотел бы просто написать это:

    Arrays.asList(p.getClass().getFields()).forEach(
        f -> System.out.println(f.get(p))
    );

Тем не менее, поскольку getметод может выдать проверенное исключение, которое не согласуется с Consumerконтрактом интерфейса, я должен перехватить это исключение и написать следующий код:

    Arrays.asList(p.getClass().getFields()).forEach(
            f -> {
                try {
                    System.out.println(f.get(p));
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
    );

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

Таким образом, я хотел бы иметь свое мнение о моем спорноге для обхода проверяемых исключений раздражения. Для этого я создал вспомогательный интерфейс ConsumerCheckException<T>и служебную функцию rethrow( обновленную в соответствии с предложением комментария Довала ) следующим образом:

  @FunctionalInterface
  public interface ConsumerCheckException<T>{
      void accept(T elem) throws Exception;
  }

  public class Wrappers {
      public static <T> Consumer<T> rethrow(ConsumerCheckException<T> c) {
        return elem -> {
          try {
            c.accept(elem);
          } catch (Exception ex) {
            /**
             * within sneakyThrow() we cast to the parameterized type T. 
             * In this case that type is RuntimeException. 
             * At runtime, however, the generic types have been erased, so 
             * that there is no T type anymore to cast to, so the cast
             * disappears.
             */
            Wrappers.<RuntimeException>sneakyThrow(ex);
          }
        };
      }

      /**
       * Reinier Zwitserloot who, as far as I know, had the first mention of this
       * technique in 2009 on the java posse mailing list.
       * http://www.mail-archive.com/javaposse@googlegroups.com/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

И теперь я могу просто написать:

    Arrays.asList(p.getClass().getFields()).forEach(
            rethrow(f -> System.out.println(f.get(p)))
    );

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

Фернандо Мигель Карвалью
источник
2
В дополнение к ссылке Роберта, взгляните также на Sneakily Throwing Checked Exceptions . Если вы хотите, вы можете использовать sneakyThrowвнутри, rethrowчтобы бросить исходное, проверенное исключение вместо того, чтобы обернуть его в RuntimeException. В качестве альтернативы вы можете использовать @SneakyThrowsаннотацию от Project Lombok, которая делает то же самое.
Довал
1
Также обратите внимание, что Consumers in forEachможет выполняться параллельно при использовании параллельных Streams. Затем отбрасываемое значение, вызванное отказом от потребителя, будет распространяться на вызывающий поток, который 1) не остановит других одновременно работающих потребителей, что может или не может быть уместно, и 2) если более одного из потребителей что-то выбрасывают, только одна из меток будет видна вызывающему потоку.
Joonas Pulakka
2
Лямбды такие уродливые.
Тулаинс Кордова

Ответы:

16

Преимущества, недостатки и ограничения вашей техники:

  • Если вызывающий код должен обрабатывать проверенное исключение, вы ДОЛЖНЫ добавить его в предложение throws метода, который содержит поток. Компилятор больше не будет заставлять вас добавлять его, поэтому его легче забыть. Например:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
    
  • Если вызывающий код уже обрабатывает проверенное исключение, компилятор напомнит вам добавить предложение throws к объявлению метода, содержащему поток (если вы этого не сделаете, он скажет: исключение никогда не выдается в теле соответствующего оператора try) ,

  • В любом случае, вы не сможете окружить сам поток, чтобы поймать проверенное исключение ВНУТРИ метода, который содержит поток (если вы попытаетесь, компилятор скажет: исключение никогда не выдается в теле соответствующего оператора try).

  • Если вы вызываете метод, который буквально никогда не может выбросить исключение, которое он объявляет, вам не следует включать предложение throws. Например: new String (byteArr, "UTF-8") выбрасывает UnsupportedEncodingException, но UTF-8 гарантируется, что спецификация Java всегда присутствует. Здесь, декларация бросков - неприятность, и любое решение заставить ее замолчать с минимальным количеством шаблонов приветствуется.

  • Если вы ненавидите проверенные исключения и чувствуете, что они никогда не должны добавляться в язык Java с самого начала (все больше людей думают так, а я НЕ один из них), тогда просто не добавляйте проверенное исключение к броскам предложение метода, содержащего поток. Тогда проверенное исключение будет вести себя так же, как исключение UNchecked.

  • Если вы реализуете строгий интерфейс, в котором у вас нет возможности добавить объявление throws, и, тем не менее, выбрасывать исключение вполне уместно, тогда оборачивание исключения только для того, чтобы получить привилегию для его выброса, приводит к трассировке стека с ложными исключениями, которые не сообщать информацию о том, что на самом деле пошло не так. Хорошим примером является Runnable.run (), который не генерирует никаких проверенных исключений. В этом случае вы можете решить не добавлять проверенное исключение в предложение throws метода, содержащего поток.

  • В любом случае, если вы решите НЕ добавлять (или забыть добавить) проверенное исключение к предложению throws метода, содержащего поток, учтите следующие 2 последствия выброса исключений CHECKED:

    1. Вызывающий код не сможет поймать его по имени (если вы попробуете, компилятор скажет: исключение никогда не выдается в теле соответствующего оператора try). Он будет пузыриться и, вероятно, будет перехвачен в цикле основной программы каким-нибудь «catch Exception» или «catch Throwable», что может быть тем, что вы хотите в любом случае.

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

Рекомендации:

ПРИМЕЧАНИЕ. Если вы решите использовать этот метод, вы можете скопировать LambdaExceptionUtilвспомогательный класс из StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8 -потоки . Это дает вам полную реализацию (Function, Consumer, Supplier ...), с примерами.

MarcG
источник
6

В этом примере, может ли он когда-нибудь действительно потерпеть неудачу? Не думаю, но, возможно, ваш случай особенный. Если он действительно «не может потерпеть неудачу», и это просто раздражает компилятор, я хотел бы обернуть исключение и Errorдобавить комментарий «не может произойти». Делает вещи очень понятными для обслуживания. Иначе они будут удивляться, «как это может случиться» и «кто, черт возьми, справляется с этим?»

Это в спорной практике так YMMV. Я, вероятно, получу некоторые отрицательные отзывы.

user949300
источник
3
+1, потому что вы выбросите ошибку. Сколько раз я заканчивал после нескольких часов отладки в блоке catch, содержащем только однострочный комментарий, «не может случиться» ...
Аксель
3
-1 потому что вы выбросите ошибку. Ошибки указывают на то, что в JVM что-то не так, и верхние уровни могут обрабатывать их соответствующим образом. Если вы выберете этот путь, вы должны выбросить RuntimeException. Другими возможными обходными путями являются утверждения (флаг -ea включен) или ведение журнала.
duros
3
@duros: В зависимости от того, почему исключение «не может произойти», тот факт , что выкинут может означать , что что - то серьезно не так. Предположим, например, если кто-то вызывает cloneзапечатанный тип, Fooкоторый, как известно, поддерживает его, и он выбрасывает CloneNotSupportedException. Как это могло произойти, если код не был связан с каким-то другим неожиданным видом Foo? И если это произойдет, можно ли доверять чему-либо?
суперкат
1
@supercat отличный пример. Другие выдают исключения из отсутствующих кодировок строк или MessageDigests. Если отсутствуют UTF-8 или SHA, ваша среда выполнения, вероятно, повреждена.
user949300
1
@duros Это полное заблуждение. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(цитата из Javadoc of Error). Это как раз такая ситуация, поэтому она далеко не является неадекватной, поэтому использование Errorздесь является наиболее подходящим вариантом.
Бизиклоп
1

другая версия этого кода, где проверенное исключение просто задерживается:

public class Cocoon {
         static <T extends Throwable> T forgetThrowsClause(Throwable t) throws T{
            throw (T) t;
        }

        public static <X, T extends Throwable> Consumer<X> consumer(PeskyConsumer<X,T> touchyConsumer) throws T {
            return new Consumer<X>() {
                @Override
                public void accept(X t) {
                    try {
                        touchyConsumer.accept(t);
                    } catch (Throwable exc) {
                        Cocoon.<RuntimeException>forgetThrowsClause(exc) ;
                    }

                }
            } ;
        }
// and so on for Function, and other codes from java.util.function
}

магия в том, что если вы позвоните:

 myArrayList.forEach(Cocoon.consumer(MyClass::methodThatThrowsException)) ;

тогда ваш код будет обязан перехватить исключение

ява медведь
источник
Что произойдет, если это выполняется в параллельном потоке, где элементы используются в разных потоках?
Прогон
0

подлый это круто! Я полностью чувствую твою боль.

Если вам нужно остаться на Яве ...

Paguro имеет функциональные интерфейсы, которые обертывают проверенные исключения, поэтому вам больше не нужно об этом думать. Он включает в себя неизменяемые коллекции и функциональные преобразования по типу преобразователей Clojure (или потоков Java 8), которые принимают функциональные интерфейсы и переносят проверенные исключения.

Также можно попробовать VAVr и The Eclipse Collections .

В противном случае используйте Kotlin

Kotlin является двухсторонней совместимостью с Java, не имеет проверенных исключений, никаких примитивов (ну, не о чем должен думать программист), лучшей системы типов, предполагает неизменяемость и т. Д. Несмотря на то, что я курирую библиотеку Paguro выше, я ' Я преобразую весь код Java, который я могу, в Kotlin. Все, что я делал на Яве, теперь я предпочитаю делать на Котлине.

GlenPeterson
источник
Кто-то за это проголосовал, это нормально, но я ничего не узнаю, если вы не скажете мне, почему вы проголосовали за это.
ГленПетерсон
1
+1 для вашей библиотеки в github. Вы также можете проверить eclipse.org/collections или проект vavr, которые предоставляют функциональные и неизменные коллекции. Что вы думаете об этом?
Firephil
-1

Проверенные исключения очень полезны, и их обработка зависит от типа продукта, частью которого вы код:

  • библиотека
  • настольное приложение
  • сервер работает в какой-то коробке
  • академическое упражнение

Обычно библиотека не должна обрабатывать проверенные исключения, а объявляется как генерируемая публичным API. Редкий случай, когда они могут быть обернуты в обычный RuntimeExcepton, например, создает внутри кода библиотеки некоторый частный XML-документ и анализирует его, создает некоторый временный файл и затем читает его.

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

Для серверов, как правило, ваш код будет работать внутри какой-то платформы. Если вы не используете какой-либо (пустой сервер), создайте свой собственный фреймворк, аналогичный (на основе Runtime), описанный выше.

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

Размик
источник
-1

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

С его помощью вы можете сделать это вместо написания собственного кода:

// I don't know the type of f, so it's Foo...
final ThrowingConsumer<Foo> consumer = f -> System.out.println(f.get(p));

Поскольку все эти интерфейсы Throwing * расширяют свои аналоги, не относящиеся к Throwing, вы можете использовать их непосредственно в потоке:

Arrays.asList(p.getClass().getFields()).forEach(consumer);

Или вы можете просто:

import static com.github.fge.lambdas.consumers.Consumers.wrap;

Arrays.asList(p.getClass().getFields())
    .forEach(wrap(f -> System.out.println(f.get(p)));

И другие вещи.

FGE
источник
Еще лучшеfields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
user2418306
Не могли бы объяснить, что downvoters?
fge