Dlaczego niektórzy twierdzą, że implementacja typów generycznych w Javie jest zła?

115

Czasami słyszałem, że w przypadku leków generycznych Java nie radziła sobie dobrze. (najbliższa referencja, tutaj )

Przepraszam za mój brak doświadczenia, ale co by je polepszyło?

sdellysse
źródło
23
Nie widzę powodu, aby to zamykać; z całym tym bzdurnym tematem generycznym, wyjaśnienie różnic między Javą a C # może pomóc ludziom próbującym uniknąć niedoinformowanego błota.
Rob

Odpowiedzi:

146

Zły:

  • Informacje o typie są tracone w czasie kompilacji, więc w czasie wykonywania nie można określić, jakiego typu ma to być
  • Nie można go używać dla typów wartości (jest to duży problem - w .NET List<byte>naprawdę jest wspierany przez byte[]na przykład i nie jest wymagany boks)
  • Składnia wywoływania metod ogólnych jest do bani (IMO)
  • Składnia ograniczeń może być myląca
  • Stosowanie symboli wieloznacznych jest ogólnie mylące
  • Różne ograniczenia wynikające z powyższego - odlewanie itp

Dobry:

  • Dzikie karty umożliwiają określenie kowariancji / kontrawariancji po stronie dzwoniącej, co jest bardzo przydatne w wielu sytuacjach
  • To jest lepsze niż nic!
Jon Skeet
źródło
51
@Paul: Myślę, że wolałbym tymczasowe uszkodzenie, ale potem przyzwoity interfejs API. Lub wybierz trasę .NET i wprowadź nowe typy kolekcji. (.NET generics w wersji 2.0 nie zepsuło aplikacji 1.1.)
Jon Skeet
6
Mając dużą istniejącą bazę kodu, podoba mi się fakt, że mogę zacząć zmieniać sygnaturę jednej metody na używanie typów generycznych bez zmiany każdego, kto ją wywołuje.
Paul Tomblin
38
Ale wady tego są ogromne i trwałe. Jest duża krótkoterminowa wygrana i długoterminowa strata IMO.
Jon Skeet
11
Jon - „Lepiej niż nic” jest subiektywne :)
MetroidFan2002
6
@Rogerio: Twierdziłeś, że mój pierwszy „zły” przedmiot jest nieprawidłowy. Moja pierwsza pozycja „Zła” mówi, że informacje są tracone w czasie kompilacji. Aby to było niepoprawne, musisz mówić, że informacje nie są tracone podczas kompilacji ... Jeśli chodzi o dezorientację innych programistów, wydaje się, że jesteś jedyną osobą zdezorientowaną tym, co mówię.
Jon Skeet
26

Największym problemem jest to, że generyczne programy Java są dostępne tylko w czasie kompilacji i można to zmienić w czasie wykonywania. C # jest chwalony, ponieważ wykonuje więcej sprawdzania w czasie wykonywania. W tym poście znajduje się naprawdę dobra dyskusja , która zawiera linki do innych dyskusji.

Paul Tomblin
źródło
To naprawdę nie jest problem, nigdy nie został stworzony do działania. To tak, jakbyś powiedział, że łodzie są problemem, ponieważ nie mogą wspinać się po górach. Jako język Java robi to, czego potrzebuje wiele osób: wyraża typy w znaczący sposób. W przypadku informacji typu runtime ludzie nadal mogą przekazywać Classobiekty.
Vincent Cantin
1
On ma rację. twoja anologia jest zła, ponieważ ludzie projektujący łódź POWINNI zaprojektować ją do wspinaczki po górach. Ich cele projektowe nie były zbyt dobrze przemyślane pod kątem potrzeb. Teraz wszyscy utknęliśmy tylko w łodzi i nie możemy wspinać się po górach.
WiegleyJ
1
@VincentCantin - to z pewnością problem. Dlatego wszyscy na to narzekamy. Generyczne programy Java są niedopieczone.
Josh M.
17

Głównym problemem jest to, że Java w rzeczywistości nie ma generycznych w czasie wykonywania. Jest to funkcja czasu kompilacji.

Kiedy tworzysz klasę ogólną w Javie, używają metody o nazwie „Type Erasure”, aby faktycznie usunąć wszystkie typy ogólne z klasy i zasadniczo zastąpić je Object. Wersja typów generycznych o długości mili jest taka, że ​​kompilator po prostu wstawia rzutowania do określonego typu ogólnego, gdy pojawia się on w treści metody.

Ma to wiele wad. Jednym z największych, IMHO, jest to, że nie można użyć odbicia do sprawdzenia typu ogólnego. Typy nie są w rzeczywistości generyczne w kodzie bajtowym i dlatego nie mogą być sprawdzane jako ogólne.

Świetny przegląd różnic tutaj: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
źródło
2
Nie jestem pewien, dlaczego zostałeś przegłosowany. Z tego, co rozumiem, masz rację, a ten link jest bardzo pouczający. Głosowano za.
Jason Jackson
@Jason, dzięki. W mojej oryginalnej wersji była literówka, myślę, że zostałem za to odrzucony.
JaredPar
3
Błąd, ponieważ można użyć odbicia, aby spojrzeć na typ ogólny, sygnaturę metody ogólnej. Po prostu nie możesz użyć odbicia do zbadania ogólnych informacji powiązanych ze sparametryzowaną instancją. Na przykład, jeśli mam klasę z metodą foo (List <String>), mogę w sposób refleksyjny znaleźć „String”
oxbow_lakes,
Istnieje wiele artykułów, które mówią, że wymazywanie typu uniemożliwia znalezienie rzeczywistych parametrów typu. Powiedzmy: Obiekt x = nowy X [Y] (); czy możesz pokazać, jak, mając x, znaleźć typ Y?
user48956
@oxbow_lakes - konieczność korzystania z odbicia w celu znalezienia typu ogólnego jest prawie tak samo zła, jak niemożność zrobienia tego. Np. To złe rozwiązanie.
Josh M.
14
  1. Implementacja w czasie wykonywania (tj. Bez usuwania typu);
  2. Umiejętność używania typów pierwotnych (jest to związane z (1));
  3. Chociaż wieloznaczność jest przydatna, składnia i wiedza, kiedy jej użyć, jest czymś, co przytłacza wiele osób. i
  4. Brak poprawy wydajności (z powodu (1); typy generyczne Java są cukrem składniowym dla obiektów castingi).

(1) prowadzi do bardzo dziwnego zachowania. Najlepszy przykład, jaki przychodzi mi do głowy, to. Założyć:

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

następnie zadeklaruj dwie zmienne:

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

Teraz zadzwoń getOtherStuff():

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

Drugi ma swój argument typu ogólnego usunięty przez kompilator, ponieważ jest to typ surowy (co oznacza, że ​​typ sparametryzowany nie jest dostarczany), mimo że nie ma nic wspólnego z typem sparametryzowanym.

Wspomnę też o mojej ulubionej deklaracji z JDK:

public class Enum<T extends Enum<T>>

Oprócz symboli wieloznacznych (które są mieszane) po prostu myślę, że generyczne .Net są lepsze.

cletus
źródło
Ale kompilator powie ci, szczególnie z opcją rawtypes lint.
Tom Hawtin - tackline
Przepraszam, dlaczego ostatnia linia jest błędem kompilacji? Bawię się tym w Eclipse i nie mogę sprawić, żeby tam zawiodło - jeśli dodam wystarczająco dużo rzeczy, aby reszta mogła się skompilować.
oconnor0
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 To nie jest błąd kompilacji, ale ostrzeżenie kompilatora, bez wyraźnego powodu:The expression of type List needs unchecked conversion to conform to List<String>
artbristol
Enum<T extends Enum<T>>na początku może wydawać się dziwne / zbędne, ale w rzeczywistości jest całkiem interesujące, przynajmniej w ramach ograniczeń Javy / jej rodzajów. Wyliczenia mają values()metodę statyczną, która daje tablicę ich elementów wpisanych jako wyliczenie, a nie Enum, a ten typ jest określany przez parametr ogólny, co oznacza, że ​​chcesz Enum<T>. Oczywiście to wpisywanie ma sens tylko w kontekście wyliczonego typu, a wszystkie wyliczenia są podklasami Enum, więc chcesz Enum<T extends Enum>. Jednak Java nie lubi mieszać typów surowych z rodzajami, co Enum<T extends Enum<T>>zapewnia spójność.
JAB
10

Wyrzucę naprawdę kontrowersyjną opinię. Generics komplikują język i komplikują kod. Na przykład, powiedzmy, że mam mapę, która mapuje ciąg na listę ciągów. W dawnych czasach mógłbym to po prostu określić jako

Map someMap;

Teraz muszę to zadeklarować jako

Map<String, List<String>> someMap;

I za każdym razem, gdy przekazuję to do jakiejś metody, muszę od nowa powtarzać tę wielką, długą deklarację. Moim zdaniem całe to dodatkowe wpisywanie odwraca uwagę programisty i wyprowadza go ze „strefy”. Ponadto, gdy kod jest wypełniony dużą ilością cruft, czasami trudno jest do niego wrócić później i szybko przejrzeć wszystkie okrucieństwa, aby znaleźć ważną logikę.

Java ma już złą reputację jako jeden z najbardziej rozwlekłych języków w powszechnym użyciu, a rodzaje generyczne tylko dodają do tego problemu.

A co tak naprawdę kupujesz za tę dodatkową gadatliwość? Ile razy naprawdę miałeś problemy, gdy ktoś umieścił liczbę całkowitą w kolekcji, która powinna zawierać ciągi, lub gdy ktoś próbował wyciągnąć ciąg znaków ze zbioru liczb całkowitych? W moim 10-letnim doświadczeniu w tworzeniu komercyjnych aplikacji Java nigdy nie było to dużym źródłem błędów. Nie jestem więc pewien, co otrzymujesz za dodatkową szczegółowość. Wydaje mi się, że to dodatkowy bagaż biurokratyczny.

Teraz będę naprawdę kontrowersyjny. To, co uważam za największy problem z kolekcjami w Javie 1.4, to konieczność pisania wszędzie. Postrzegam te typy jako dodatkowe, rozwlekłe okrucieństwo, które ma wiele tych samych problemów, co generyczne. Na przykład nie mogę tak po prostu zrobić

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

muszę zrobić

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

Powodem jest oczywiście to, że get () zwraca obiekt będący nadtypem listy. Dlatego przypisanie nie może zostać wykonane bez rzutowania. Ponownie zastanów się, ile naprawdę kupuje ta reguła. Z mojego doświadczenia niewiele.

Myślę, że Java byłaby znacznie lepsza, gdyby 1) nie dodała typów ogólnych, ale 2) zamiast tego umożliwiła niejawne rzutowanie z nadtypu na podtyp. Pozwól na wychwycenie nieprawidłowych rzutów w czasie wykonywania Wtedy mógłbym mieć prostotę definiowania

Map someMap;

a później robię

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

całe okrucieństwo by zniknęło i naprawdę nie sądzę, żebym wprowadzał nowe duże źródło błędów do mojego kodu.

Clint Miller
źródło
15
Przepraszam, nie zgadzam się ze wszystkim, co powiedziałeś. Ale nie zamierzam głosować, ponieważ dobrze się z tym argumentujesz.
Paul Tomblin,
2
Oczywiście istnieje wiele przykładów dużych, udanych systemów zbudowanych w językach takich jak Python i Ruby, które robią dokładnie to, co zasugerowałem w mojej odpowiedzi.
Clint Miller,
Myślę, że cały mój sprzeciw wobec tego typu pomysłów nie polega na tym, że jest to zły pomysł per se, ale raczej to, że ponieważ współczesne języki, takie jak Python i Ruby, ułatwiają życie programistom, programiści z kolei stają się intelektualnie zadowoleni i ostatecznie mają niższy zrozumienie własnego kodu.
Dan Tao
4
„A co tak naprawdę kupujesz za tę dodatkową gadatliwość?…” Mówi biednemu programistowi, co powinno znajdować się w tych kolekcjach. Zauważyłem, że jest to problem podczas pracy z komercyjnymi aplikacjami Java.
richj
4
„Ile razy naprawdę miałeś problemy, kiedy ktoś umieścił [x] w kolekcji, która powinna zawierać [y] s?” - O rany, straciłem rachubę! Nawet jeśli nie ma błędu, zabija czytelność. Skanowanie wielu plików w celu ustalenia, jaki będzie obiekt (lub debugowanie), naprawdę wyciąga mnie ze strefy. Możesz polubić Haskell - nawet mocne pisanie, ale mniej okrucieństwa (ponieważ typy są wywnioskowane).
Martin Capodici
8

Innym efektem ubocznym ich kompilacji, a nie czasu wykonywania, jest to, że nie można wywołać konstruktora typu ogólnego. Więc nie możesz ich użyć do wdrożenia generycznej fabryki ...


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

--jeffk ++

jdkoftinoff
źródło
6

Typy generyczne Java są sprawdzane pod kątem poprawności w czasie kompilacji, a następnie wszystkie informacje o typie są usuwane (proces ten nazywa się wymazywaniem typu . W ten sposób rodzaj ogólny List<Integer>zostanie zredukowany do jego surowego typu , nieogólnego List, który może zawierać obiekty dowolnej klasy.

Powoduje to możliwość wstawiania dowolnych obiektów do listy w czasie wykonywania, a także nie można teraz stwierdzić, jakie typy były używane jako parametry ogólne. To z kolei skutkuje

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
źródło
5

Chciałbym, żeby to była wiki, żebym mógł dodać ją innym ludziom ... ale ...

Problemy:

  • Typ Erasure (brak dostępności środowiska wykonawczego)
  • Brak obsługi typów pierwotnych
  • Niezgodność z adnotacjami (oba zostały dodane w wersji 1.5 Nadal nie jestem pewien, dlaczego adnotacje nie pozwalają na generyczne oprócz przyspieszenia funkcji)
  • Niezgodność z tablicami. (Czasami naprawdę chcę zrobić coś takiego jak Class <? Rozszerza MyObject >[], ale nie mogę)
  • Dziwna składnia i zachowanie symboli wieloznacznych
  • Fakt, że ogólne wsparcie jest niespójne między klasami Java. Dodali go do większości metod kolekcji, ale od czasu do czasu napotykasz instancję, w której jej nie ma.
Laplie Anderson
źródło
5

Ignorując cały bałagan związany z wymazywaniem typów, określone typy generyczne po prostu nie działają.

To kompiluje:

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

Ale to jest błąd składniowy:

foo(Collections.emptyList());

Gdzie foo jest zdefiniowane jako:

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

Zatem to, czy typ wyrażenia sprawdza, zależy od tego, czy jest przypisywany do zmiennej lokalnej, czy do rzeczywistego parametru wywołania metody. Jak szalone to jest?

Nat
źródło
3
Niespójne wnioskowanie javaca to bzdura.
mP.
3
Przypuszczam, że powodem odrzucenia tego drugiego formularza może być fakt, że może istnieć więcej niż jedna wersja „foo” z powodu przeciążenia metody.
Jason Creighton
2
ta krytyka nie ma już zastosowania, ponieważ Java 8 wprowadziła ulepszone wnioskowanie o typie celu
oldrinb
4

Wprowadzenie typów ogólnych do języka Java było trudnym zadaniem, ponieważ architekci starali się zrównoważyć funkcjonalność, łatwość użycia i zgodność wsteczną ze starszym kodem. Dość oczekiwano, że trzeba było pójść na kompromisy.

Są też tacy, którzy uważają, że implementacja typów generycznych w Javie zwiększyła złożoność języka do niedopuszczalnego poziomu (patrz „ Generics Consented Harmful ” Kena Arnolda ). Często zadawane pytania dotyczące generycznych Angeliki Langer dają całkiem dobry pogląd na to, jak skomplikowane mogą się stać.

Zach Scrivena
źródło
3

Java nie wymusza typów ogólnych w czasie wykonywania, tylko w czasie kompilacji.

Oznacza to, że możesz robić interesujące rzeczy, takie jak dodawanie niewłaściwych typów do ogólnych kolekcji.

Władca
źródło
1
Kompilator powie ci, że schrzaniłeś sprawę, jeśli ci się to uda.
Tom Hawtin - tackline
@Tom - niekoniecznie. Istnieją sposoby na oszukanie kompilatora.
Paul Tomblin
2
Nie słuchasz tego, co mówi Ci kompilator? (zobacz mój pierwszy komentarz)
Tom Hawtin - tackline
1
@Tom, jeśli masz ArrayList <String> i przekazujesz ją do metody starego stylu (powiedzmy, starszego lub kodu innej firmy), która pobiera prostą Listę, ta metoda może następnie dodać wszystko, co lubi, do tej listy. Jeśli zostanie wymuszony w czasie wykonywania, pojawi się wyjątek.
Paul Tomblin
1
static void addToList (lista list) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (lista); To kompiluje się, a nawet działa w czasie wykonywania. Problem występuje tylko wtedy, gdy usuwasz z listy oczekując na łańcuch i otrzymujesz int.
Laplie Anderson
3

Typy Java są kompilowane tylko w czasie kompilacji i są kompilowane do kodu innego niż rodzajowy. W języku C # rzeczywisty skompilowany plik MSIL jest ogólny. Ma to ogromny wpływ na wydajność, ponieważ Java nadal przesyła dane w czasie wykonywania. Więcej tutaj .

Szymon Rozga
źródło
2

Jeśli posłuchasz Java Posse # 279 - Interview with Joe Darcy i Alex Buckley , rozmawiają o tym problemie. To również prowadzi do posta na blogu Neal Gafter zatytułowanego Reified Generics for Java, który mówi:

Wiele osób jest niezadowolonych z ograniczeń wynikających ze sposobu implementacji typów generycznych w Javie. W szczególności są niezadowoleni, że parametry typu ogólnego nie są reifikowane: nie są dostępne w czasie wykonywania. Typy ogólne są implementowane przy użyciu wymazywania, w którym parametry typu ogólnego są po prostu usuwane w czasie wykonywania.

Ten wpis na blogu odnosi się do starszego wpisu, Puzzling Through Erasure: answer , w którym podkreślono kwestię zgodności migracji w wymaganiach.

Celem było zapewnienie kompatybilności wstecznej zarówno kodu źródłowego, jak i wynikowego, a także kompatybilności migracji.

Kevin Hakanson
źródło