La méthode a le même effacement qu'une autre méthode de type

381

Pourquoi n'est-il pas légal d'avoir les deux méthodes suivantes dans la même classe?

class Test{
   void add(Set<Integer> ii){}
   void add(Set<String> ss){}
}

Je reçois le compilation error

La méthode add (Set) a la même fonction d'effacement add (Set) qu'une autre méthode de type Test.

alors que je peux contourner cela, je me demandais pourquoi javac n'aime pas ça.

Je peux voir que dans de nombreux cas, la logique de ces deux méthodes serait très similaire et pourrait être remplacée par une seule

public void add(Set<?> set){}

mais ce n'est pas toujours le cas.

C'est très ennuyeux si vous voulez en avoir deux constructorsqui prennent ces arguments car vous ne pouvez pas simplement changer le nom de l'un des constructors.

Omry Yadan
la source
1
que faire si vous manquez de structures de données et que vous avez encore besoin de plus de versions?
Omry Yadan
1
Vous pouvez créer des classes personnalisées qui héritent des versions de base.
willlma
1
OP, avez-vous trouvé une solution au problème du constructeur? Je dois accepter deux types de Listet je ne sais pas comment y faire face.
Tomáš Zato - Reinstate Monica
9
Lorsque je travaille avec Java, je manque vraiment C # ...
Svish
1
@ TomášZato, j'ai résolu cela en ajoutant des paramètres factices au constructeur: booléen noopSignatureOverload.
Nthalk

Réponses:

357

Cette règle vise à éviter les conflits dans le code hérité qui utilise toujours des types bruts.

Voici une illustration de la raison pour laquelle cela n'a pas été autorisé, tirée du JLS. Supposons que, avant l'introduction des génériques en Java, j'ai écrit du code comme celui-ci:

class CollectionConverter {
  List toList(Collection c) {...}
}

Vous prolongez ma classe, comme ceci:

class Overrider extends CollectionConverter{
  List toList(Collection c) {...}
}

Après l'introduction des génériques, j'ai décidé de mettre à jour ma bibliothèque.

class CollectionConverter {
  <T> List<T> toList(Collection<T> c) {...}
}

Vous n'êtes pas prêt à faire des mises à jour, alors vous laissez votre Overriderclasse tranquille. Afin de remplacer correctement la toList()méthode, les concepteurs de langage ont décidé qu'un type brut était "équivalent à la substitution" à tout type généré. Cela signifie que bien que la signature de votre méthode ne soit plus formellement égale à la signature de ma superclasse, votre méthode est toujours prioritaire.

Maintenant, le temps passe et vous décidez que vous êtes prêt à mettre à jour votre classe. Mais vous bousiller un peu, et au lieu de modifier la toList()méthode brute existante , vous ajoutez une nouvelle méthode comme celle-ci:

class Overrider extends CollectionConverter {
  @Override
  List toList(Collection c) {...}
  @Override
  <T> List<T> toList(Collection<T> c) {...}
}

En raison de l'équivalence de remplacement des types bruts, les deux méthodes sont sous une forme valide pour remplacer la toList(Collection<T>)méthode. Mais bien sûr, le compilateur doit résoudre une seule méthode. Pour éliminer cette ambiguïté, les classes ne sont pas autorisées à avoir plusieurs méthodes équivalentes à la substitution, c'est-à-dire plusieurs méthodes avec les mêmes types de paramètres après l'effacement.

La clé est qu'il s'agit d'une règle de langage conçue pour maintenir la compatibilité avec l'ancien code à l'aide de types bruts. Ce n'est pas une limitation requise par l'effacement des paramètres de type; étant donné que la résolution de méthode se produit au moment de la compilation, l'ajout de types génériques à l'identificateur de méthode aurait été suffisant.

erickson
la source
3
Excellente réponse et exemple! Je ne suis pas sûr, cependant, si je comprends parfaitement votre dernière phrase ("Parce que la résolution de la méthode se produit au moment de la compilation, avant l'effacement, la réification du type n'est pas nécessaire pour que cela fonctionne."). Pourriez-vous développer un peu?
Jonas Eicher
2
Logique. Je viens de passer du temps à réfléchir à la réification de type dans les méthodes de modèle, mais oui: le compilateur s'assure que la bonne méthode est sélectionnée avant l'effacement du type. Beau. S'il n'était pas entaché par les problèmes de compatibilité du code hérité.
Jonas Eicher
1
@daveloyall Non, je ne connais pas une telle option pour javac.
erickson
13
Ce n'est pas la première fois que je rencontre une erreur Java qui n'est pas du tout une erreur et pourrait être compilée si seuls les auteurs de Java utilisaient des avertissements comme tout le monde. Seulement, ils pensent qu'ils savent tout mieux.
Tomáš Zato - Reinstate Monica
1
@ TomášZato Non, je ne connais aucune solution de contournement propre à cela. Si vous voulez quelque chose de sale, vous pouvez passer le List<?>et certains enumque vous définissez pour indiquer le type. La logique spécifique au type pourrait en fait être dans une méthode sur l'énumération. Alternativement, vous souhaiterez peut-être créer deux classes différentes, plutôt qu'une classe avec deux constructeurs différents. La logique commune serait dans une superclasse ou un objet d'assistance auquel les deux types délèguent.
erickson
118

Les génériques Java utilisent l'effacement des types. Le bit entre les crochets ( <Integer>et <String>) est supprimé, vous vous retrouveriez donc avec deux méthodes qui ont une signature identique (celle que add(Set)vous voyez dans l'erreur). Ce n'est pas autorisé car le runtime ne sait pas lequel utiliser pour chaque cas.

Si Java obtient des génériques réifiés, vous pouvez le faire, mais c'est probablement peu probable maintenant.

GaryF
la source
24
Je suis désolé mais votre réponse (et les autres réponses) n'explique pas pourquoi il y a une erreur ici. La résolution de surcharge se fait au moment de la compilation et le compilateur a sûrement les informations de type nécessaires pour décider de la méthode à lier par adresse ou par tout ce qui est référencé dans le bytecode qui, je crois, n'est pas une signature. Je pense même que certains compilateurs permettront de compiler cela.
Stilgar
5
@Stilgar qu'est-ce qui empêche que la méthode soit appelée ou inspectée par réflexion? La liste des méthodes renvoyées par Class.getMethods () aurait deux méthodes identiques, ce qui n'aurait aucun sens.
Adrian Mouat
5
Les informations de réflexion peuvent / doivent contenir les métadonnées nécessaires pour travailler avec des génériques. Sinon, comment le compilateur Java connaît-il les méthodes génériques lorsque vous importez une bibliothèque déjà compilée?
Stilgar
4
Ensuite, la méthode getMethod doit être corrigée. Par exemple, introduisez une surcharge qui spécifie une surcharge générique et faites en sorte que la méthode d'origine ne renvoie que la version non générique, sans renvoyer aucune méthode anotée comme générique. Bien sûr, cela aurait dû être fait dans la version 1.5. S'ils le font maintenant, ils briseront la compatibilité descendante de la méthode. Je maintiens ma déclaration selon laquelle l'effacement de caractères ne dicte pas ce comportement. C'est la mise en œuvre qui n'a pas obtenu suffisamment de travail, probablement en raison de ressources limitées.
Stilgar
1
Ce n'est pas une réponse précise, mais cela résume rapidement le problème dans une fiction utile: les signatures de méthode sont trop similaires, le compilateur pourrait ne pas faire la différence, et vous obtiendrez alors des "problèmes de compilation non résolus".
worc
46

En effet, Java Generics est implémenté avec Type Erasure .

Vos méthodes seraient traduites, au moment de la compilation, en quelque chose comme:

La résolution de méthode se produit au moment de la compilation et ne prend pas en compte les paramètres de type. ( voir la réponse d'Erickson )

void add(Set ii);
void add(Set ss);

Les deux méthodes ont la même signature sans les paramètres de type, d'où l'erreur.

bruno conde
la source
21

Le problème est que Set<Integer>et Set<String>sont en fait traités comme un Setde la JVM. La sélection d'un type pour l'ensemble (chaîne ou entier dans votre cas) n'est que du sucre syntaxique utilisé par le compilateur. La JVM ne peut pas distinguer entre Set<String>et Set<Integer>.

kgiannakakis
la source
7
Il est vrai que le runtime JVM n'a aucune information pour les distinguer Set, mais comme la résolution de la méthode se produit au moment de la compilation, lorsque les informations nécessaires sont disponibles, cela n'est pas pertinent. Le problème est qu'autoriser ces surcharges entrerait en conflit avec la tolérance pour les types bruts, ils ont donc été rendus illégaux dans la syntaxe Java.
erickson
@erickson Même lorsque le compilateur sait quelle méthode appeler, il ne peut pas, comme dans le bytecode, ils ont tous deux la même apparence. Vous devez modifier la façon dont un appel de méthode est spécifié, car (Ljava/util/Collection;)Ljava/util/List;cela ne fonctionne pas. Vous pouvez utiliser (Ljava/util/Collection<String>;)Ljava/util/List<String>;, mais c'est un changement incompatible et vous rencontreriez des problèmes insolubles dans des endroits où tout ce que vous avez est un type effacé. Vous devrez probablement supprimer complètement l'effacement, mais c'est assez compliqué.
maaartinus
@maaartinus Oui, je suis d'accord que vous devrez changer le spécificateur de méthode. J'essaie de résoudre certains des problèmes insolubles qui les ont amenés à abandonner cette tentative.
erickson
7

Définissez une seule méthode sans type comme void add(Set ii){}

Vous pouvez mentionner le type lors de l'appel de la méthode en fonction de votre choix. Cela fonctionnera pour tout type de jeu.

Idrees Ashraf
la source
3

Il est possible que le compilateur traduise Set (Integer) en Set (Object) en code d'octet java. Si tel est le cas, Set (Integer) ne sera utilisé qu'à la phase de compilation pour la vérification de la syntaxe.

Rossoft
la source
5
C'est techniquement juste le type brut Set. Les génériques n'existent pas dans le code octet, ils sont du sucre syntaxique pour la conversion et offrent une sécurité de type au moment de la compilation.
Andrzej Doyle
1

Je suis tombé sur cela quand j'ai essayé d'écrire quelque chose comme: Continuable<T> callAsync(Callable<T> code) {....} et Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} ils deviennent pour le compilateur les 2 définitions de Continuable<> callAsync(Callable<> veryAsyncCode) {...}

L'effacement de type signifie littéralement l'effacement des informations d'arguments de type des génériques. C'est TRÈS ennuyeux, mais c'est une limitation qui sera avec Java pendant un certain temps. Pour les constructeurs, pas grand chose à faire, 2 nouvelles sous-classes spécialisées avec différents paramètres en constructeur par exemple. Ou utilisez plutôt des méthodes d'initialisation ... (constructeurs virtuels?) Avec des noms différents ...

pour des méthodes de fonctionnement similaires, renommer aiderait, comme

class Test{
   void addIntegers(Set<Integer> ii){}
   void addStrings(Set<String> ss){}
}

Ou avec des noms plus descriptifs, auto-documentés pour les cas oyu, comme addNameset addIndexesou tels.

Kote Isaev
la source