Pourquoi certains prétendent que l'implémentation Java des génériques est mauvaise?

115

J'ai parfois entendu dire qu'avec les génériques, Java ne faisait pas les choses correctement. (référence la plus proche, ici )

Pardonnez mon inexpérience, mais qu'est-ce qui les aurait améliorés?

sdellysse
la source
23
Je ne vois aucune raison de fermer cela; avec toute la merde qui circule sur les génériques, avoir des éclaircissements entre les différences entre Java et C # pourrait aider les gens à essayer d'éviter le brouillage mal informé.
Rob

Réponses:

146

Mauvais:

  • Les informations de type sont perdues au moment de la compilation, donc au moment de l'exécution, vous ne pouvez pas dire quel type il est "censé" être
  • Ne peut pas être utilisé pour les types de valeur (c'est un gros - dans .NET, un List<byte>est vraiment soutenu par un byte[]par exemple, et aucune boxe n'est requise)
  • Syntaxe pour appeler des méthodes génériques est nul (IMO)
  • La syntaxe des contraintes peut prêter à confusion
  • Le joker est généralement déroutant
  • Diverses restrictions dues à ce qui précède - casting, etc.

Bien:

  • Le caractère générique permet de spécifier la covariance / contravariance du côté appelant, ce qui est très soigné dans de nombreuses situations
  • C'est mieux que rien!
Jon Skeet
la source
51
@Paul: Je pense que j'aurais préféré une casse temporaire mais une API décente après. Ou prenez la route .NET et introduisez de nouveaux types de collection. (Les génériques .NET dans 2.0 n'ont pas cassé les applications 1.1.)
Jon Skeet
6
Avec une grande base de code existante, j'aime le fait que je puisse commencer à changer la signature d'une méthode pour utiliser des génériques sans changer tous ceux qui l'appellent.
Paul Tomblin
38
Mais les inconvénients sont énormes et permanents. Il y a une grande victoire à court terme et une perte à long terme à l'OMI.
Jon Skeet
11
Jon - "C'est mieux que rien" est subjectif :)
MetroidFan2002
6
@Rogerio: Vous avez affirmé que mon premier "mauvais" élément n'était pas correct. Mon premier élément "Bad" indique que les informations sont perdues au moment de la compilation. Pour que cela soit incorrect, vous devez dire que les informations ne sont pas perdues au moment de la compilation ... Quant à dérouter les autres développeurs, vous semblez être le seul confus par ce que je dis.
Jon Skeet
26

Le plus gros problème est que les génériques Java sont une seule chose au moment de la compilation, et vous pouvez le subvertir au moment de l'exécution. C # est loué car il effectue plus de vérification à l'exécution. Il y a de très bonnes discussions dans cet article et des liens vers d'autres discussions.

Paul Tomblin
la source
Ce n'est pas vraiment un problème, cela n'a jamais été fait pour l'exécution. C'est comme si vous disiez que les bateaux sont un problème parce qu'ils ne peuvent pas gravir les montagnes. En tant que langage, Java fait ce dont beaucoup de gens ont besoin: exprimer des types de manière significative. Pour les renforcements de type runtime, les utilisateurs peuvent toujours transmettre des Classobjets.
Vincent Cantin
1
Il a raison. votre anologie est fausse parce que les concepteurs du bateau DEVRAIENT l'avoir conçu pour escalader des montagnes. Leurs objectifs de conception n'étaient pas très bien pensés pour ce qui était nécessaire. Maintenant, nous sommes tous coincés avec juste un bateau et nous ne pouvons pas escalader des montagnes.
WiegleyJ
1
@VincentCantin - c'est certainement un problème. Par conséquent, nous nous en plaignons tous. Les génériques Java sont à moitié cuits.
Josh M.
17

Le problème principal est que Java n'a pas réellement de génériques au moment de l'exécution. C'est une fonctionnalité de compilation.

Lorsque vous créez une classe générique en Java, ils utilisent une méthode appelée "Effacement de type" pour supprimer en fait tous les types génériques de la classe et les remplacer essentiellement par Object. La version la plus élevée des génériques est que le compilateur insère simplement des casts vers le type générique spécifié chaque fois qu'il apparaît dans le corps de la méthode.

Cela présente de nombreux inconvénients. L'un des plus grands, à mon humble avis, est que vous ne pouvez pas utiliser la réflexion pour inspecter un type générique. Les types ne sont pas réellement génériques dans le code d'octet et ne peuvent donc pas être inspectés en tant que génériques.

Grand aperçu des différences ici: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
la source
2
Je ne sais pas pourquoi vous avez voté à la baisse. D'après ce que je comprends, vous avez raison, et ce lien est très instructif. Voté.
Jason Jackson
@Jason, merci. Il y avait une faute de frappe dans ma version originale, je suppose que j'ai été critiqué pour cela.
JaredPar le
3
Err, car vous pouvez utiliser la réflexion pour regarder un type générique, une signature de méthode générique. Vous ne pouvez tout simplement pas utiliser la réflexion pour inspecter les informations génériques associées à une instance paramétrée. Par exemple, si j'ai une classe avec une méthode foo (List <String>), je peux trouver par réflexion "String"
oxbow_lakes
Il existe de nombreux articles qui disent que l'effacement de type rend impossible la recherche des paramètres de type réels. Disons: Object x = new X [Y] (); pouvez-vous montrer comment, étant donné x, trouver le type Y?
user48956
@oxbow_lakes - devoir utiliser la réflexion pour trouver un type générique est presque aussi mauvais que de ne pas pouvoir le faire. Par exemple, c'est une mauvaise solution.
Josh M.
14
  1. Implémentation d'exécution (c'est-à-dire pas d'effacement de type);
  2. La possibilité d'utiliser des types primitifs (ceci est lié à (1));
  3. Bien que le joker soit utile, la syntaxe et savoir quand l'utiliser est quelque chose qui déconcertent beaucoup de gens. et
  4. Aucune amélioration des performances (à cause de (1); les génériques Java sont du sucre syntaxique pour la conversion d'objets).

(1) conduit à un comportement très étrange. Le meilleur exemple auquel je puisse penser est. Présumer:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

puis déclarez deux variables:

MyClass<T> m1 = ...
MyClass m2 = ...

Appelez maintenant getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

Le second a son argument de type générique supprimé par le compilateur car il s'agit d'un type brut (ce qui signifie que le type paramétré n'est pas fourni) même s'il n'a rien à voir avec le type paramétré.

Je mentionnerai également ma déclaration préférée du JDK:

public class Enum<T extends Enum<T>>

Mis à part le joker (qui est un sac mélangé), je pense juste que les génériques .Net sont meilleurs.

cletus
la source
Mais le compilateur vous le dira, en particulier avec l'option lint rawtypes.
Tom Hawtin - tackline
Désolé, pourquoi la dernière ligne est-elle une erreur de compilation? Je dérange avec Eclipse et je ne peux pas le faire échouer là-bas - si j'ajoute suffisamment de choses pour que le reste soit compilé.
oconnor0
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 Ce n'est pas un échec de compilation, mais un avertissement du compilateur, sans raison apparente:The expression of type List needs unchecked conversion to conform to List<String>
artbristol
Enum<T extends Enum<T>>peut sembler bizarre / redondant au début mais c'est en fait assez intéressant, du moins dans les contraintes de Java / c'est générique. Les énumérations ont une values()méthode statique donnant un tableau de leurs éléments typés comme enum, non Enum, et ce type est déterminé par le paramètre générique, ce qui signifie que vous voulez Enum<T>. Bien sûr, ce typage n'a de sens que dans le contexte d'un type énuméré, et toutes les énumérations sont des sous-classes de Enum, donc vous voulez Enum<T extends Enum>. Cependant, Java n'aime pas mélanger des types bruts avec des génériques, donc Enum<T extends Enum<T>>par souci de cohérence.
JAB
10

Je vais lancer une opinion vraiment controversée. Les génériques compliquent le langage et compliquent le code. Par exemple, disons que j'ai une carte qui mappe une chaîne à une liste de chaînes. Dans l'ancien temps, je pouvais déclarer cela simplement comme

Map someMap;

Maintenant, je dois le déclarer comme

Map<String, List<String>> someMap;

Et chaque fois que je la transmets à une méthode, je dois répéter cette longue et longue déclaration une fois de plus. À mon avis, toute cette frappe supplémentaire distrait le développeur et le sort de «la zone». De plus, lorsque le code est rempli de beaucoup de cruauté, il est parfois difficile d'y revenir plus tard et de passer rapidement au crible toute la cruauté pour trouver la logique importante.

Java a déjà la mauvaise réputation d'être l'un des langages les plus verbeux d'usage courant, et les génériques ne font qu'ajouter à ce problème.

Et qu'achetez-vous vraiment pour toute cette verbosité supplémentaire? Combien de fois avez-vous vraiment eu des problèmes lorsque quelqu'un a mis un entier dans une collection qui est censée contenir des chaînes, ou où quelqu'un a essayé de retirer une chaîne d'une collection d'entiers? Au cours de mes 10 années d'expérience dans la création d'applications Java commerciales, cela n'a jamais été une grande source d'erreurs. Donc, je ne suis pas vraiment sûr de ce que vous obtenez pour la verbosité supplémentaire. Cela me semble vraiment un bagage bureaucratique supplémentaire.

Maintenant, je vais devenir vraiment controversé. Ce que je vois comme le plus gros problème avec les collections en Java 1.4, c'est la nécessité de typer partout. Je considère ces typecasts comme des crottes extra, verbeuses qui ont beaucoup des mêmes problèmes que les génériques. Donc, par exemple, je ne peux pas simplement faire

List someList = someMap.get("some key");

je dois faire

List someList = (List) someMap.get("some key");

La raison, bien sûr, est que get () renvoie un Object qui est un supertype de List. Donc, l'attribution ne peut pas être faite sans un typage. Encore une fois, pensez à combien cette règle vous achète vraiment. D'après mon expérience, pas grand-chose.

Je pense que Java aurait été bien mieux si 1) il n'avait pas ajouté de génériques mais 2) avait à la place permis la conversion implicite d'un supertype vers un sous-type. Laissez les casts incorrects être capturés au moment de l'exécution. Alors j'aurais pu avoir la simplicité de définir

Map someMap;

et plus tard faire

List someList = someMap.get("some key");

toute la cruauté serait partie, et je ne pense vraiment pas que j'introduirais une grande nouvelle source de bogues dans mon code.

Clint Miller
la source
15
Désolé, je ne suis pas d'accord avec à peu près tout ce que vous avez dit. Mais je ne vais pas le rejeter parce que vous le soutenez bien.
Paul Tomblin
2
Bien sûr, il existe de nombreux exemples de grands systèmes performants construits dans des langages comme Python et Ruby qui font exactement ce que j'ai suggéré dans ma réponse.
Clint Miller
Je pense que toute mon objection à ce type d'idée n'est pas que ce soit une mauvaise idée en soi, mais plutôt que comme les langages modernes tels que Python et Ruby rendent la vie plus facile et plus facile pour les développeurs, les développeurs à leur tour deviennent intellectuellement complaisants et finissent par avoir un niveau inférieur. compréhension de leur propre code.
Dan Tao le
4
"Et qu'achetez-vous vraiment pour toute cette verbosité supplémentaire? ..." Cela dit au pauvre programmeur de maintenance ce qui est censé être dans ces collections. J'ai trouvé que c'était un problème avec les applications Java commerciales.
richj
4
"Combien de fois avez-vous vraiment eu des problèmes lorsque quelqu'un a mis un [x] dans une collection censée contenir [y] s?" - Oh mon garçon, j'ai perdu le compte! Même s'il n'y a pas de bogue, c'est un tueur de lisibilité. Et l'analyse de nombreux fichiers pour savoir ce que va être l'objet (ou le débogage) me tire vraiment hors de la zone. Vous aimerez peut-être Haskell - même un typage fort mais moins cruel (parce que les types sont déduits).
Martin Capodici
8

Un autre effet secondaire de leur temps de compilation et non d'exécution est que vous ne pouvez pas appeler le constructeur du type générique. Vous ne pouvez donc pas les utiliser pour implémenter une usine générique ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++

jdkoftinoff
la source
6

L'exactitude des génériques Java est vérifiée au moment de la compilation, puis toutes les informations de type sont supprimées (le processus est appelé effacement de type . Ainsi, le générique List<Integer>sera réduit à son type brut , non générique List, qui peut contenir des objets de classe arbitraire.

Cela permet d'insérer des objets arbitraires dans la liste lors de l'exécution, et il est maintenant impossible de dire quels types ont été utilisés comme paramètres génériques. Ce dernier entraîne à son tour

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Anton Gogolev
la source
5

Je souhaite que ce soit un wiki afin que je puisse ajouter à d'autres personnes ... mais ...

Problèmes:

  • Effacement de type (aucune disponibilité d'exécution)
  • Pas de prise en charge des types primatifs
  • Incompatibilité avec les annotations (ils ont tous deux été ajoutés dans la version 1.5, je ne sais toujours pas pourquoi les annotations n'autorisent pas les génériques en plus de précipiter les fonctionnalités)
  • Incompatibilité avec les tableaux. (Parfois, je veux vraiment faire quelque chose comme Class <? Etend MyObject >[], mais je ne suis pas autorisé)
  • Syntaxe et comportement des caractères génériques Wierd
  • Le fait que le support générique est incohérent entre les classes Java. Ils l'ont ajouté à la plupart des méthodes de collecte, mais de temps en temps, vous rencontrez une instance où ce n'est pas là.
Laplie Anderson
la source
5

En ignorant tout le désordre d'effacement de type, les génériques tels que spécifiés ne fonctionnent tout simplement pas.

Cela compile:

List<Integer> x = Collections.emptyList();

Mais c'est une erreur de syntaxe:

foo(Collections.emptyList());

Où foo est défini comme:

void foo(List<Integer> x) { /* method body not important */ }

Ainsi, la vérification d'un type d'expression dépend de son affectation à une variable locale ou à un paramètre réel d'un appel de méthode. A quel point est-ce fou?

Nat
la source
3
L'inférence inconsistante de javac est de la merde.
mP.
3
Je suppose que la raison pour laquelle cette dernière forme est rejetée est qu'il peut y avoir plus d'une version de "foo" en raison d'une surcharge de méthode.
Jason Creighton
2
cette critique ne s'applique plus car Java 8 a introduit une inférence de type cible améliorée
oldrinb
4

L'introduction de génériques dans Java était une tâche difficile car les architectes essayaient d'équilibrer fonctionnalité, facilité d'utilisation et rétrocompatibilité avec le code hérité. Comme on pouvait s'y attendre, des compromis ont dû être faits.

Certains estiment également que l'implémentation par Java des génériques a augmenté la complexité du langage à un niveau inacceptable (voir " Generics Considered Harmful " de Ken Arnold ). La FAQ générique d' Angelika Langer donne une assez bonne idée de la complexité des choses.

Zach Scrivena
la source
3

Java n'applique pas les génériques au moment de l'exécution, uniquement au moment de la compilation.

Cela signifie que vous pouvez faire des choses intéressantes comme ajouter les mauvais types aux collections génériques.

Powerlord
la source
1
Le compilateur vous dira que vous avez foiré si vous gérez cela.
Tom Hawtin - tackline
@Tom - pas nécessairement. Il existe des moyens de tromper le compilateur.
Paul Tomblin le
2
N'écoutez-vous pas ce que le compilateur vous dit ?? (voir mon premier commentaire)
Tom Hawtin - tackline
1
@Tom, si vous avez une ArrayList <String> et que vous la transmettez à une méthode à l'ancienne (par exemple, code hérité ou tiers) qui prend une simple List, cette méthode peut alors ajouter tout ce qu'elle veut à cette List. S'il est appliqué au moment de l'exécution, vous obtiendrez une exception.
Paul Tomblin
1
static void addToList (Liste liste) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (liste); Cela compile et fonctionne même au moment de l'exécution. Le seul moment où vous voyez un problème est lorsque vous supprimez de la liste en attente d'une chaîne et obtenez un int.
Laplie Anderson
3

Les génériques Java sont uniquement à la compilation et sont compilés en code non générique. En C #, le MSIL compilé réel est générique. Cela a d'énormes implications pour les performances, car Java effectue toujours un cast pendant l'exécution. Cliquez ici pour en savoir plus .

Szymon Rozga
la source
2

Si vous écoutez Java Posse # 279 - Entretien avec Joe Darcy et Alex Buckley , ils parlent de ce problème. Cela renvoie également à un article de blog Neal Gafter intitulé Reified Generics for Java qui dit:

De nombreuses personnes ne sont pas satisfaites des restrictions causées par la manière dont les génériques sont implémentés en Java. Plus précisément, ils ne sont pas satisfaits que les paramètres de type générique ne soient pas réifiés: ils ne sont pas disponibles au moment de l'exécution. Les génériques sont implémentés à l'aide de l'effacement, dans lequel les paramètres de type générique sont simplement supprimés au moment de l'exécution.

Ce billet de blog fait référence à une entrée plus ancienne, Puzzling Through Erasure: section de réponse , qui soulignait le point sur la compatibilité de la migration dans les exigences.

L'objectif était de fournir une compatibilité descendante du code source et du code objet, ainsi que la compatibilité de la migration.

Kevin Hakanson
la source