Perché Java non consente le sottoclassi generiche di Throwable?


146

Secondo la Java Language Sepecification , 3a edizione:

È un errore in fase di compilazione se una classe generica è una sottoclasse diretta o indiretta di Throwable.

Vorrei capire perché questa decisione è stata presa. Cosa c'è che non va nelle eccezioni generiche?

(Per quanto ne so, i generici sono semplicemente zucchero sintattico in fase di compilazione e verranno Objectcomunque tradotti nei .classfile, quindi dichiarare efficacemente una classe generica è come se tutto fosse in essa Object. Per favore correggimi se sbaglio .)


1
Gli argomenti di tipo generico sono sostituiti dal limite superiore, che per impostazione predefinita è Object. Se hai qualcosa come List <? estende A>, quindi A viene utilizzato nei file di classe.
Torsten Marek,

Grazie @Torsten. Non avevo pensato a quel caso prima.
Hosam Aly,

2
È una buona domanda per l'intervista, questa.
Skaffman,

@TorstenMarek: se uno chiama myList.get(i), ovviamente getrestituisce ancora un Object. Il compilatore inserisce un cast Aper catturare parte del vincolo in fase di esecuzione? In caso contrario, l'OP ha ragione a dire che alla fine si riduce a Objects in fase di esecuzione. (Il file di classe contiene sicuramente metadati A, ma sono solo metadati AFAIK.)
Mihai Danila,

Risposte:


155

Come detto mark, i tipi non sono riutilizzabili, il che è un problema nel seguente caso:

try {
   doSomeStuff();
} catch (SomeException<Integer> e) {
   // ignore that
} catch (SomeException<String> e) {
   crashAndBurn()
}

Entrambi SomeException<Integer>e SomeException<String>vengono cancellati nello stesso tipo, non c'è modo per la JVM di distinguere le istanze di eccezione, e quindi non c'è modo di dire quale catchblocco debba essere eseguito.


3
ma cosa significa "riutilizzabile"?
Aberrant80,

61
Quindi la regola non dovrebbe essere "i tipi generici non possono essere sottoclassabili da lanciare" ma invece "le clausole di cattura devono sempre usare tipi non elaborati".
Archie,

3
Potrebbero semplicemente impedire l'uso insieme di due blocchi di cattura dello stesso tipo. In modo che l'utilizzo di SomeExc <Integer> da solo sarebbe legale, solo l'utilizzo di SomeExc <Integer> e SomeExc <String> insieme sarebbe illegale. Ciò non creerebbe problemi, o sarebbe?
Viliam Búr,

3
Oh, ora capisco. La mia soluzione potrebbe causare problemi con RuntimeExceptions, che non devono essere dichiarati. Quindi, se SomeExc è una sottoclasse di RuntimeException, potrei lanciare e catturare esplicitamente SomeExc <Integer>, ma forse un'altra funzione sta silenziosamente lanciando SomeExc <String> e anche il mio blocco catch per SomeExc <Integer> lo catturerebbe accidentalmente.
Viliam Búr,

4
@ SuperJedi224 - No. Li fa bene - dato il vincolo che i generici dovevano essere retrocompatibili.
Stephen C,

14

Ecco un semplice esempio di come utilizzare l'eccezione:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}

Il corpo dell'istruzione TRy genera l'eccezione con un determinato valore, che viene catturato dalla clausola catch.

Al contrario, la seguente definizione di nuova eccezione è vietata, poiché crea un tipo con parametri:

class ParametricException<T> extends Exception {  // compile-time error
  private final T value;
  public ParametricException(T value) { this.value = value; }
  public T getValue() { return value; }
}

Un tentativo di compilare quanto sopra riporta un errore:

% javac ParametricException.java
ParametricException.java:1: a generic class may not extend
java.lang.Throwable
class ParametricException<T> extends Exception {  // compile-time error
                                     ^
1 error

Questa restrizione è ragionevole perché quasi tutti i tentativi di rilevare tale eccezione devono fallire, poiché il tipo non è riutilizzabile. Ci si potrebbe aspettare che un uso tipico dell'eccezione sia simile al seguente:

class ParametricExceptionTest {
  public static void main(String[] args) {
    try {
      throw new ParametricException<Integer>(42);
    } catch (ParametricException<Integer> e) {  // compile-time error
      assert e.getValue()==42;
    }
  }
}

Ciò non è consentito, poiché il tipo nella clausola catch non è riutilizzabile. Al momento della stesura di questo documento, il compilatore Sun riporta una cascata di errori di sintassi in tal caso:

% javac ParametricExceptionTest.java
ParametricExceptionTest.java:5: <identifier> expected
    } catch (ParametricException<Integer> e) {
                                ^
ParametricExceptionTest.java:8: ')' expected
  }
  ^
ParametricExceptionTest.java:9: '}' expected
}
 ^
3 errors

Poiché le eccezioni non possono essere parametriche, la sintassi è limitata in modo che il tipo debba essere scritto come identificatore, senza il seguente parametro.


2
Cosa intendi quando dici "riutilizzabile"? "riutilizzabile" non è una parola.
ForYourOwnGood

1
Non conoscevo la parola da solo, ma una rapida ricerca in google mi ha fatto questo: java.sun.com/docs/books/jls/third_edition/html/…
Hosam Aly


13

È essenzialmente perché è stato progettato in modo negativo.

Questo problema impedisce un design pulito e astratto, ad es.

public interface Repository<ID, E extends Entity<ID>> {

    E getById(ID id) throws EntityNotFoundException<E, ID>;
}

Il fatto che una clausola di cattura fallisca per i generici non viene reificata, non è una scusa per questo. Il compilatore potrebbe semplicemente non consentire tipi generici concreti che estendono Throwable o non consentano generici all'interno di clausole di cattura.



1
L'unico modo in cui avrebbero potuto progettarlo meglio era rendere incompatibili ~ 10 anni di codice dei clienti. È stata una decisione commerciale fattibile. Il design era corretto ... dato il contesto .
Stephen C,

1
Quindi, come prenderai questa eccezione? L'unico modo che funzionerebbe è catturare il tipo grezzo EntityNotFoundException. Ma ciò renderebbe inutili i generici.
Frans,

4

I generici sono controllati in fase di compilazione per la correttezza del tipo. Le informazioni sul tipo generico vengono quindi rimosse in un processo chiamato cancellazione del tipo . Ad esempio, List<Integer>verrà convertito nel tipo non genericoList .

A causa della cancellazione del tipo , i parametri del tipo non possono essere determinati in fase di esecuzione.

Supponiamo che ti sia permesso di estendere in Throwablequesto modo:

public class GenericException<T> extends Throwable

Consideriamo ora il seguente codice:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

A causa della cancellazione del tipo , il runtime non saprà quale blocco catch eseguire.

Pertanto è un errore in fase di compilazione se una classe generica è una sottoclasse diretta o indiretta di Throwable.

Fonte: problemi con la cancellazione del tipo


Grazie. Questa è la stessa risposta di quella fornita da Torsten .
Hosam Aly,

No non lo è. La risposta di Torsten non mi ha aiutato, perché non ha spiegato che tipo di cancellazione / reificazione è.
Good Night Nerd Pride,

2

Mi aspetto che sia perché non c'è modo di garantire la parametrizzazione. Considera il seguente codice:

try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

Come notate, la parametrizzazione è solo zucchero sintattico. Tuttavia, il compilatore cerca di garantire che la parametrizzazione rimanga coerente tra tutti i riferimenti a un oggetto nell'ambito della compilazione. Nel caso di un'eccezione, il compilatore non ha modo di garantire che MyException venga generato solo da un ambito che sta elaborando.


Sì, ma perché non viene contrassegnato come "non sicuro", come ad esempio con i cast?
eljenso,

Perché con un cast, stai dicendo al compilatore "So che questo percorso di esecuzione produce il risultato atteso". Con un'eccezione, non puoi dire (per tutte le possibili eccezioni) "So dove è stato gettato." Ma, come ho detto sopra, è un'ipotesi; Non ero lì.
kdgregory,

"So che questo percorso di esecuzione produce il risultato previsto." Non lo sai, lo speri. Ecco perché i generici e i downcast sono staticamente non sicuri, ma sono comunque consentiti. Ho votato a favore della risposta di Torsten, perché lì vedo il problema. Qui io no.
eljenso

Se non sai che un oggetto è di un tipo particolare, non dovresti lanciarlo. L'idea generale di un cast è che tu abbia più conoscenza del compilatore e stai rendendo tale conoscenza esplicitamente parte del codice.
kdgregory,

Sì, e qui potresti avere più conoscenza del compilatore, dal momento che vuoi fare una conversione non controllata da MyException a MyException <Foo>. Forse "sai" che sarà un MyException <Foo>.
eljenso
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.