Java SneakyThrow исключений, тип стирания

Может кто-нибудь объяснить этот код?

public class SneakyThrow {


  public static void sneakyThrow(Throwable ex) {
    SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
  }

  private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
    throw (T) ex;
  }



  public static void main(String[] args) {
    SneakyThrow.sneakyThrow(new Exception());
  }


}

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

Обратите внимание, что ни sneakyThrow(...), ни main не объявляют никаких проверенных исключений, но вывод:

Exception in thread "main" java.lang.Exception
    at com.xxx.SneakyThrow.main(SneakyThrow.java:20)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Этот хак используется на Ломбоке с аннотацией @SneakyThrow, которая позволяет генерировать проверенные исключения без их объявления.


Я знаю, что это имеет какое-то отношение к стиранию типа, но я не уверен, что понимаю все части хака.


Редактировать: я знаю, что мы можем вставить Integer в List<String> и что различие проверенных и непроверенных исключений является функцией времени компиляции.

При приведении неуниверсального типа, например List, к универсальному типу, например List<XXX>, компилятор выдает предупреждение. Но менее распространено прямое приведение к общему типу, например (T) ex в приведенном выше коде.

Если хотите, часть, которая кажется мне странной, заключается в том, что я понимаю, что внутри JVM List<Dog> и List<Cat> выглядят одинаково, но приведенный выше код, похоже, означает, что, наконец, мы также можем присвоить значение типа Cat переменной типа Собака или что-то в этом роде.


person Sebastien Lorber    schedule 26.12.2012    source источник
comment
См. также stackoverflow.com/questions /18198176/   -  person Vadzim    schedule 12.10.2015
comment
... и stackoverflow.com/questions/31270759/   -  person Vadzim    schedule 16.10.2015


Ответы (4)


Если вы скомпилируете его с -Xlint, вы получите предупреждение:

c:\Users\Jon\Test>javac -Xlint SneakyThrow.java
SneakyThrow.java:9: warning: [unchecked] unchecked cast
    throw (T) ex;
              ^
  required: T
  found:    Throwable
  where T is a type-variable:
    T extends Throwable declared in method <T>sneakyThrowInner(Throwable)
1 warning

Это в основном говорит: «Это приведение не на самом деле проверяется во время выполнения» (из-за стирания типа) — поэтому компилятор неохотно предполагает, что вы делаете правильные вещи, зная, что на самом деле это не будет проверено.

Теперь о проверенных и непроверенных исключениях заботится только компилятор — он вообще не является частью JVM. Итак, как только вы прошли компилятор, вы свободны дома.

Я настоятельно рекомендую вам избегать этого.

Во многих случаях существует "настоящая" проверка, когда вы используете дженерики, потому что что-то использует нужный тип, но всегда так бывает. Например:

List<String> strings = new ArrayList<String>();
List raw = strings;
raw.add(new Object()); // Haha! I've put a non-String in a List<String>!
Object x = strings.get(0); // This doesn't need a cast, so no exception...
person Jon Skeet    schedule 26.12.2012
comment
Смотрите мою правку. Кстати, если я помню, что вы пришли из слова C #, так почему вы советуете не использовать такие вещи, как @SneakyThrow Ломбока, который помогает не иметь дело с проверенными исключениями и делать то, что в C # - person Sebastien Lorber; 26.12.2012
comment
@SebastienLorber: Потому что, когда я пишу Java, я пытаюсь написать идиоматическую Java. Между прочим, я не понимаю, насколько важна сертификация SCJP. Ваше редактирование говорит о назначении значений — где значение когда-либо присваивалось в вашем коде? - person Jon Skeet; 26.12.2012
comment
Извините, если это покажется высокомерным, просто хотел сказать, что эти вещи подлежат сертификации. Я постараюсь найти соответствующий код с назначением, чтобы показать вам, что я имею в виду - person Sebastien Lorber; 26.12.2012
comment
@SebastienLorber: По моему опыту, сертификация не означает, что кто-то действительно понимает материал на глубоком уровне. В ходе опроса множества Java-инженеров я не могу сказать, что видел какую-либо связь между сертификацией и пониманием. Отсюда мое мнение, что это не очень актуально в вашем вопросе. - person Jon Skeet; 26.12.2012
comment
@JonSkeet Можете ли вы объяснить логику компилятора? Почему он думает, что это RuntimeException? - person gstackoverflow; 05.09.2017
comment
@gstackoverflow: Что вы имеете в виду под «Почему он думает, что это RuntimeException?» Почему компилятор думает, что что является RuntimeException? Пожалуйста, уточните, какую часть вам трудно понять. - person Jon Skeet; 05.09.2017
comment
SneakyThrowInner может генерировать любое исключение (давайте подумаем, что ‹RuntimeException›) нет. Почему компилятор думает, что это RuntimeException? - person gstackoverflow; 05.09.2017
comment
@gstackoverflow: он объявляет, что может генерировать выбрасываемый тип, который вы указываете в качестве аргумента типа. Теперь sneakyThrow вызывает его, используя RuntimeException в качестве аргумента типа, поэтому компилятор действует так, как если бы он был объявлен для генерирования RuntimeException. - person Jon Skeet; 05.09.2017
comment
stackoverflow.com/questions/31316581/ - person gstackoverflow; 05.09.2017
comment
@gstackoverflow: Значит, вы спрашиваете о коде в совершенно другом вопросе? Это не совсем полезно... - person Jon Skeet; 05.09.2017
comment
Это связанный вопрос. Я могу спросить вас о коде из ответа на ответ stackoverflow.com/a/36584422/2674303 - person gstackoverflow; 05.09.2017
comment
@gstackoverflow: Да, это связанный вопрос, но это означает, что вы должны были добавить комментарий к этому вопросу. Зачем добавлять его сюда, если здесь нет вывода типов? (И зачем упоминать другой вопрос только через полчаса после первого вопроса?) Я до сих пор не знаю, смущает ли вас код в этом вопросе или в другом. - person Jon Skeet; 05.09.2017
comment
@JonSkeet, почему исключение classcast не выдается? В конце концов, именно здесь JVm не сможет разыграть (T) ex ? - person Number945; 22.07.2018
comment
@BreakingBenjamin: потому что дженерики не являются частью JVM, кроме как с точки зрения аннотаций. Во время выполнения этот код не знает, что такое T, поэтому он не может выполнить приведение типов. - person Jon Skeet; 22.07.2018

приведенный выше код, по-видимому, означает, что, наконец, мы также можем присвоить значение типа Cat переменной типа Dog или что-то в этом роде.

Вы должны думать о том, как структурированы классы. T extends Throwable и вы передаете ему Exception. Это все равно, что присвоить Dog Animal, а не Dog Cat.

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


Проверенные исключения — это функция времени компиляции (например, дженерики).

Кстати, Throwable также является отмеченным исключением. Если вы подклассируете его, он будет проверен, если только он не является подклассом Error или RuntimeException.

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

Thread.currentThread().stop(throwable);

Unsafe.getUnsafe().throwException(throwable);

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

person Peter Lawrey    schedule 26.12.2012

Начиная с Java 8 вспомогательный метод sneakyThrowInner больше не требуется. sneakyThrow можно записать как:

@SuppressWarnings("unchecked")
static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
    throw (T)t;
}

См. статью Особая особенность вывода типа исключения в Java 8.

Подразумевается, что значение T для snickyThrow является RuntimeException. Об этом можно узнать из спецификации языка по выводу типов (http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html)

Примечание. Функция sneakyThrow объявлена ​​возвращающей RuntimeException, поэтому ее можно использовать следующим образом:

int example() { 
   if (a_problem_occurred) {
      throw sneakyThrow(new IOException("An I/O exception occurred"));
      // Without the throw here, we'd need a return statement!
   } else {
      return 42;
   }
}

Выбрасывая RuntimeException, возвращаемый sneakyThrow, компилятор Java знает, что этот путь выполнения завершается. (Конечно, сам sneakyThrow не возвращается.)

person AJNeufeld    schedule 12.04.2016
comment
это это ответ - person Eugene; 08.04.2019

Давайте посмотрим на код ниже:

public class SneakyThrow {


  public static void sneakyThrow(Throwable ex) {
    SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
  }

  private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
    throw (T) ex;
  }



  public static void main(String[] args) {
    SneakyThrow.sneakyThrow(new Exception());
  }


}

Давайте посмотрим, почему он так себя ведет.

Во-первых, любое приведение к параметру типа игнорируется компилятором. JVM будет напрямую использовать его. Таким образом, throw (T) ex; не будет проверяться компилятором на безопасность типов. Но к тому времени, когда код достигает JVM, происходит стирание типа. Следовательно, код будет выглядеть так: [Примечание: фактический код будет байтовым кодом. Ниже просто объясняется, что делает стирание типа.]

public class SneakyThrow {


    public static void sneakyThrow(Throwable ex) {
        SneakyThrow.sneakyThrowInner(ex); // Note : Throwable is checked exception but we don't mention anything here for it
    }

    private static  Throwable sneakyThrowInner(Throwable ex) throws Throwable {
        throw (Throwable) ex;
    }


    public static void main(String[] args) {
        SneakyThrow.sneakyThrow(new Exception());

    }


}

Первое, что нужно отметить, это то, что все будет работать гладко, и ClassCastException не будет выброшено, поскольку JVM легко может привести тип ex к Throwable.

Второе, на что следует обратить внимание, это то, что компилятор всегда заставлял нас перехватывать или передавать полученное проверенное исключение. JVM не делает таких проверок. Здесь, после приведения типа, в идеале SneakyThrow.sneakyThrowInner(ex); должен быть вынужден обрабатывать исключение Throwable, но помните, что байт-код этой версии доходит до JVM. Значит, мы как-то обманули компилятор.

Делай так:

public class SneakyThrow {


    public static void sneakyThrow(Throwable ex) {
        SneakyThrow.<RuntimeException>sneakyThrowInner(ex);
    }

    private static <T extends Throwable> T sneakyThrowInner(Throwable ex) throws T {
        throw (T) ex;
    }


    public static void main(String[] args) {
        try {
            SneakyThrow.sneakyThrow(new Exception());
        }catch (Throwable ex){
            System.out.println("Done Succesfully"); // Added to show everything runs fine
        }
    }


}

Вывод: выполнено успешно

person Number945    schedule 22.07.2018