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

85

При написании кода для другого ответа на этом сайте я обнаружил такую ​​особенность:

static void testSneaky() {
  final Exception e = new Exception();
  sneakyThrow(e);    //no problems here
  nonSneakyThrow(e); //ERRROR: Unhandled exception: java.lang.Exception
}

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

static <T extends Throwable> void nonSneakyThrow(T t) throws T {
  throw t;
}

Во-первых, я совершенно не понимаю, почему sneakyThrowкомпилятор работает с вызовом. Какой возможный тип он сделал, Tесли нигде не упоминается непроверенный тип исключения?

Во-вторых, если согласиться с тем, что это работает, почему тогда компилятор жалуется на nonSneakyThrowвызов? Они кажутся очень похожими.

Марко Топольник
источник

Ответы:

66

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

Во-первых, в разделе 18.1.3 есть примечание:

Граница формы throws αносит чисто информационный характер: она направляет разрешение для оптимизации создания экземпляра α, чтобы, по возможности, это не был проверенный тип исключения.

Это ни на что не влияет, но указывает на раздел «Решение» (18.4), в котором содержится дополнительная информация о предполагаемых типах исключений с особым случаем:

... В противном случае, если связанный набор throws αi, и соответствующие верхние границы & alpha ; i являются, в лучшем случае , Exception, Throwableи Object, то Ti = RuntimeException.

Этот случай применяется к sneakyThrow- единственная верхняя граница Throwable, поэтому Tпредполагается, что она RuntimeExceptionсоответствует спецификации, поэтому она компилируется. Тело метода несущественно - неконтролируемое приведение выполняется успешно во время выполнения, потому что на самом деле этого не происходит, оставляя метод, который может победить систему исключений, проверенную во время компиляции.

nonSneakyThrowне компилируется, поскольку этот метод Tимеет нижнюю границу Exception(т.е. Tдолжен быть супертипом Exceptionили Exceptionсамим собой), что является проверенным исключением из-за типа, с которым он вызывается, поэтому он Tвыводится как Exception.

чашка
источник
1
@Maksym Ты, должно быть, имел ввиду sneakyThrowзвонок. Особых правил вывода throws Tформ не существует в спецификации Java 7.
Марко Топольник
1
Небольшая придирка: In nonSneakyThrow, Tдолжно быть Exception, а не «супертипом» Exception, потому что это в точности тот тип аргумента, который был объявлен во время компиляции на сайте вызова.
llogiq 09
1
@llogiq Если я правильно прочитал спецификацию, у нее есть нижняя граница Exceptionи верхняя граница Throwable, поэтому наименьшая верхняя граница, которая является результирующим предполагаемым типом, равна Exception.
thecoop
1
@llogiq Обратите внимание, что тип аргумента устанавливает только нижнюю границу типа, потому что любой супертип аргумента также приемлем.
Марко Топольник
2
Exceptionфраза «или сам» может быть полезна для читателя, но в целом следует отметить, что в спецификации всегда используются термины «подтип» и «супертип» в смысле «включая себя»…
Хольгер,
17

Если вывод типа дает единственную верхнюю границу для переменной типа, обычно в качестве решения выбирается верхняя граница. Например, если T<<Number, решение есть T=Number. Хотя Integerи Floatт. Д. Также могут удовлетворять ограничению, нет веских причин выбирать их Number.

Это было также в случае throws Tв Java 5-7: T<<Throwable => T=Throwable. (Все решения скрытого броска имеют явные <RuntimeException>аргументы типа, иначе <Throwable>предполагается.)

В java8 с введением лямбда это становится проблематичным. Рассмотрим этот случай

interface Action<T extends Throwable>
{
    void doIt() throws T;
}

<T extends Throwable> void invoke(Action<T> action) throws T
{
    action.doIt(); // throws T
}    

Если мы вызываем с пустой лямбдой, что будет Tвыводиться как?

    invoke( ()->{} ); 

Единственное ограничение T- это верхняя граница Throwable. На более ранней стадии java8 T=Throwableпредполагалось. См. Этот отчет, который я подал.

Но довольно глупо делать вывод Throwableо проверенном исключении из пустого блока. Решение было предложено в отчете (который, по-видимому, принят JLS) -

If E has not been inferred from previous steps, and E is in the throw clause, 
and E has an upper constraint E<<X,
    if X:>RuntimeException, infer E=RuntimeException
    otherwise, infer E=X. (X is an Error or a checked exception)

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

Чжун Ю
источник
Что означает X:>RuntimeExceptionв вашем последнем фрагменте примера?
Марсуф
1

При sneakyThrowтипе Tявляется ограниченная переменная универсального типа без определенного типа (потому что нет места, откуда тип мог бы появиться).

With nonSneakyThrow, тип Tтого же типа, что и аргумент, поэтому в вашем примере Tof nonSneakyThrow(e);is Exception. Поскольку testSneaky()не объявляет бросок Exception, отображается ошибка.

Обратите внимание, что это известное вмешательство Generics с отмеченными исключениями.

llogiq
источник
Так что, на sneakyThrowсамом деле он не относится к какому-то конкретному типу, а «приведение» относится к такому неопределенному типу? Интересно, что на самом деле с этим происходит.
Марко Топольник,