Co jest nie tak z rodzajami języka Java? [Zamknięte]

49

Kilka razy widziałem na tej stronie posty, które potępiają implementację generycznych Java. Teraz mogę szczerze powiedzieć, że nie miałem żadnych problemów z ich używaniem. Jednak sam nie próbowałem stworzyć ogólnej klasy. Jakie masz problemy z ogólną obsługą Javy?

Michael K.
źródło
1
Zobacz także stackoverflow.com/questions/31693/…
Bill Michell,
jak powiedział Clinton Begin („facet iBatis”): „nie działają ...”
zgryź

Odpowiedzi:

54

Ogólna implementacja Java wykorzystuje usuwanie typu . Oznacza to, że silnie typowane kolekcje ogólne są w rzeczywistości typami Objectw czasie wykonywania. Ma to pewne względy dotyczące wydajności, ponieważ oznacza, że ​​typy pierwotne muszą być umieszczane w ramkach po dodaniu do kolekcji ogólnej. Oczywiście korzyści z poprawności typu czasu kompilacji przewyższają ogólną głupotę usuwania typu i obsesyjne skupienie się na kompatybilności wstecznej.

ChaosPandion
źródło
4
Wystarczy dodać, ze względu na wymazywanie typu, pobieranie ogólnych informacji w czasie wykonywania nie jest możliwe, chyba że użyjesz czegoś takiego jak TypeLiteral google-guice.googlecode.com/svn/trunk/javadoc/com/google/inject/...
Jeremy Heiler
43
Oznacza to, żenew ArrayList<String>.getClass() == new ArrayList<Integer>.getClass()
Uwaga dla siebie - wymyśl nazwę
9
@ Job nie, nie robi. A C ++ stosuje zupełnie inne podejście do metaprogramowania zwane szablonami. Używa pisania kaczego i możesz robić użyteczne rzeczy, template<T> T add(T v1, T v2) { return v1->add(v2); }a tam masz naprawdę ogólny sposób tworzenia funkcji, która addjest dwiema rzeczami bez względu na to, co to są, muszą mieć metodę o nazwie add()z jednym parametrem.
Trinidad
7
@ Job to rzeczywiście generowany kod. Szablony mają na celu zastąpienie preprocesora C i dlatego powinny być w stanie wykonać bardzo sprytne sztuczki i same w sobie są językiem kompletnym Turinga. Generyczne Java są tylko cukrem dla kontenerów bezpiecznych dla typu, a generyczne C # są lepsze, ale nadal stanowią szablon C ++ dla biednych ludzi.
Trinidad
3
@ Job AFAIK, generycy Java nie generują klasy dla każdego nowego typu ogólnego, po prostu dodaje typecast do kodu, który używa metod ogólnych, więc nie jest to w rzeczywistości meta-programowanie IMO. Szablony C # generują nowy kod dla każdego rodzaju ogólnego, to znaczy, jeśli użyjesz List <int>, a List <double> w świecie C # wygeneruje kod dla każdego z nich. W porównaniu do szablonów, generyczne C # nadal wymagają wiedzy, jaki typ można karmić. Nie możesz zaimplementować prostego Addprzykładu, który podałem, nie ma mowy bez uprzedniej wiedzy, do których klas można zastosować, co jest wadą.
Trinidad
26

Zauważ, że już udzielone odpowiedzi koncentrują się na połączeniu języka Java, JVM i biblioteki klas Java.

Jeśli chodzi o język Java, nie ma nic złego w ogólnych zasadach Javy. Jak opisano w języku C # w porównaniu z generycznymi programami Java, ogólne programy Java są w porządku na poziomie języka 1 .

Nieoptymalne jest to, że JVM nie obsługuje bezpośrednio generyków, co ma kilka poważnych konsekwencji:

  • części biblioteki klas związane z odbiciem nie mogą ujawnić pełnych informacji o typie, które były dostępne w języku Java
  • wiąże się to z pewnym ograniczeniem wydajności

Podejrzewam, że rozróżnienie, które robię, jest pedantyczne, ponieważ język Java jest powszechnie kompilowany z JVM jako celem, a Java Class Library jako podstawową biblioteką.

1 Z możliwym wyjątkiem symboli wieloznacznych, co, jak się uważa, powoduje, że wnioskowanie typu jest nierozstrzygalne w ogólnym przypadku. Jest to główna różnica między standardami C # i Java, która nie jest często wymieniana. Dzięki, Antymon.

Roman Starkov
źródło
14
JVM nie musi obsługiwać generycznych. To byłoby to samo, co stwierdzenie, że C ++ nie ma szablonów, ponieważ prawdziwa maszyna ix86 nie obsługuje szablonów, co jest fałszem. Problem polega na podejściu kompilatora do implementacji typów ogólnych. I znowu, szablony C ++ nie wiążą się z żadnymi karami w czasie wykonywania, nie ma w ogóle refleksji, myślę, że jedynym powodem, który należałoby zrobić w Javie, jest kiepski projekt językowy.
Trynidad,
2
@Trinidad, ale nie powiedziałem, że Java nie ma ogólnych, więc nie rozumiem, jak to jest takie samo. I tak, JVM nie musi ich obsługiwać, tak jak C ++ nie musi optymalizować kodu. Ale brak optymalizacji z pewnością liczyłby się jako „coś złego”.
Roman Starkov
3
Argumentowałem za tym, że JVM nie musi mieć bezpośredniego wsparcia dla generyków, aby móc używać refleksji nad nimi. Inni to zrobili, więc można to zrobić, problem polega na tym, że Java nie generuje nowych klas z generics, co nie wymaga weryfikacji.
Trynidad,
4
@Trinidad: W C ++, zakładając, że kompilator ma wystarczającą ilość czasu i pamięci, szablonów można używać do robienia wszystkiego i wszystkiego, co można zrobić z informacjami dostępnymi w czasie kompilacji (ponieważ kompilacja szablonów jest zakończona przez Turinga), ale istnieje nie ma możliwości tworzenia szablonów przy użyciu informacji, które nie są dostępne przed uruchomieniem programu. Natomiast w .net program może tworzyć typy na podstawie danych wejściowych; dość łatwo byłoby napisać program, w którym liczba różnych typów, które można by stworzyć, przekracza liczbę elektronów we wszechświecie.
supercat
2
@Trinidad: Oczywiście, żadne pojedyncze wykonanie programu nie mogłoby stworzyć wszystkich tych typów (lub, właściwie, niczego poza nieskończenie małą ich częścią), ale chodzi o to, że nie musi. Musi tylko tworzyć typy, które są faktycznie używane. Można mieć program, który mógłby zaakceptować dowolny dowolny numer (np. 8675309) I stworzyć z niego typ unikalny dla tego numeru (np. Z8<Z6<Z7<Z5<Z3<Z0<Z9>>>>>>>), Który miałby inne elementy niż jakikolwiek inny typ. W C ++ wszystkie typy, które można by wygenerować z dowolnego wejścia, musiałyby zostać wygenerowane w czasie kompilacji.
supercat
13

Zwykłą krytyką jest brak reifikacji. Innymi słowy, obiekty w czasie wykonywania nie zawierają informacji o ich ogólnych argumentach (chociaż informacje te są nadal obecne w polach, metodzie, konstruktorach oraz rozszerzonej klasie i interfejsach). Oznacza to, że możesz rzucić, powiedzmy, ArrayList<String>na List<File>. Kompilator wyświetli ostrzeżenia, ale ostrzeże również, jeśli weźmiesz ArrayList<String>przypisanie do Objectodwołania, a następnie prześlesz je List<String>. Zaletą jest to, że przy generyce prawdopodobnie nie powinieneś robić rzutowania, wydajność jest lepsza bez niepotrzebnych danych i, oczywiście, wstecznej kompatybilności.

Niektóre osoby narzekają, że nie można przeciążać na podstawie ogólnych argumentów ( void fn(Set<String>)i void fn(Set<File>)). Zamiast tego musisz użyć lepszych nazw metod. Zauważ, że to przeciążenie nie wymagałoby ponownej weryfikacji, ponieważ przeciążenie jest statycznym problemem podczas kompilacji.

Typy pierwotne nie działają z ogólnymi.

Symbole wieloznaczne i granice są dość skomplikowane. Są bardzo przydatne. Jeśli Java wolałaby interfejsy niezmienności i polecenia „nie pytaj”, bardziej odpowiednie byłyby ogólne informacje po stronie deklaracji niż po stronie użycia.

Tom Hawtin - tackline
źródło
„Pamiętaj, że to przeciążenie nie wymagałoby ponownej weryfikacji, ponieważ przeciążenie jest statycznym problemem związanym z czasem kompilacji”. Prawda? Jak by to działało bez reifikacji? Czy JVM nie musiałby wiedzieć w czasie wykonywania, jaki typ zestawu masz, aby wiedzieć, którą metodę wywołać?
MatrixFrog
2
@ MatrixFrog Nie. Jak mówię, przeciążenie jest problemem związanym z czasem kompilacji. Kompilator używa statycznego typu wyrażenia, aby wybrać określone przeciążenie, które jest następnie metodą zapisaną w pliku klasy. Jeśli masz Object cs = new char[] { 'H', 'i' }; System.out.println(cs);wydrukowane bzdury. Zmień typ csna, char[]a dostaniesz Hi.
Tom Hawtin - tackline
10

Generycy Java są do bani, ponieważ nie można wykonać następujących czynności:

public class AsyncAdapter<Parser,Adapter> extends AsyncTask<String,Integer,Adapter> {
    proptected Adapter doInBackground(String... keywords) {
      Parser p = new Parser(keywords[0]); // this is an error
      /* some more stuff I was hoping for but couldn't do because
         the compiler wouldn't let me
      */
    }
}

Nie musiałbyś rozbijać wszystkich klas w powyższym kodzie, aby działał tak, jak powinien, gdyby generics były rzeczywiście ogólnymi parametrami klasy.

davidk01
źródło
Nie musisz masować każdej klasy. Wystarczy dodać odpowiednią klasę fabryczną i wstrzyknąć ją do AsyncAdapter. (I zasadniczo nie chodzi tu o generyczne, ale fakt, że konstruktory nie są dziedziczone w Javie).
Peter Taylor
13
@PeterTaylor: Odpowiednia klasa fabryczna po prostu wpycha całą płytę kotła do innego niewidzialnego bałaganu i nadal nie rozwiązuje problemu. Teraz za każdym razem, gdy chcę utworzyć nową instancję Parser, będę musiał jeszcze bardziej skomplikować kod fabryczny. Podczas gdy „ogólne” naprawdę oznaczałoby ogólne, wówczas nie byłoby potrzeby fabryk i wszystkich innych bzdur, które noszą nazwę wzorców projektowych. To jeden z wielu powodów, dla których wolę pracować w dynamicznych językach.
davidk01
2
Czasami podobnie w C ++, gdzie nie można przekazać adresu konstruktora, gdy używa się szablonów + wirtualnych - zamiast tego można po prostu użyć funktora fabrycznego.
Mark K Cowan
Java jest do bani, ponieważ nie można napisać „proptected” przed nazwą metody? Biedny. Trzymaj się dynamicznych języków. I szczególnie uważaj na Haskella.
fdreger
5
  • ostrzeżenia kompilatora o brakujących ogólnych parametrach, które nie służą żadnemu celowi, sprawiają, że język jest bezcelowo gadatliwy, np public String getName(Class<?> klazz){ return klazz.getName();}

  • Generyczne nie działają dobrze z tablicami

  • Informacje o utraconym typie sprawiają, że odbicie jest bałaganem odlewania i taśmy izolacyjnej.

Steve B.
źródło
Denerwuje mnie, gdy pojawia się ostrzeżenie o używaniu HashMapzamiast HashMap<String, String>.
Michael K,
1
@Michael, ale tak naprawdę powinien być używany z lekami generycznymi ...
alternatywny
Problem macierzy dotyczy powtarzalności. Wyjaśnienia znajdują się w książce Java Generics (Alligator).
ncmathsadist
5

Sądzę, że inne odpowiedzi mówiły to do pewnego stopnia, ale niezbyt wyraźnie. Jednym z problemów z lekami generycznymi jest utrata typów ogólnych podczas procesu refleksji. Na przykład:

List<String> arr = new ArrayList<String>();
assertTrue( ArrayList.class, arr.getClass() );
TypeVarible[] types = arr.getClass().getTypedVariables();

Niestety zwrócone typy nie mogą powiedzieć, że ogólne typy arr to String. To subtelna różnica, ale ważna. Ponieważ arr jest tworzony w czasie wykonywania, typy ogólne są usuwane w czasie wykonywania, więc nie możesz tego rozgryźć. Jak niektórzy twierdzą, ArrayList<Integer>wygląda tak samo, jak ArrayList<String>z punktu widzenia refleksji.

Może to nie mieć znaczenia dla użytkownika Generics, ale powiedzmy, że chcieliśmy stworzyć fantazyjne ramy, które wykorzystałyby refleksję, aby wymyślić fantazyjne rzeczy na temat tego, jak użytkownik zadeklarował konkretne typy ogólne instancji.

Factory<MySpecialObject> factory = new Factory<MySpecialObject>();
MySpecialObject obj = factory.create();

Powiedzmy, że chcieliśmy, aby fabryka ogólna utworzyła instancję, MySpecialObjectponieważ jest to konkretny typ ogólny, który zadeklarowaliśmy dla tego wystąpienia. Cóż, klasa Factory nie może się przesłuchać w celu ustalenia konkretnego typu zadeklarowanego dla tego wystąpienia, ponieważ Java je usunęła.

W generics .Net możesz to zrobić, ponieważ w czasie wykonywania obiekt wie, że jest to rodzaj ogólny, ponieważ kompilator zastosował go do pliku binarnego. Po wymazaniu Java nie może tego zrobić.

chubbsondubs
źródło
1

Mógłbym powiedzieć kilka dobrych rzeczy na temat leków generycznych, ale to nie było pytanie. Mógłbym narzekać, że nie są one dostępne w czasie wykonywania i nie działają z tablicami, ale zostało to wspomniane.

Duża irytacja psychologiczna: czasami wpadam w sytuacje, w których nie można zmusić leków generycznych do działania. (Tablice są najprostszym przykładem.) I nie mogę zrozumieć, czy leki generyczne nie są w stanie wykonać tej pracy, czy też jestem po prostu głupi. Nienawidzę tego. Coś w rodzaju leków generycznych powinno zawsze działać. Za każdym razem, gdy nie mogę robić tego, co chcę, używając języka Java, wiem, że problem to ja i wiem, że jeśli będę dalej naciskać, w końcu tam dotrę. W przypadku leków generycznych, jeśli stanę się zbyt wytrwały, mogę zmarnować dużo czasu.

Ale prawdziwym problemem jest to, że leki generyczne dodają zbyt dużej złożoności dla zbyt małego zysku. W najprostszych przypadkach może mnie to powstrzymać od dodania jabłka do listy zawierającej samochody. W porządku. Ale bez generyków ten błąd spowodowałby, że ClassCastException byłby bardzo szybki w czasie wykonywania, bez marnowania czasu. Jeśli dodam samochód z fotelikiem dziecięcym z dzieckiem, czy potrzebuję ostrzeżenia podczas kompilacji, że lista dotyczy tylko samochodów z fotelikami dziecięcymi, które zawierają szympansy dziecięce? Lista prostych instancji Object zaczyna wyglądać na dobry pomysł.

Kod ogólny może zawierać wiele słów i znaków, a także zajmować dużo miejsca i czytać. Mogę spędzić dużo czasu, żeby cały ten dodatkowy kod działał poprawnie. Kiedy tak się dzieje, czuję się niesamowicie sprytny. Zmarnowałem też kilka godzin lub więcej własnego czasu i muszę się zastanawiać, czy ktokolwiek kiedykolwiek będzie w stanie znaleźć kod. Chciałbym móc przekazać to na utrzymanie osobom, które są mniej inteligentne ode mnie lub mają mniej czasu do stracenia.

Z drugiej strony (trochę się tam skończyłem i czuję potrzebę zapewnienia równowagi), miło jest, gdy używa się prostych kolekcji i map do dodawania, wstawiania i sprawdzania podczas pisania i zwykle nie dodaje dużo złożoność kodu ( jeśli ktoś inny napisze kolekcję lub mapę) . I Java jest w tym lepsza niż C #. Wygląda na to, że żadna z kolekcji C #, której chcę używać, nie obsługuje generycznych. (Przyznaję, że mam dziwne upodobania w kolekcjach).

RalphChapin
źródło
Niezły rant. Istnieje jednak wiele bibliotek / frameworków, które mogłyby istnieć bez generycznych, np. Guice, Gson, Hibernacja. A leki ogólne nie są już takie trudne, kiedy się do tego przyzwyczaisz. Pisanie i czytanie argumentów typu to prawdziwa PITA; tutaj val pomaga, jeśli możesz użyć.
maaartinus
1
@maaartinus: Napisałem to jakiś czas temu. Również OP poprosił o „problemy”. Uważam, że leki generyczne są niezwykle przydatne i bardzo je lubię - teraz znacznie bardziej niż wtedy. Jednak jeśli piszesz własną kolekcję, bardzo trudno się ich nauczyć. I ich przydatność zanika gdy typ kolekcji jest określana w czasie wykonywania - gdy zbiór ma metodę informujący Cię klasę swoich wpisów. W tym momencie generyczne nie działają, a twoje IDE wygeneruje setki bezsensownych ostrzeżeń. 99,99% programowania Java nie obejmuje tego. Ale ja po prostu miałem dużo kłopotów z tym, gdy pisałem wyżej.
RalphChapin
Jako programista Java i C # zastanawiam się, dlaczego wolisz kolekcje Java?
Ivaylo Slavov
Fine. But without generics this error would throw a ClassCastException really quick at run time with little time wasted.To naprawdę zależy od tego, ile czasu występuje między uruchomieniem programu a dotknięciem linii danego kodu. Jeśli użytkownik zgłosi błąd i odtworzenie go zajmie kilka minut (lub, w najgorszym przypadku, godziny lub nawet dni), weryfikacja czasu kompilacji zacznie wyglądać coraz lepiej ...
Mason Wheeler,