Pourquoi devrais-je me soucier que Java n'ait pas de génériques réifiés?


102

Cela est apparu comme une question que j'ai posée lors d'une interview récemment comme quelque chose que le candidat souhaitait voir ajouté au langage Java. Il est communément identifié comme une douleur que Java n'ait pas de génériques réifiés mais, une fois poussé, le candidat ne pouvait pas vraiment me dire le genre de choses qu'il aurait pu accomplir s'ils étaient là.

Évidemment, comme les types bruts sont autorisés en Java (et les vérifications non sécurisées), il est possible de subvertir les génériques et de se retrouver avec un List<Integer>qui (par exemple) contient en fait Strings. Cela pourrait clairement être rendu impossible si les informations de type étaient réifiées; mais il doit y avoir plus que ça !

Les gens pourraient-ils publier des exemples de choses qu'ils voudraient vraiment faire , si des génériques réifiés étaient disponibles? Je veux dire, évidemment, vous pouvez obtenir le type de a Listau moment de l'exécution - mais que feriez-vous avec?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDIT : Une mise à jour rapide à cela car les réponses semblent principalement préoccupées par la nécessité de passer un Classcomme paramètre (par exemple EnumSet.noneOf(TimeUnit.class)). Je cherchais davantage quelque chose du genre où ce n'est tout simplement pas possible . Par exemple:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Cela signifierait-il que vous pourriez obtenir la classe d'un type générique au moment de l'exécution? (si oui, j'ai un exemple!)
James B

Je pense que la plupart des désirs avec les génériques réifiables proviennent de personnes qui utilisent principalement des génériques avec des collections, et veulent que ces collections se comportent plus comme des tableaux.
kdgregory

2
La question la plus intéressante (pour moi): que faudrait-il pour implémenter des génériques de style C ++ en Java? Cela semble certainement faisable dans le runtime, mais briserait tous les chargeurs de classe existants (car il findClass()faudrait ignorer le paramétrage, mais defineClass()ne le pourrait pas). Et comme nous le savons, les pouvoirs en place sont primordiaux pour la rétrocompatibilité.
kdgregory

1
En fait, Java fournit des génériques réifiés de manière très restreinte . Je donne plus de détails dans ce fil de discussion SO: stackoverflow.com/questions/879855/…
Richard Gomes

Le JavaOne Keynote indique que Java 9 prendra en charge la réification.
Rangi Keen

Réponses:


81

Depuis les quelques fois où je suis tombé sur ce "besoin", cela se résume finalement à cette construction:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Cela fonctionne en C # en supposant qu'il Ta un constructeur par défaut . Vous pouvez même obtenir le type d'exécution typeof(T)et obtenir les constructeurs Type.GetConstructor().

La solution Java courante serait de passer l' Class<T>argument as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(il n'a pas nécessairement besoin d'être passé en tant qu'argument constructeur, car un argument de méthode convient également, ce qui précède n'est qu'un exemple, et le try-catchest également omis par souci de concision)

Pour toutes les autres constructions de type générique, le type réel peut facilement être résolu avec un peu d'aide de réflexion. Les questions-réponses ci-dessous illustrent les cas d'utilisation et les possibilités:


5
Cela fonctionne (par exemple) en C #? Comment savez-vous que T a un constructeur par défaut?
Thomas Jung

4
Oui, c'est vrai. Et ce n'est qu'un exemple basique (supposons que nous travaillons avec des javabeans). Le fait est qu'avec Java, vous ne pouvez pas obtenir la classe pendant l' exécution par T.classou T.getClass(), de sorte que vous puissiez accéder à tous ses champs, constructeurs et méthodes. Cela rend également la construction impossible.
BalusC

3
Cela me semble vraiment faible en tant que "gros problème", d'autant plus que cela ne sera probablement utile qu'en conjonction avec une réflexion très fragile autour des constructeurs / paramètres, etc.
oxbow_lakes

33
Il compile en C # à condition que vous déclariez le type comme: public class Foo <T> où T: new (). Ce qui limitera les types valides de T à ceux qui contiennent un constructeur sans paramètre.
Martin Harris

3
@Colin Vous pouvez en fait dire à java qu'un type générique donné est nécessaire pour étendre plus d'une classe ou interface. Consultez la section «Multiple Bounds» de docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Bien sûr, vous ne pouvez pas dire que l'objet fera partie d'un ensemble spécifique qui ne partage pas de méthodes qui pourraient être abstraites dans une interface, ce qui pourrait être quelque peu implémenté en C ++ en utilisant la spécialisation de modèle.)
JAB

99

La chose qui me pique le plus souvent est l'incapacité de tirer parti de l'envoi multiple sur plusieurs types génériques. Ce qui suit n'est pas possible et dans de nombreux cas, ce serait la meilleure solution:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
Il n'y a absolument aucun besoin de réification pour pouvoir faire cela. La sélection de méthode est effectuée au moment de la compilation lorsque les informations de type au moment de la compilation sont disponibles.
Tom Hawtin - tackline

26
@Tom: cela ne compile même pas à cause de l'effacement de type. Les deux sont compilés comme public void my_method(List input) {}. Je n'ai cependant jamais rencontré ce besoin, simplement parce qu'ils n'auraient pas le même nom. S'ils ont le même nom, je me demande si ce public <T extends Object> void my_method(List<T> input) {}n'est pas une meilleure idée.
BalusC

1
Hm, j'aurais tendance à éviter de surcharger avec un nombre identique de paramètres, et à préférer quelque chose comme myStringsMethod(List<String> input)et myIntegersMethod(List<Integer> input)même si une surcharge pour un tel cas était possible en Java.
Fabian Steeg

4
@Fabian: Ce qui signifie que vous devez avoir un code séparé et éviter les avantages dont vous bénéficiez <algorithm>en C ++.
David Thornley

Je suis confus, c'est essentiellement le même que mon deuxième point, mais j'ai eu 2 votes négatifs à ce sujet? Quelqu'un veut-il m'éclairer?
rsp

34

La sécurité des types vient à l'esprit. Le downcasting vers un type paramétré sera toujours dangereux sans les génériques réifiés:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

De plus, les abstractions fuiraient moins - du moins celles qui pourraient être intéressées par des informations d'exécution sur leurs paramètres de type. Aujourd'hui, si vous avez besoin d'informations d'exécution sur le type de l'un des paramètres génériques, vous devez également les transmettre Class. De cette façon, votre interface externe dépend de votre implémentation (que vous utilisiez ou non RTTI pour vos paramètres).


1
Oui - J'ai un moyen de contourner cela en créant un ParametrizedListqui copie les données dans les types de vérification de la collection source. C'est un peu comme Collections.checkedListmais peut être semé avec une collection pour commencer.
oxbow_lakes

@tackline - eh bien, quelques abstractions fuiraient moins. Si vous avez besoin d'accéder aux métadonnées de type dans votre implémentation, l'interface externe vous le dira car les clients doivent vous envoyer un objet de classe.
gustafc

... ce qui signifie qu'avec les génériques réifiés, vous pouvez ajouter des éléments comme T.class.getAnnotation(MyAnnotation.class)(où Test un type générique) sans changer l'interface externe.
gustafc

@gustafc: si vous pensez que les modèles C ++ vous offrent une sécurité de type complète, lisez ceci: kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory: Je n'ai jamais dit que C ++ était sûr à 100% - juste que l'effacement endommage la sécurité du type. Comme vous le dites vous-même, "C ++, il s'avère, a sa propre forme d'effacement de type, connue sous le nom de fonte de pointeur de style C." Mais Java ne fait que des cast dynamiques (pas de réinterprétation), donc la réification brancherait tout cela dans le système de types.
gustafc

26

Vous pourriez créer des tableaux génériques dans votre code.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

quel est le problème avec Object? Un tableau d'objets est de toute façon un tableau de références. Ce n'est pas comme si les données de l'objet reposaient sur la pile - tout est dans le tas.
Ran Biron

19
Sécurité de type. Je peux mettre ce que je veux dans Object [], mais uniquement des chaînes dans String [].
Turnor

3
Ran: Sans être sarcastique: vous aimeriez peut-être utiliser un langage de script au lieu de Java, alors vous avez la flexibilité de variables non typées n'importe où!
mouton volant

2
Les tableaux sont covariants (et donc non sécurisés) dans les deux langues. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Compile bien dans les deux langues. Ne fonctionne pas très bien.
Martijn

16

C'est une vieille question, il y a une tonne de réponses, mais je pense que les réponses existantes sont hors de propos.

«réifié» signifie simplement réel et signifie généralement le contraire de l'effacement de type.

Le gros problème lié aux génériques Java:

  • Cette horrible exigence de boxe et de déconnexion entre les primitives et les types de référence. Ce n'est pas directement lié à la réification ou à l'effacement de type. C # / Scala corrige ce problème.
  • Aucun type de soi. JavaFX 8 a dû supprimer les "constructeurs" pour cette raison. Absolument rien à voir avec l'effacement de caractères. Scala corrige cela, pas sûr de C #.
  • Pas de variance de type côté déclaration. C # 4.0 / Scala ont ceci. Absolument rien à voir avec l'effacement de caractères.
  • Impossible de surcharger void method(List<A> l)et method(List<B> l). Cela est dû à l'effacement de caractères mais est extrêmement mesquin.
  • Pas de prise en charge de la réflexion de type d'exécution. C'est le cœur de l'effacement de caractères. Si vous aimez les compilateurs super avancés qui vérifient et prouvent autant de logique de votre programme au moment de la compilation, vous devriez utiliser la réflexion le moins possible et ce type d'effacement de type ne devrait pas vous déranger. Si vous aimez la programmation de type plus inégale, scripty et dynamique et que vous ne vous souciez pas tellement d'un compilateur qui prouve autant que possible votre logique correcte, alors vous voulez une meilleure réflexion et il est important de corriger l'effacement du type.

1
La plupart du temps, je trouve cela difficile avec les cas de sérialisation. Vous aimeriez souvent être en mesure de détecter les types de classe des éléments génériques qui sont sérialisés, mais vous êtes arrêté à cause de l'effacement de type. Il est difficile de faire quelque chose comme çadeserialize(thingy, List<Integer>.class)
Cogman

3
Je pense que c'est la meilleure réponse. Surtout les parties décrivant les problèmes réellement dus à l'effacement de type et ce qui ne sont que des problèmes de conception du langage Java. La première chose que je dis aux gens qui commencent à pleurnicher est que Scala et Haskell travaillent également par effacement de caractères.
aemxdp

13

La sérialisation serait plus simple avec la réification. Ce que nous voudrions, c'est

deserialize(thingy, List<Integer>.class);

Ce que nous devons faire c'est

deserialize(thing, new TypeReference<List<Integer>>(){});

semble moche et fonctionne bizarrement.

Il y a aussi des cas où il serait vraiment utile de dire quelque chose comme

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Ces choses ne mordent pas souvent, mais elles mordent quand elles se produisent.


Ce. Mille fois cela. Chaque fois que j'essaie d'écrire du code de désérialisation en Java, je passe la journée à me plaindre de ne pas travailler dans autre chose
Basic

11

Mon exposition à Java Geneircs est assez limitée, et à part les points que d'autres réponses ont déjà mentionnés, il y a un scénario expliqué dans le livre Java Generics and Collections , par Maurice Naftalin et Philip Walder, où les génériques réifiés sont utiles.

Étant donné que les types ne sont pas réifiables, il n'est pas possible d'avoir des exceptions paramétrées.

Par exemple, la déclaration du formulaire ci-dessous n'est pas valide.

class ParametericException<T> extends Exception // compile error

En effet, la clause catch vérifie si l'exception levée correspond à un type donné. Cette vérification est identique à la vérification effectuée par le test d'instance et comme le type n'est pas réifiable, la forme d'instruction ci-dessus n'est pas valide.

Si le code ci-dessus était valide, la gestion des exceptions de la manière ci-dessous aurait été possible:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Le livre mentionne également que si les génériques Java sont définis de la même manière que les modèles C ++ (expansion), cela peut conduire à une implémentation plus efficace car cela offre plus de possibilités d'optimisation. Mais n'offre aucune explication plus que cela, donc toute explication (pointeurs) de la part de personnes bien informées serait utile.


1
C'est un point valable, mais je ne sais pas trop pourquoi une classe d'exception ainsi paramétrée serait utile. Pourriez-vous modifier votre réponse pour contenir un bref exemple de quand cela pourrait être utile?
oxbow_lakes

@oxbow_lakes: Désolé Ma connaissance de Java Generics est assez limitée et j'essaye de l'améliorer. Alors maintenant, je ne suis pas en mesure de penser à un exemple où l'exception paramétrée pourrait être utile. J'essaierai d'y penser. THX.
sateesh

Il pourrait agir comme un substitut à l'héritage multiple de types d'exception.
meriton

L'amélioration des performances est qu'actuellement, un paramètre de type doit hériter d'Object, ce qui nécessite l'encadrement des types primitifs, ce qui impose une surcharge d'exécution et de mémoire.
meriton

8

Les tableaux joueraient probablement beaucoup mieux avec les génériques s'ils étaient réifiés.


2
Bien sûr, mais ils auraient encore des problèmes. List<String>n'est pas un List<Object>.
Tom Hawtin - tackline

D'accord - mais uniquement pour les primitives (Integer, Long, etc.). Pour un objet "normal", c'est la même chose. Étant donné que les primitives ne peuvent pas être un type paramétré (un problème beaucoup plus grave, du moins à mon humble avis), je ne vois pas cela comme une vraie douleur.
Ran Biron

3
Le problème avec les tableaux est leur covariance, rien à voir avec la réification.
Recurse le

5

J'ai un wrapper qui présente un jeu de résultats jdbc comme un itérateur, (cela signifie que je peux tester des opérations basées sur une base de données beaucoup plus facilement grâce à l'injection de dépendances).

L'API ressemble à Iterator<T>où T est un type qui peut être construit en utilisant uniquement des chaînes dans le constructeur. L'itérateur examine ensuite les chaînes renvoyées par la requête SQL, puis essaie de les faire correspondre à un constructeur de type T.

Dans la manière actuelle d'implémentation des génériques, je dois également passer dans la classe des objets que je vais créer à partir de mon jeu de résultats. Si je comprends bien, si les génériques étaient réifiés, je pourrais simplement appeler T.getClass () pour obtenir ses constructeurs, et ensuite ne pas avoir à convertir le résultat de Class.newInstance (), ce qui serait beaucoup plus soigné.

Fondamentalement, je pense que cela facilite l'écriture d'API (par opposition à la simple écriture d'une application), car vous pouvez déduire beaucoup plus d'objets, et donc moins de configuration sera nécessaire ... Je n'ai pas apprécié les implications des annotations jusqu'à ce que je les ai vus être utilisés dans des choses comme spring ou xstream au lieu de rames de configuration.


Mais passer dans la classe me semble plus sûr partout. Dans tous les cas, la création réfléchie d'instances à partir de requêtes de base de données est extrêmement fragile face aux modifications telles que la refactorisation de toute façon (à la fois dans votre code et dans la base de données). Je suppose que je cherchais des choses pour lesquelles il n'est tout simplement pas possible de fournir la classe
oxbow_lakes

5

Une bonne chose serait d'éviter la boxe pour les types primitifs (valeur). Ceci est en quelque sorte lié à la plainte de tableau que d'autres ont soulevée, et dans les cas où l'utilisation de la mémoire est limitée, cela pourrait en fait faire une différence significative.

Il existe également plusieurs types de problèmes lors de l'écriture d'un framework où il est important de pouvoir réfléchir sur le type paramétré. Bien sûr, cela peut être contourné en passant un objet de classe au moment de l'exécution, mais cela obscurcit l'API et impose une charge supplémentaire à l'utilisateur du framework.


2
Cela revient essentiellement à pouvoir faire new T[]là où T est de type primitif!
oxbow_lakes

2

Ce n'est pas que vous obtiendrez quelque chose d'extraordinaire. Ce sera simplement plus simple à comprendre. L'effacement de type semble être une période difficile pour les débutants, et cela nécessite finalement une compréhension du fonctionnement du compilateur.

Mon opinion est que les génériques sont simplement un extra qui évite beaucoup de casting redondant.


C'est certainement ce que sont les génériques en Java . En C # et dans d'autres langages, ils sont un outil puissant
base

1

Quelque chose que toutes les réponses ici ont manqué et qui est constamment un casse-tête pour moi est que, puisque les types sont effacés, vous ne pouvez pas hériter d'une interface générique deux fois. Cela peut poser problème lorsque vous souhaitez créer des interfaces à granularité fine.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

En voici une qui m'a attrapé aujourd'hui: sans réification, si vous écrivez une méthode qui accepte une liste varargs d'éléments génériques ... les appelants peuvent PENSER qu'ils sont typés, mais passer accidentellement dans n'importe quel vieux crud, et faire exploser votre méthode.

Cela semble peu probable? ... Bien sûr, jusqu'à ce que ... vous utilisiez Class comme type de données. À ce stade, votre appelant vous enverra volontiers de nombreux objets de classe, mais une simple faute de frappe vous enverra des objets de classe qui n'adhèrent pas à T, et un désastre se produit.

(NB: j'ai peut-être fait une erreur ici, mais en cherchant sur Google "generics varargs", ce qui précède semble être exactement ce à quoi vous vous attendez. Ce qui en fait un problème pratique, c'est l'utilisation de Class, je pense - les appelants semblent pour être moins prudent :()


Par exemple, j'utilise un paradigme qui utilise des objets de classe comme clé dans les cartes (c'est plus complexe qu'une simple carte - mais conceptuellement, c'est ce qui se passe).

par exemple, cela fonctionne très bien dans Java Generics (exemple trivial):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

par exemple sans réification en Java Generics, celui-ci accepte TOUT objet "Class". Et ce n'est qu'une toute petite extension du code précédent:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Les méthodes ci-dessus doivent être écrites des milliers de fois dans un projet individuel - de sorte que le risque d'erreur humaine devient élevé. Les erreurs de débogage se révèlent «pas amusantes». J'essaie actuellement de trouver une alternative, mais je n'ai pas beaucoup d'espoir.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.