Вирішення винятків для перевірених 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);
                }
            }
    );

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

Отже, я хотів би мати вашу думку щодо мого суперечливого вирішення роздратування перевірених винятків. З цією метою я створив допоміжний інтерфейс ConsumerCheckException<T>та функцію утиліти rethrow( оновлену відповідно до опису коментаря Doval ) наступним чином:

  @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 у forEachможе бути виконано паралельно при використанні паралельних Streams. Тоді перекидається, що піднімається від відмовлення від споживача, потім поширюється на викликову нитку, яка 1) не зупинить інших одночасно працюючих споживачів, що може бути, а може і не підходить, і 2) якщо більше одного з споживачів щось кине, тільки один із кидаючих предметів буде видно за викликом.
Joonas Pulakaka


2
Лямбди такі некрасиві.
Tulains Córdova

Відповіді:


16

Переваги, недоліки та обмеження вашої техніки:

  • Якщо call-код повинен обробляти перевірений виняток, ОБОВ'ЯЗКОВО додати його до пункту кидків методу, що містить потік. Компілятор більше не змусить вас додавати його, тому простіше забути його. Наприклад:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
    
  • Якщо call-код вже обробляє перевірений виняток, компілятор нагадає вам додати пункт кидок до декларації методу, що містить потік (якщо ви цього не зробите, то виняток ніколи не викидається в тіло відповідного оператора спробу) .

  • У будь-якому випадку, ви не зможете оточити сам потік, щоб зловити перевірений виняток INSIDE методом, який містить потік (якщо ви спробуєте, компілятор скаже: Виняток ніколи не викидається в тіло відповідного оператора спробу).

  • Якщо ви викликаєте метод, який буквально ніколи не може викинути виняток, який він декларує, тоді ви не повинні включати пункт про кидки. Наприклад: нова String (byteArr, "UTF-8") кидає UnsupportedEncodingException, але UTF-8 гарантує, що специфікація Java завжди буде присутня. Тут декларація кидків викликає неприємність, і будь-яке рішення замовкнути її мінімальними котлами вітається.

  • Якщо ви ненавидите перевірені винятки і вважаєте, що їх ніколи не слід додавати до мови Java (все більше людей думають саме так, а я НЕ з них), тоді просто не додайте перевірені винятки до кидків пункт методу, який містить потік. Тоді перевірений виняток буде поводитись як виняток, що перевіряється ООН.

  • Якщо ви реалізуєте суворий інтерфейс, коли у вас немає можливості додавати декларацію про кидки, і все ж викид винятку цілком доречний, тоді перенесення винятку просто для отримання привілею для його кидання призводить до стек-траксів із помилковими винятками, які не вносити жодної інформації про те, що насправді пішло не так. Хороший приклад - Runnable.run (), який не кидає перевірених винятків. У цьому випадку ви можете вирішити не додавати перевірений виняток до пункту кидків методу, який містить потік.

  • У будь-якому випадку, якщо ви вирішили НЕ додати (або забути додати) перевірений виняток до пункту кидків методу, що містить потік, пам’ятайте про ці 2 наслідки відкидання перевірених винятків:

    1. Код виклику не зможе зловити його по імені (якщо ви спробуєте, компілятор скаже: Виняток ніколи не викидається в тіло відповідного оператора спробу). Він буде бульбашком і, ймовірно, буде схоплений у головному циклі програми деяким "catch Exception" або "catch Throwable", який може бути тим, що ви хочете в будь-якому випадку.

    2. Це порушує принцип найменшого сюрпризу: його більше не буде достатньо, щоб зловити RuntimeException, щоб мати можливість гарантувати вилов усіх можливих винятків. З цієї причини, я вважаю, це слід робити не в рамковому коді, а лише в бізнес-коді, який ви повністю контролюєте.

Список літератури:

ПРИМІТКА. Якщо ви вирішили використовувати цю техніку, ви можете скопіювати LambdaExceptionUtilклас помічника з StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-cont-exceptions-from-inside-java-8 -потоки . Це дає вам повну реалізацію (функція, споживач, постачальник ...), із прикладами.


6

У цьому прикладі, чи може він колись справді провалитися? Не думайте так, але, можливо, ваш випадок особливий. Якщо це дійсно "не може вийти з ладу", і це просто дратує компілятор, я люблю обгортати виняток і кидати Errorкоментар "не може статися". Робить речі дуже зрозумілими для обслуговування. Інакше вони будуть задаватися питанням "як це може статися" та "хто чорт поводиться з цим?"

Це в суперечливій практиці так YMMV. Я, мабуть, отримаю деякі відгуки.


3
+1, оскільки ви видасте помилку. Скільки разів я опинився після годин налагодження в блоці лову, що містить лише один коментар у рядку "не може статися" ...
Axel

3
-1 тому, що ви кинете помилку. Помилки вказують на те, що в JVM щось не так, і верхні рівні можуть відповідати їм. Якщо ви виберете такий шлях, вам слід кинути RuntimeException. Іншим можливим способом вирішення є твердження (потрібний прапор включено) або ведення журналу.
duros

3
@duros: Залежно від того, чому виняток "не може статися", факт, що його кидають, може означати, що щось сильно не так. Припустимо, наприклад, якщо хтось викликає cloneгерметичний тип, Fooякий, як відомо, підтримує його, і він кидає CloneNotSupportedException. Як це могло статися, якщо код не пов'язаний з яким-небудь іншим несподіваним видом Foo? І якщо це станеться, чи можна довіряти чомусь?
supercat

1
@supercat відмінний приклад. Інші викидають винятки з відсутніх String Encodings або 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тут - це найбільш підходящий варіант.
biziclop

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

підлийThrow класно! Я цілком відчуваю твій біль.

Якщо вам доведеться залишитися на Яві ...

Paguro має функціональні інтерфейси, які містять перевірені винятки, тому вам більше не доведеться думати про це. Він включає в себе незмінні колекції та функціональні перетворення по лінії перетворювачів Clojure (або Java 8 Streams), які приймають функціональні інтерфейси і містять перевірені винятки.

Є також колекції VAVr та The Eclipse, які можна спробувати.

В іншому випадку використовуйте Kotlin

Kotlin є двостороннім сумісним з Java, не має перевірених винятків, ніяких примітивів (ну, не програміст повинен думати), кращої системи типу, передбачає незмінність і т.д. м перетворюю весь код Java, який я можу, на Котлін. Все, що я раніше робив на Яві, я зараз вважаю за краще робити в Котліні.


Хтось проголосував це, що добре, але я нічого не дізнаюся, якщо ви не скажете мені, чому ви це зробили.
GlenPeterson

1
+1 для вашої бібліотеки в github. Ви також можете перевірити eclipse.org/collections або проект vavr, які надають функціональні та незмінні колекції. Які ваші думки з цього приводу?
firephil

-1

Перевірені винятки дуже корисні, і поводження з ними залежить від виду продукту, до якого ви входите:

  • бібліотека
  • настільний додаток
  • сервер, який працює в якомусь полі
  • академічна вправа

Зазвичай бібліотека не повинна обробляти перевірені винятки, а оголошувати їх як передані загальнодоступним API. Рідкісний випадок, коли вони могли перетворитись на звичайний RuntimeExcepton, наприклад, створити всередині бібліотечного коду якийсь приватний XML-документ і проаналізувати його, створити якийсь тимчасовий файл, а потім прочитати його.

Для настільних додатків потрібно розпочати розробку продукту, створивши власну структуру обробки винятків. Використовуючи власну структуру, ви зазвичай загортаєте вивірений виняток у деякі з двох-чотирьох обгортків (ваші спеціальні підкласи RuntimeExcepion) та обробляєте їх одним обробником винятків.

Для серверів ваш код зазвичай працює в якійсь рамці. Якщо ви не використовуєте жодного (простого сервера), створіть власну структуру, подібну (на основі Runtimes), описану вище.

Для академічних вправ ви завжди можете зафіксувати їх безпосередньо в 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 * розширюють свої аналоги, що не кидаються, ви можете використовувати їх безпосередньо в потоці:

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)));

І інші речі.


Ще кращеfields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
користувач2418306

Чи поясніть, будь ласка, пояснення?
fge
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.