Una caratteristica peculiare dell'inferenza del tipo di eccezione in Java 8


85

Mentre scrivevo il codice per un'altra risposta su questo sito mi sono imbattuto in questa particolarità:

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

Innanzitutto, sono abbastanza confuso sul motivo per cui la sneakyThrowchiamata è OK per il compilatore. Per quale possibile tipo ha dedotto Tquando non viene menzionato da nessuna parte un tipo di eccezione non controllato?

Secondo, ammettendo che funzioni, perché il compilatore si lamenta della nonSneakyThrowchiamata? Sembrano molto simili.

Risposte:


66

Si sneakyThrowdeduce che la T di sia RuntimeException. Questo può essere seguito dalle specifiche di lingua sull'inferenza del tipo ( http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html )

Innanzitutto, c'è una nota nella sezione 18.1.3:

Un limite della forma throws αè puramente informativo: indirizza la risoluzione per ottimizzare l'istanziazione di α in modo che, se possibile, non sia un tipo di eccezione controllata.

Questo non influisce su nulla, ma ci rimanda alla sezione Risoluzione (18.4), che contiene maggiori informazioni sui tipi di eccezione dedotti con un caso speciale:

... In caso contrario, se il set legato contiene throws αi, ei limiti superiori propri della αi sono, al massimo, Exception, Throwable, e Object, quindi Ti = RuntimeException.

Questo caso si applica a sneakyThrow: l'unico limite superiore è Throwable, quindi Tsi deduce che sia RuntimeExceptionconforme alle specifiche, quindi viene compilato. Il corpo del metodo è irrilevante: il cast non controllato ha esito positivo in fase di esecuzione perché in realtà non accade, lasciando un metodo che può annullare il sistema di eccezioni verificate in fase di compilazione.

nonSneakyThrownon viene compilato in quanto il metodo Tha un limite inferiore di Exception(cioè Tdeve essere un supertipo di Exception, o Exceptionse stesso), che è un'eccezione controllata, a causa del tipo con cui viene chiamato, quindi Tviene dedotto come Exception.


1
@Maksym Devi intendere la sneakyThrowchiamata. Le norme speciali sull'inferenza delle throws Tforme non esistevano nelle specifiche di Java 7.
Marko Topolnik

1
Piccolo nitpick: In nonSneakyThrow, Tdeve essere Exception, non "un supertipo di" Exception, perché è esattamente il tipo di argomento dichiarato in fase di compilazione nel sito della chiamata.
llogiq

1
@llogiq Se ho letto correttamente la specifica, ha un limite inferiore di Exceptione un limite superiore di Throwable, quindi il limite superiore minimo, che è il tipo inferito risultante, è Exception.
thecoop

1
@llogiq Nota che il tipo dell'argomento imposta solo un limite di tipo inferiore perché anche qualsiasi supertipo dell'argomento è accettabile.
Marko Topolnik

2
la Exceptionfrase "o se stessa" potrebbe essere utile al lettore, ma in generale, va notato che la specifica utilizza sempre i termini "sottotipo" e "supertipo" nel senso di "incluso se stesso" ...
Holger

17

Se l'inferenza del tipo produce un unico limite superiore per una variabile di tipo, in genere il limite superiore viene scelto come soluzione. Ad esempio, se T<<Number, la soluzione è T=Number. Sebbene Integer, Floatecc. , Possano anche soddisfare il vincolo, non ci sono buoni motivi per sceglierli Number.

Questo è stato anche il caso di throws Tin Java 5-7: T<<Throwable => T=Throwable. (Le soluzioni di lancio subdole avevano tutte <RuntimeException>argomenti di tipo esplicito , altrimenti <Throwable>viene dedotto.)

In java8, con l'introduzione di lambda, questo diventa problematico. Considera questo caso

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

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

Se invochiamo con un lambda vuoto, cosa verrebbe Tdedotto come?

    invoke( ()->{} ); 

L'unico vincolo Tè un limite superiore Throwable. Nella fase precedente di java8, T=Throwablesarebbe stato dedotto. Vedi questo rapporto che ho presentato.

Ma questo è abbastanza sciocco, dedurre Throwable, un'eccezione controllata, da un blocco vuoto. Una soluzione è stata proposta nella relazione (che è apparentemente adottata da 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)

cioè se il limite superiore è Exceptiono Throwable, scegli RuntimeExceptioncome soluzione. In questo caso, non v'è un buon motivo per scegliere un particolare sottotipo del limite superiore.


Qual è il significato del X:>RuntimeExceptiontuo ultimo snippet di esempio?
marsouf

1

Con sneakyThrow, il tipo Tè una variabile di tipo generico limitato senza un tipo specifico (perché non esiste la provenienza del tipo).

Con nonSneakyThrow, il tipo Tè lo stesso tipo dell'argomento, quindi nel tuo esempio, il Tdi nonSneakyThrow(e);è Exception. Poiché testSneaky()non dichiara un lancio Exception, viene visualizzato un errore.

Notare che questa è una nota interferenza dei Generics con eccezioni controllate.


Quindi perché sneakyThrownon è effettivamente dedotto da un tipo specifico e il "cast" è un tipo così indefinito? Mi chiedo cosa succede effettivamente con questo.
Marko Topolnik
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.