Osobliwa funkcja wnioskowania o typie wyjątku w Javie 8

85

Pisząc kod dla innej odpowiedzi na tej stronie, natknąłem się na tę osobliwość:

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

Po pierwsze, jestem dość zdezorientowany, dlaczego sneakyThrowwywołanie kompilatora jest OK. Jaki możliwy typ to wywnioskowało, Tkiedy nigdzie nie ma wzmianki o niezaznaczonym typie wyjątku?

Po drugie, przyjmując, że to działa, dlaczego kompilator narzeka na nonSneakyThrowwywołanie? Wydają się do siebie bardzo podobni.

Marko Topolnik
źródło

Odpowiedzi:

66

sneakyThrowZakłada się, że T z jest RuntimeException. Można to sprawdzić w specyfikacji językowej dotyczącej wnioskowania typu ( http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html )

Po pierwsze, uwaga w sekcji 18.1.3:

Ograniczenie formularza throws αjest czysto informacyjne: kieruje rozdzielczością tak, aby zoptymalizować wystąpienie α, tak aby, jeśli to możliwe, nie był sprawdzanym typem wyjątku.

Nie wpływa to na nic, ale kieruje nas do sekcji Rozwiązanie (18.4), która zawiera więcej informacji na temat wywnioskowanych typów wyjątków ze specjalnym przypadkiem:

... W przeciwnym razie, jeśli związany zestaw zawiera throws αi, a odpowiednie górne granice αi są, co najwyżej Exception, Throwablei Object, następnie Ti = RuntimeException.

Ten przypadek dotyczy sneakyThrow- jedyną górną granicą jest Throwable, więc zakłada się , że Tjest zgodna RuntimeExceptionze specyfikacją, więc kompiluje się. Treść metody jest nieistotna - niesprawdzone rzutowanie kończy się sukcesem w czasie wykonywania, ponieważ tak się nie dzieje, pozostawiając metodę, która może pokonać system wyjątków sprawdzanych w czasie kompilacji.

nonSneakyThrownie kompiluje się, ponieważ ta metoda Tma dolną granicę Exception(tj. Tmusi być nadtypem Exceptionlub Exceptionsama w sobie), co jest sprawdzanym wyjątkiem, ze względu na typ, z którym jest wywoływana, więc Tjest wywnioskowana jako Exception.

thecoop
źródło
1
@Maksym Musisz mieć na myśli sneakyThrowwezwanie. Specjalne przepisy dotyczące wnioskowania z throws Tformularzy nie istniały w specyfikacji Java 7.
Marko Topolnik
1
Mały dzióbek: w nonSneakyThrow, Tmusi być Exception, a nie „nadtypem” Exception, ponieważ jest to dokładnie typ argumentu zadeklarowanego w czasie kompilacji w miejscu wywołania.
llogiq
1
@llogiq Jeśli poprawnie przeczytałem specyfikację, ma ona dolną granicę Exceptioni górną granicę Throwable, a więc najmniejszą górną granicą, która jest wynikiem wywnioskowanego typu, jest Exception.
thecoop
1
@llogiq Zauważ, że typ argumentu ustawia tylko dolną granicę typu, ponieważ każdy nadtyp argumentu jest również akceptowalny.
Marko Topolnik
2
Exceptionfraza „lub sama” może być pomocna dla czytelnika, ale ogólnie należy zauważyć, że specyfikacja zawsze używa terminów „podtyp” i „nadtyp” w znaczeniu „obejmujący siebie”…
Holger
17

Jeśli wnioskowanie o typie tworzy pojedynczą górną granicę dla zmiennej typu, zwykle jako rozwiązanie wybierana jest górna granica. Na przykład, jeśli T<<Numberrozwiązaniem jest T=Number. Mimo Integer, Floatitd. Mogą również spełniać ograniczenia, nie ma dobry powód, aby wybrać je Number.

To było również w przypadku throws Tw java 5-7 T<<Throwable => T=Throwable. (Wszystkie rozwiązania podstępnego rzutowania miały jawne <RuntimeException>argumenty typu, w przeciwnym razie <Throwable>są wywnioskowane).

W java8, wraz z wprowadzeniem lambdy, staje się to problematyczne. Rozważ ten przypadek

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

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

Jeśli wywołamy z pustą lambdą, co Tzostanie wywnioskowane jako?

    invoke( ()->{} ); 

Jedynym ograniczeniem Tjest górna granica Throwable. Na wcześniejszym etapie java8 T=Throwablemożna by wywnioskować. Zobacz ten raport, który złożyłem.

Ale to jest dość głupie, wnioskując Throwable, sprawdzony wyjątek z pustego bloku. W raporcie zaproponowano rozwiązanie (najwyraźniej przyjęte przez 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)

tj. jeśli górną granicą jest Exceptionlub Throwable, wybierz RuntimeExceptionjako rozwiązanie. W tym przypadku nie jest to dobry powód, aby wybrać konkretny podtyp górna granica.

ZhongYu
źródło
Jakie jest znaczenie X:>RuntimeExceptionw ostatnim przykładowym fragmencie?
marsouf
1

W sneakyThrowprzypadku typ Tjest ograniczoną zmienną typu ogólnego bez określonego typu (ponieważ nie ma miejsca, z którego może pochodzić typ).

W nonSneakyThrowprzypadku typ Tjest tego samego typu co argument, dlatego w naszym przykładzie Tof nonSneakyThrow(e);to Exception. Ponieważ testSneaky()nie deklaruje rzucenia Exception, wyświetlany jest błąd.

Zauważ, że jest to znana interferencja Generics z zaznaczonymi wyjątkami.

llogiq
źródło
Czyli sneakyThrowtak naprawdę nie jest to wywnioskowane do żadnego konkretnego typu, a „rzutowanie” dotyczy takiego niezdefiniowanego typu? Zastanawiam się, co się z tym właściwie dzieje.
Marko Topolnik