Die Methode hat dieselbe Löschung wie eine andere Methode

381

Warum ist es nicht legal, die folgenden zwei Methoden in derselben Klasse zu haben?

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

Ich bekomme die compilation error

Die Methode add (Set) hat dieselbe Löschaddition (Set) wie eine andere Methode im Typ Test.

Während ich es umgehen kann, habe ich mich gefragt, warum Javac das nicht mag.

Ich kann sehen, dass in vielen Fällen die Logik dieser beiden Methoden sehr ähnlich wäre und durch eine einzige ersetzt werden könnte

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

Methode, aber das ist nicht immer der Fall.

Dies ist besonders ärgerlich, wenn Sie zwei haben möchten constructors, die diese Argumente verwenden, da Sie dann nicht einfach den Namen eines der ändern können constructors.

Omry Yadan
quelle
1
Was ist, wenn Ihnen die Datenstrukturen ausgehen und Sie noch mehr Versionen benötigen?
Omry Yadan
1
Sie können benutzerdefinierte Klassen erstellen, die von Basisversionen erben.
Willlma
1
OP, haben Sie eine Lösung für das Konstruktorproblem gefunden? Ich muss zwei Arten akzeptieren Listund weiß nicht, wie ich damit umgehen soll.
Tomáš Zato - Wiedereinsetzung Monica
9
Wenn ich mit Java arbeite, vermisse ich C # ...
Svish
1
@ TomášZato, ich habe das gelöst, indem ich dem Konstruktor Dummy-Parameter hinzugefügt habe: Boolean noopSignatureOverload.
Nthalk

Antworten:

357

Diese Regel soll Konflikte in Legacy-Code vermeiden, der weiterhin Rohtypen verwendet.

Hier ist ein Beispiel dafür, warum dies nicht erlaubt war , aus dem JLS. Angenommen, bevor Generika in Java eingeführt wurden, habe ich folgenden Code geschrieben:

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

Sie erweitern meine Klasse wie folgt:

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

Nach der Einführung von Generika habe ich beschlossen, meine Bibliothek zu aktualisieren.

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

Sie sind nicht bereit, Aktualisierungen vorzunehmen, und lassen Ihre OverriderKlasse in Ruhe. Um das richtig zu überschreibentoList() Methode , entschieden die Sprachdesigner, dass ein Rohtyp einem generierten Typ "überschreibungsäquivalent" ist. Dies bedeutet, dass Ihre Methodensignatur formal nicht mehr der Signatur meiner Oberklasse entspricht, Ihre Methode jedoch weiterhin überschreibt.

Jetzt vergeht die Zeit und Sie entscheiden, dass Sie bereit sind, Ihre Klasse zu aktualisieren. Aber Sie vermasseln ein wenig und anstatt die vorhandene Rohmethode zu bearbeiten toList(), fügen Sie eine neue Methode wie die folgende hinzu:

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

Aufgrund der Überschreibungsäquivalenz von Rohtypen liegen beide Methoden in einer gültigen Form vor, um die toList(Collection<T>)Methode zu überschreiben . Aber natürlich muss der Compiler eine einzelne Methode auflösen. Um diese Mehrdeutigkeit zu beseitigen, dürfen Klassen nicht mehrere Methoden haben, die überschreibungsäquivalent sind, dh mehrere Methoden mit denselben Parametertypen nach dem Löschen.

Der Schlüssel ist, dass dies eine Sprachregel ist, die entwickelt wurde, um die Kompatibilität mit altem Code unter Verwendung von Rohtypen aufrechtzuerhalten. Es ist keine Einschränkung, die durch das Löschen von Typparametern erforderlich ist. Da die Methodenauflösung zur Kompilierungszeit erfolgt, wäre das Hinzufügen generischer Typen zur Methodenkennung ausreichend gewesen.

erickson
quelle
3
Tolle Antwort und Beispiel! Ich bin mir jedoch nicht sicher, ob ich Ihren letzten Satz vollständig verstehe ("Da die Methodenauflösung zur Kompilierungszeit vor dem Löschen erfolgt, ist keine erneute Typisierung erforderlich, damit dies funktioniert."). Könnten Sie etwas näher darauf eingehen?
Jonas Eicher
2
Macht Sinn. Ich habe gerade einige Zeit damit verbracht, über die Typwiederherstellung in Vorlagenmethoden nachzudenken, aber ja: Der Compiler stellt sicher, dass die richtige Methode ausgewählt wird, bevor der Typ gelöscht wird. Wunderschönen. Wenn es nicht durch die Probleme mit der Legacy-Code-Kompatibilität beeinträchtigt wurde.
Jonas Eicher
1
@daveloyall Nein, mir ist eine solche Option für nicht bekannt javac.
Erickson
13
Nicht das erste Mal stoße ich auf einen Java-Fehler, der überhaupt kein Fehler ist und kompiliert werden könnte, wenn nur die Autoren von Java wie alle anderen Warnungen verwenden würden. Nur sie denken, sie wissen alles besser.
Tomáš Zato - Wiedereinsetzung Monica
1
@ TomášZato Nein, ich kenne keine sauberen Workarounds dafür. Wenn Sie etwas schmutziges wollen, können Sie das List<?>und einige enum, die Sie definieren, übergeben, um den Typ anzugeben. Die typspezifische Logik könnte sich tatsächlich in einer Methode in der Aufzählung befinden. Alternativ möchten Sie möglicherweise zwei verschiedene Klassen anstelle einer Klasse mit zwei verschiedenen Konstruktoren erstellen. Gemeinsame Logik wäre in einer Oberklasse oder einem Hilfsobjekt, an das beide Typen delegieren.
Erickson
118

Java-Generika verwenden Typlöschung. Das Bit in den spitzen Klammern ( <Integer>und <String>) wird entfernt, sodass Sie zwei Methoden mit identischer Signatur erhalten (die add(Set)Sie im Fehler sehen). Dies ist nicht zulässig, da die Laufzeit nicht weiß, welche für jeden Fall verwendet werden soll.

Wenn Java jemals generische Generika erhält, können Sie dies tun, aber das ist jetzt wahrscheinlich unwahrscheinlich.

GaryF
quelle
24
Es tut mir leid, aber Ihre Antwort (und die anderen Antworten) erklären nicht, warum hier ein Fehler vorliegt. Die Überlastungsauflösung erfolgt zur Kompilierungszeit, und der Compiler verfügt mit Sicherheit über die Typinformationen, die erforderlich sind, um zu entscheiden, welche Methode nach Adresse oder nach welcher Methode auch immer im Bytecode referenziert werden soll, von dem ich glaube, dass es keine Signatur ist. Ich denke sogar, dass einige Compiler dies zulassen werden.
Stilgar
5
@Stilgar Was soll verhindern, dass die Methode durch Reflektion aufgerufen oder überprüft wird? Die Liste der von Class.getMethods () zurückgegebenen Methoden hätte zwei identische Methoden, was keinen Sinn ergeben würde.
Adrian Mouat
5
Die Reflexionsinformationen können / sollten die Metadaten enthalten, die für die Arbeit mit Generika erforderlich sind. Wenn nicht, woher kennt der Java-Compiler generische Methoden, wenn Sie bereits kompilierte Bibliotheken importieren?
Stilgar
4
Dann muss die getMethod-Methode korrigiert werden. Führen Sie beispielsweise eine Überladung ein, die eine generische Überladung angibt, und lassen Sie die ursprüngliche Methode nur eine nicht generische Version zurückgeben, ohne eine als generisch bezeichnete Methode zurückzugeben. Dies sollte natürlich in Version 1.5 geschehen sein. Wenn sie es jetzt tun, brechen sie die Abwärtskompatibilität der Methode. Ich stehe zu meiner Aussage, dass das Löschen von Typen dieses Verhalten nicht vorschreibt. Es ist die Implementierung, die wahrscheinlich aufgrund begrenzter Ressourcen nicht genügend Arbeit erhalten hat.
Stilgar
1
Dies ist keine genaue Antwort, aber sie fasst das Problem schnell in einer nützlichen Fiktion zusammen: Die Methodensignaturen sind zu ähnlich, der Compiler erkennt möglicherweise keinen Unterschied, und dann erhalten Sie "ungelöste Kompilierungsprobleme".
Worc
46

Dies liegt daran, dass Java-Generika mit Type Erasure implementiert werden .

Ihre Methoden würden zur Kompilierungszeit in Folgendes übersetzt:

Die Methodenauflösung erfolgt zur Kompilierungszeit und berücksichtigt keine Typparameter. ( siehe ericksons antwort )

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

Beide Methoden haben dieselbe Signatur ohne die Typparameter, daher der Fehler.

bruno conde
quelle
21

Das Problem ist, dass Set<Integer>und Set<String>tatsächlich als Setvon der JVM behandelt werden. Die Auswahl eines Typs für die Menge (in Ihrem Fall String oder Integer) ist nur syntaktischer Zucker, der vom Compiler verwendet wird. Die JVM kann nicht zwischen Set<String>und unterscheiden Set<Integer>.

kgiannakakis
quelle
7
Es ist richtig, dass die JVM- Laufzeit keine Informationen zur Unterscheidung enthält. SetDa die Methodenauflösung jedoch zur Kompilierungszeit erfolgt und die erforderlichen Informationen verfügbar sind, ist dies nicht relevant. Das Problem besteht darin, dass das Zulassen dieser Überladungen mit der Berücksichtigung von Rohtypen in Konflikt steht, sodass sie in der Java-Syntax unzulässig gemacht wurden.
Erickson
@erickson Auch wenn der Compiler weiß, welche Methode er aufrufen soll, kann es nicht wie im Bytecode sein, beide sehen genau gleich aus. Sie müssen ändern, wie ein Methodenaufruf angegeben wird, da (Ljava/util/Collection;)Ljava/util/List;dies nicht funktioniert. Sie könnten verwenden (Ljava/util/Collection<String>;)Ljava/util/List<String>;, aber das ist eine inkompatible Änderung, und Sie würden an Stellen, an denen Sie nur einen gelöschten Typ haben, auf unlösbare Probleme stoßen. Sie müssten die Löschung wahrscheinlich vollständig fallen lassen, aber das ist ziemlich kompliziert.
Maaartinus
@maaartinus Ja, ich bin damit einverstanden, dass Sie den Methodenspezifizierer ändern müssen. Ich versuche, einige der unlösbaren Probleme zu lösen, die dazu geführt haben, dass sie diesen Versuch aufgegeben haben.
Erickson
7

Definieren Sie eine einzelne Methode ohne Typ wie void add(Set ii){}

Sie können den Typ beim Aufrufen der Methode basierend auf Ihrer Wahl angeben. Es funktioniert für jede Art von Set.

Idrees Ashraf
quelle
3

Möglicherweise übersetzt der Compiler Set (Integer) in Set (Object) in Java-Bytecode. In diesem Fall wird Set (Integer) nur in der Kompilierungsphase für die Syntaxprüfung verwendet.

Rossoft
quelle
5
Es ist technisch nur der rohe Typ Set. Generika sind im Bytecode nicht vorhanden, sie sind syntaktischer Zucker für das Casting und bieten Sicherheit beim Kompilieren.
Andrzej Doyle
1

Ich bin darauf gestoßen, als ich versucht habe, etwas zu schreiben wie: Continuable<T> callAsync(Callable<T> code) {....} und Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} sie werden für den Compiler die 2 Definitionen von Continuable<> callAsync(Callable<> veryAsyncCode) {...}

Das Löschen von Typen bedeutet wörtlich das Löschen von Informationen zu Typargumenten aus Generika. Dies ist SEHR ärgerlich, aber dies ist eine Einschränkung, die für eine Weile bei Java bestehen bleibt. Für Konstruktoren kann nicht viel getan werden, zum Beispiel 2 neue Unterklassen, die auf unterschiedliche Parameter im Konstruktor spezialisiert sind. Oder verwenden Sie stattdessen Initialisierungsmethoden ... (virtuelle Konstruktoren?) Mit unterschiedlichen Namen ...

für ähnliche Betriebsmethoden würde das Umbenennen helfen, wie

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

Oder mit einigen aussagekräftigeren Namen, die sich für Oyu-Fälle wie addNamesund addIndexesoder so selbst dokumentieren .

Kote Isaev
quelle