Ein besonderes Merkmal der Inferenz vom Ausnahmetyp in Java 8

84

Beim Schreiben von Code für eine andere Antwort auf dieser Site bin ich auf diese Besonderheit gestoßen:

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

Erstens bin ich ziemlich verwirrt, warum der sneakyThrowAufruf des Compilers in Ordnung ist. Auf welchen möglichen Typ wurde geschlossen, Twenn kein ungeprüfter Ausnahmetyp erwähnt wird?

Zweitens, wenn man akzeptiert, dass dies funktioniert, warum beschwert sich der Compiler dann über den nonSneakyThrowAufruf? Sie scheinen sich sehr ähnlich zu sein.

Marko Topolnik
quelle

Antworten:

66

Es sneakyThrowwird gefolgert, dass das T von ist RuntimeException. Dies kann der Langauge-Spezifikation zur Typinferenz ( http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html ) entnommen werden.

Erstens gibt es in Abschnitt 18.1.3 einen Hinweis:

Eine Bindung des Formulars throws αdient lediglich der Information: Sie leitet die Auflösung an, um die Instanziierung von α so zu optimieren, dass es sich nach Möglichkeit nicht um einen geprüften Ausnahmetyp handelt.

Dies hat keine Auswirkungen, verweist jedoch auf den Abschnitt "Auflösung" (18.4), in dem weitere Informationen zu abgeleiteten Ausnahmetypen mit einem Sonderfall enthalten sind:

... Andernfalls, wenn der gebundene Satz enthält throws αiund die korrekten oberen Grenzen von & alpha; i sind, höchstens Exception, Throwableund Objectdann Ti = RuntimeException.

Dieser Fall gilt für sneakyThrow- die einzige Obergrenze ist Throwable, daher Twird davon RuntimeExceptionausgegangen, dass sie der Spezifikation entspricht, sodass sie kompiliert wird. Der Hauptteil der Methode ist unerheblich - die ungeprüfte Umwandlung ist zur Laufzeit erfolgreich, da dies nicht tatsächlich geschieht, sodass eine Methode übrig bleibt, die das zur Kompilierungszeit geprüfte Ausnahmesystem umgehen kann.

nonSneakyThrowwird nicht kompiliert, da diese Methode Teine Untergrenze von hat Exception(dh Tein Supertyp von Exceptionoder Exceptionselbst sein muss), was aufgrund des Typs, mit dem sie aufgerufen wird, eine geprüfte Ausnahme darstellt, sodass Tdaraus abgeleitet wird Exception.

thecoop
quelle
1
@Maksym Du musst den sneakyThrowAnruf meinen . Die besonderen Bestimmungen über den Rückschluss von throws TFormularen gab es in der Spezifikation von Java 7 nicht.
Marko Topolnik
1
Kleiner Nitpick: In nonSneakyThrow, Tmuss Exceptionnicht "ein Supertyp von" sein Exception, da es genau der Typ des Arguments ist, das zur Kompilierungszeit am Aufrufstandort deklariert wurde.
llogiq
1
@llogiq Wenn ich die Spezifikation richtig gelesen habe, hat sie eine Untergrenze von Exceptionund eine Obergrenze von Throwable, so dass die kleinste Obergrenze, die der resultierende abgeleitete Typ ist, ist Exception.
Thecoop
1
@llogiq Beachten Sie, dass der Typ des Arguments nur eine niedrigere Typgrenze festlegt, da jeder Supertyp des Arguments ebenfalls akzeptabel ist.
Marko Topolnik
2
Der ExceptionAusdruck „oder sich selbst“ mag für den Leser hilfreich sein, aber im Allgemeinen sollte beachtet werden, dass in der Spezifikation immer die Begriffe „Subtyp“ und „Supertyp“ im Sinne von „sich selbst einschließen“ verwendet werden…
Holger
17

Wenn die Typinferenz eine einzelne Obergrenze für eine Typvariable erzeugt, wird normalerweise die Obergrenze als Lösung ausgewählt. Zum Beispiel, wenn T<<Numberdie Lösung ist T=Number. Obwohl Integer, Floatusw. auch die Bedingung erfüllen konnte, gibt es keinen Grund , sie zu wählen , über Number.

Dies war auch throws Tin Java 5-7 der Fall : T<<Throwable => T=Throwable. (Sneaky-Throw-Lösungen hatten alle explizite <RuntimeException>Typargumente, andernfalls <Throwable>wird darauf geschlossen.)

In Java8 wird dies mit der Einführung von Lambda problematisch. Betrachten Sie diesen Fall

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

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

Wenn wir mit einem leeren Lambda anrufen, was würde Tdaraus abgeleitet werden?

    invoke( ()->{} ); 

Die einzige Einschränkung Tist eine Obergrenze Throwable. In einem früheren Stadium von Java8 T=Throwablewürde abgeleitet werden. Siehe diesen Bericht, den ich eingereicht habe.

Aber das ist ziemlich albern, um Throwableeine überprüfte Ausnahme aus einem leeren Block abzuleiten. In dem Bericht (der offenbar von JLS angenommen wird) wurde eine Lösung vorgeschlagen -

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)

dh wenn die Obergrenze Exceptionoder ist Throwable, wählen Sie RuntimeExceptionals Lösung. In diesem Fall gibt es einen guten Grund, einen bestimmten Subtyp der Obergrenze zu wählen.

ZhongYu
quelle
Was bedeutet X:>RuntimeExceptionin Ihrem letzten Beispiel-Snippet?
Marsouf
1

Mit sneakyThrowist der Typ Teine begrenzte generische Typvariable ohne einen bestimmten Typ (da es keinen Ort gibt, von dem der Typ stammen könnte).

Mit nonSneakyThrowdem Typ Tist vom gleichen Typ wie das Argument, also in Ihrem Beispiel, die Tvon nonSneakyThrow(e);ist Exception. Da ein testSneaky()Wurf nicht deklariert wird Exception, wird ein Fehler angezeigt.

Beachten Sie, dass dies eine bekannte Störung von Generics mit aktivierten Ausnahmen ist.

llogiq
quelle
Also, weil sneakyThrowes nicht wirklich auf einen bestimmten Typ geschlossen ist und die "Besetzung" auf einen so undefinierten Typ ist? Ich frage mich, was damit eigentlich passiert.
Marko Topolnik