Pourquoi Java n'autorise-t-il pas les sous-classes génériques de Throwable?

146

Selon la spécification du langage Java , 3e édition:

Il s'agit d'une erreur de compilation si une classe générique est une sous-classe directe ou indirecte de Throwable.

Je souhaite comprendre pourquoi cette décision a été prise. Quel est le problème avec les exceptions génériques?

(Autant que je sache, les génériques sont simplement du sucre syntaxique au moment de la compilation, et ils seront traduits de Objecttoute façon dans les .classfichiers, donc déclarer efficacement une classe générique est comme si tout ce qu'elle contient était un Object. Veuillez me corriger si je me trompe .)

Hosam Aly
la source
1
Les arguments de type générique sont remplacés par la limite supérieure, qui par défaut est Object. Si vous avez quelque chose comme List <? étend A>, alors A est utilisé dans les fichiers de classe.
Torsten Marek
Merci @Torsten. Je n'avais pas pensé à ce cas avant.
Hosam Aly
2
C'est une bonne question d'entrevue, celle-ci.
skaffman
@TorstenMarek: Si on appelle myList.get(i), getrenvoie évidemment toujours un Object. Le compilateur insère-t-il un cast pour Acapturer une partie de la contrainte au moment de l'exécution? Sinon, l'OP a raison de dire qu'en fin de compte, il se résume à Objects au moment de l'exécution. (Le fichier de classe contient certainement des métadonnées sur A, mais ce ne sont que des métadonnées AFAIK.)
Mihai Danila

Réponses:

155

Comme Mark l'a dit, les types ne sont pas réifiables, ce qui pose un problème dans le cas suivant:

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

Les deux SomeException<Integer>et SomeException<String>sont effacés au même type, il n'y a aucun moyen pour la JVM de distinguer les instances d'exception, et donc aucun moyen de dire quel catchbloc doit être exécuté.

Torsten Marek
la source
3
mais que veut dire «réifiable»?
aberrant80
61
La règle ne doit donc pas être "les types génériques ne peuvent pas sous-classer Throwable" mais à la place "les clauses catch doivent toujours utiliser des types bruts".
Archie
3
Ils pourraient simplement interdire l'utilisation de deux blocs catch avec le même type ensemble. Pour que l'utilisation de SomeExc <Integer> seul soit légale, seule l'utilisation de SomeExc <Integer> et SomeExc <String> ensemble serait illégale. Cela ne poserait aucun problème, ou le ferait-il?
Viliam Búr le
3
Oh, maintenant je comprends. Ma solution poserait des problèmes avec RuntimeExceptions, qui n'ont pas à être déclarées. Donc, si SomeExc est une sous-classe de RuntimeException, je pourrais lancer et attraper explicitement SomeExc <Integer>, mais peut-être qu'une autre fonction lance silencieusement SomeExc <String> et mon bloc catch pour SomeExc <Integer> l'attraperait accidentellement aussi.
Viliam Búr
4
@ SuperJedi224 - Non. Cela les fait bien - étant donné la contrainte que les génériques devaient être rétrocompatibles.
Stephen C
14

Voici un exemple simple d'utilisation de l'exception:

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

Le corps de l'instruction TRy lève l'exception avec une valeur donnée, qui est interceptée par la clause catch.

En revanche, la définition suivante d'une nouvelle exception est interdite, car elle crée un type paramétré:

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

Une tentative de compilation de ce qui précède signale une erreur:

% 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

Cette restriction est judicieuse car presque toute tentative pour intercepter une telle exception doit échouer, car le type n'est pas réifiable. On peut s'attendre à ce qu'une utilisation typique de l'exception ressemble à ce qui suit:

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

Cela n'est pas autorisé, car le type dans la clause catch n'est pas réifiable. Au moment d'écrire ces lignes, le compilateur Sun signale une cascade d'erreurs de syntaxe dans un tel cas:

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

Étant donné que les exceptions ne peuvent pas être paramétriques, la syntaxe est restreinte de sorte que le type doit être écrit en tant qu'identificateur, sans paramètre suivant.

IAdapter
la source
2
Que voulez-vous dire quand vous dites «réifiable»? «réifiable» n'est pas un mot.
ForYourOwnGood
1
Je ne connaissais pas le mot moi-même, mais une recherche rapide dans Google m'a permis d'obtenir ceci: java.sun.com/docs/books/jls/third_edition/html/…
Hosam Aly
13

C'est essentiellement parce qu'il a été mal conçu.

Ce problème empêche une conception abstraite propre, par exemple

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

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

Le fait qu'une clause catch échouerait si les génériques ne sont pas réifiés n'est pas une excuse pour cela. Le compilateur pourrait simplement interdire les types génériques concrets qui étendent Throwable ou interdire les génériques à l'intérieur des clauses catch.

Michele Sollecito
la source
+1. ma réponse - stackoverflow.com/questions/30759692/…
ZhongYu
1
La seule façon dont ils auraient pu mieux le concevoir était de rendre incompatible environ 10 ans de code client. C'était une décision commerciale viable. La conception était correcte ... compte tenu du contexte .
Stephen C
1
Alors, comment allez-vous attraper cette exception? La seule façon de fonctionner est d'attraper le type brut EntityNotFoundException. Mais cela rendrait les génériques inutiles.
Frans
4

Les génériques sont vérifiés au moment de la compilation pour la correction du type. Les informations de type générique sont ensuite supprimées dans un processus appelé effacement de type . Par exemple, List<Integer>sera converti en type non génériqueList .

En raison de l' effacement du type , les paramètres de type ne peuvent pas être déterminés au moment de l'exécution.

Supposons que vous êtes autorisé à vous étendre Throwablecomme ceci:

public class GenericException<T> extends Throwable

Considérons maintenant le code suivant:

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

En raison de l' effacement du type , le runtime ne saura pas quel bloc catch exécuter.

C'est donc une erreur de compilation si une classe générique est une sous-classe directe ou indirecte de Throwable.

Source: problèmes d'effacement de type

outdev
la source
Merci. C'est la même réponse que celle fournie par Torsten .
Hosam Aly
Non ce n'est pas. La réponse de Torsten ne m'a pas aidé, car elle n'expliquait pas le type d'effacement / réification.
Good Night Nerd Pride
2

Je suppose que c'est parce qu'il n'y a aucun moyen de garantir le paramétrage. Considérez le code suivant:

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

Comme vous le notez, le paramétrage n'est que du sucre syntaxique. Cependant, le compilateur essaie de s'assurer que le paramétrage reste cohérent dans toutes les références à un objet dans la portée de compilation. Dans le cas d'une exception, le compilateur n'a aucun moyen de garantir que MyException est uniquement levée à partir d'une étendue qu'il traite.

kdgregory
la source
Oui, mais pourquoi n'est-il pas alors signalé comme "dangereux", comme pour les lancers par exemple?
eljenso
Parce qu'avec une distribution, vous dites au compilateur "Je sais que ce chemin d'exécution produit le résultat attendu." À une exception près, vous ne pouvez pas dire (pour toutes les exceptions possibles) «Je sais où cela a été lancé». Mais, comme je l'ai dit ci-dessus, c'est une supposition; Je n'étais pas là.
kdgregory
"Je sais que ce chemin d'exécution produit le résultat attendu." Vous ne savez pas, vous l'espérez. C'est pourquoi les diffusions génériques et descendantes sont statiquement dangereuses, mais elles sont néanmoins autorisées. J'ai voté pour la réponse de Torsten, car là je vois le problème. Ici, je ne le fais pas.
eljenso
Si vous ne savez pas qu'un objet est d'un type particulier, vous ne devriez pas le lancer. L'idée même d'un cast est que vous avez plus de connaissances que le compilateur et que vous intégrez explicitement ces connaissances au code.
kdgregory
Oui, et ici, vous pouvez également avoir plus de connaissances que le compilateur, car vous souhaitez effectuer une conversion non cochée de MyException vers MyException <Foo>. Peut-être que vous «savez» que ce sera une MyException <Foo>.
eljenso