Metoda ma takie samo usunięcie jak inna metoda w typie

381

Dlaczego posiadanie następujących dwóch metod w tej samej klasie jest niezgodne z prawem?

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

Rozumiem compilation error

Metoda add (Set) ma to samo dodanie kasowania (Set), co inna metoda w teście typu.

chociaż mogę sobie z tym poradzić, zastanawiałem się, dlaczego javac tego nie lubi.

Widzę, że w wielu przypadkach logika tych dwóch metod byłaby bardzo podobna i mogłaby zostać zastąpiona jedną

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

metoda, ale nie zawsze tak jest.

Jest to szczególnie denerwujące, jeśli chcesz mieć dwa, constructorsktóre biorą te argumenty, ponieważ wtedy nie możesz po prostu zmienić nazwy jednego z nich constructors.

Omry Yadan
źródło
1
co się stanie, jeśli zabraknie struktur danych i nadal potrzebujesz więcej wersji?
Omry Yadan,
1
Można tworzyć niestandardowe klasy, które dziedziczą po wersjach podstawowych.
willlma,
1
OP, czy wpadłeś na jakieś rozwiązanie problemu z konstruktorem? Muszę zaakceptować dwa rodzaje Listi nie wiem, jak sobie z tym poradzić.
Tomáš Zato - Przywróć Monikę
9
Podczas pracy z Javą naprawdę tęsknię za C # ...
Svish
1
@ TomášZato, rozwiązałem to, dodając do konstruktora fikcyjne parametry: Boolean noopSignatureOverload.
Nthalk,

Odpowiedzi:

357

Ta reguła ma na celu uniknięcie konfliktów w starszym kodzie, który nadal używa typów raw.

Oto przykład, dlaczego nie było to dozwolone, zaczerpnięty z JLS. Załóżmy, że przed wprowadzeniem generics do Javy napisałem taki kod:

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

Rozszerzasz moją klasę w ten sposób:

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

Po wprowadzeniu ogólnych, postanowiłem zaktualizować swoją bibliotekę.

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

Nie jesteś gotowy na żadne aktualizacje, więc zostaw Overriderklasę w spokoju. Aby poprawnie przesłonić toList()metodę, projektanci języków zdecydowali, że typ surowy jest „zastępujący odpowiednik” dla dowolnego generowanego typu. Oznacza to, że chociaż podpis twojej metody nie jest już formalnie równy podpisowi mojej nadklasy, twoja metoda wciąż zastępuje.

Teraz czas mija i decydujesz, że jesteś gotowy, aby zaktualizować swoją klasę. Ale trochę spieprzysz i zamiast edytować istniejącą, surową toList()metodę, dodajesz nową metodę taką:

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

Z powodu zastąpienia równoważności typów surowych obie metody są w prawidłowej formie, aby zastąpić toList(Collection<T>)metodę. Ale oczywiście kompilator musi rozwiązać jedną metodę. Aby wyeliminować tę dwuznaczność, klasy nie mogą mieć wielu metod, które są równoważne z nadpisaniem - to znaczy wielu metod z tymi samymi typami parametrów po skasowaniu.

Kluczem jest to, że jest to reguła językowa zaprojektowana w celu zachowania zgodności ze starym kodem używającym typów raw. Nie jest to ograniczenie wymagane przez usunięcie parametrów typu; ponieważ rozpoznawanie metod zachodzi w czasie kompilacji, dodanie typów ogólnych do identyfikatora metody byłoby wystarczające.

erickson
źródło
3
Świetna odpowiedź i przykład! Nie jestem jednak pewien, czy w pełni rozumiem twoje ostatnie zdanie („Ponieważ rozpoznawanie metod następuje w czasie kompilacji, przed skasowaniem nie jest wymagane ponowne sprawdzenie typu, aby to zadziałało”). Czy mógłbyś trochę rozwinąć?
Jonas Eicher
2
Ma sens. Właśnie spędziłem trochę czasu na zastanawianiu się nad poprawieniem typu w metodach szablonów, ale tak: kompilator upewnia się, że wybrana jest właściwa metoda przed usunięciem typu. Piękny. Jeśli nie zostały skażone starszymi problemami ze zgodnością kodu.
Jonas Eicher,
1
@daveloyall Nie, nie znam takiej opcji dla javac.
erickson,
13
Nie po raz pierwszy napotykam błąd Java, który w ogóle nie jest błędem i mógłby zostać skompilowany, gdyby tylko autorzy Java używali ostrzeżeń tak jak wszyscy inni. Tylko oni myślą, że wiedzą wszystko lepiej.
Tomáš Zato - Przywróć Monikę
1
@ TomášZato Nie, nie znam na to żadnych czystych obejść. Jeśli chcesz czegoś brudnego, możesz przekazać List<?>i niektóre enum, które zdefiniujesz, aby wskazać typ. Logika specyficzna dla typu mogłaby faktycznie być metodą w wyliczeniu. Alternatywnie, możesz chcieć utworzyć dwie różne klasy, zamiast jednej klasy z dwoma różnymi konstruktorami. Wspólna logika byłaby w nadklasie lub obiekcie pomocniczym, któremu delegują oba typy.
erickson,
117

Generics Java używa usuwania typu. Bit w nawiasach kątowych ( <Integer>i <String>) zostaje usunięty, więc otrzymujesz dwie metody o identycznej sygnaturze ( add(Set)widoczne w błędzie). Jest to niedozwolone, ponieważ środowisko wykonawcze nie wiedziałoby, którego użyć dla każdej sprawy.

Jeśli Java kiedykolwiek zostanie zweryfikowana, możesz to zrobić, ale prawdopodobnie teraz jest to mało prawdopodobne.

GaryF
źródło
24
Przykro mi, ale twoja odpowiedź (i inne odpowiedzi) nie wyjaśniają, dlaczego wystąpił tutaj błąd. Rozwiązanie problemu przeciążenia odbywa się w czasie kompilacji, a kompilator z pewnością ma informacje o typie potrzebne do podjęcia decyzji, którą metodę połączyć przez adres lub jakąkolwiek metodę, do której odwołuje się kod bajtowy, który moim zdaniem nie jest sygnaturą. Myślę nawet, że niektóre kompilatory pozwolą na kompilację.
Stilgar
5
@Stilgar, co ma powstrzymać wywoływanie lub sprawdzanie metody poprzez odbicie? Lista metod zwróconych przez Class.getMethods () miałaby dwie identyczne metody, co nie miałoby sensu.
Adrian Mouat,
5
Informacje o odbiciu mogą / powinny zawierać metadane potrzebne do pracy z ogólnymi. Jeśli nie, to skąd kompilator Java wie o metodach ogólnych podczas importowania już skompilowanej biblioteki?
Stilgar,
4
Następnie metoda getMethod wymaga naprawy. Na przykład wprowadź przeciążenie, które określa ogólne przeciążenie i spraw, aby oryginalna metoda zwróciła tylko wersję inną niż ogólna, nie zwracając żadnej metody oznaczonej jako ogólna. Oczywiście należy to zrobić w wersji 1.5. Jeśli zrobią to teraz, złamią wsteczną zgodność metody. Podtrzymuję moje stwierdzenie, że wymazanie typu nie dyktuje tego zachowania. Jest to implementacja, która nie dostała wystarczającej ilości pracy prawdopodobnie z powodu ograniczonych zasobów.
Stilgar,
1
To nie jest dokładna odpowiedź, ale szybko podsumowuje problem w użytecznej fikcji: sygnatury metod są zbyt podobne, kompilator może nie odróżnić, a wtedy dostaniesz „nierozwiązane problemy z kompilacją”.
worc,
46

Wynika to z faktu, że Java Generics są implementowane za pomocą Type Erasure .

Twoje metody zostaną przetłumaczone w czasie kompilacji na coś takiego:

Rozdzielczość metody występuje w czasie kompilacji i nie uwzględnia parametrów typu. ( patrz odpowiedź Ericksona )

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

Obie metody mają tę samą sygnaturę bez parametrów typu, stąd błąd.

Bruno Conde
źródło
21

Problemem jest to, że Set<Integer>i Set<String>są właściwie traktowane jako Setz JVM. Wybranie typu dla zestawu (w twoim przypadku łańcuch lub liczba całkowita) to tylko cukier syntaktyczny używany przez kompilator. JVM nie może rozróżnić między Set<String>i Set<Integer>.

kgiannakakis
źródło
7
Prawdą jest, że środowisko wykonawcze JVM nie ma informacji umożliwiających ich rozróżnienie Set, ale ponieważ rozpoznawanie metod odbywa się w czasie kompilacji, gdy dostępne są niezbędne informacje, nie ma to znaczenia. Problem polega na tym, że zezwolenie na te przeciążenia powodowałoby konflikt z tolerancją dla typów surowych, dlatego zostały one uznane za nielegalne w składni Java.
erickson,
@erickson Nawet jeśli kompilator wie, jaką metodę wywołać, nie może tak jak w kodzie bajtowym, oba wyglądają dokładnie tak samo. Musisz zmienić sposób określania wywołania metody, ponieważ (Ljava/util/Collection;)Ljava/util/List;nie działa. Możesz użyć (Ljava/util/Collection<String>;)Ljava/util/List<String>;, ale jest to niezgodna zmiana i napotkasz nierozwiązywalne problemy w miejscach, w których wszystko, co masz, to wymazany typ. Prawdopodobnie musiałbyś całkowicie usunąć wymazanie, ale to dość skomplikowane.
maaartinus
@maaartinus Tak, zgadzam się, że będziesz musiał zmienić specyfikator metody. Próbuję rozwiązać niektóre z nierozwiązywalnych problemów, które skłoniły ich do rezygnacji z tej próby.
erickson,
7

Zdefiniuj jedną metodę bez typu typu void add(Set ii){}

Możesz podać typ podczas wywoływania metody na podstawie swojego wyboru. Będzie działał dla każdego rodzaju zestawu.

Idrees Ashraf
źródło
3

Możliwe, że kompilator tłumaczy Set (Integer) na Set (Object) w kodzie bajtowym Java. W takim przypadku Set (liczba całkowita) byłby używany tylko w fazie kompilacji do sprawdzania składni.

Rossoft
źródło
5
Technicznie jest to po prostu surowy typ Set. Generics nie istnieją w kodzie bajtów, są cukrem syntaktycznym do rzucania i zapewniają bezpieczeństwo typu kompilacji.
Andrzej Doyle
1

Natknąłem się na to, gdy próbowałem napisać coś takiego: Continuable<T> callAsync(Callable<T> code) {....} i Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} stały się dla kompilatora 2 definicjami Continuable<> callAsync(Callable<> veryAsyncCode) {...}

Kasowanie typu dosłownie oznacza kasowanie informacji o argumentach typu z nazw ogólnych. Jest to BARDZO denerwujące, ale jest to ograniczenie, które będzie obowiązywać przez pewien czas w Javie. W przypadku konstruktorów niewiele można zrobić, na przykład 2 nowe podklasy specjalizujące się w różnych parametrach w konstruktorze. Lub zamiast tego użyj metod inicjowania ... (wirtualnych konstruktorów?) O różnych nazwach ...

w przypadku podobnych metod operacji pomocna byłaby zmiana nazwy, np

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

Lub z kilkoma bardziej opisowymi nazwami, samodokumentującymi dla przypadków oyu, takich jak addNamesi addIndexestakich.

Kote Isaev
źródło