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(...) или основной объявляет любое проверенное исключение, но вывод:

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)

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


Я знаю, что это как-то связано с стиранием типов, но я не уверен, что понимаю каждую часть взлома.


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

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

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

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...

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

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

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


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

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

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

Thread.currentThread().stop(throwable);

Unsafe.getUnsafe().throwException(throwable);

Разница лишь в том, что оба используют нативный код.

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

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

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

Предполагается, что T sneakyThrow является 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 сам не возвращается.)

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

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
        }
    }


}

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

Другие вопросы по тегам