Jakie są zalety wymazywania typów w Javie?


98

Przeczytałem dzisiaj tweeta, który powiedział:

To zabawne, gdy użytkownicy Javy narzekają na wymazywanie typów, co jest jedyną rzeczą, którą Java zrobiła dobrze, ignorując wszystkie rzeczy, które popełniły błąd.

Zatem moje pytanie brzmi:

Czy są korzyści z wymazywania typów w Javie? Jakie korzyści techniczne lub styl programowania oferuje (prawdopodobnie) poza preferencją implementacji JVM w zakresie wstecznej kompatybilności i wydajności w czasie wykonywania?


6
Jeśli nie chcesz, aby Twoje pytanie zostało zamknięte jako „oparte na opiniach”, zadaj je inaczej. (Na przykład „jakie są techniczne zalety wymazywania typów”). Jeszcze lepiej, zapytaj głośnik wysokotonowy, co tak naprawdę mówi.
Stephen C

3
Usuń „dlaczego to jest dobre”. „Dobra rzecz” jest wyraźnie subiektywną charakterystyką i nasuwa (oczywiście) pytanie, czy usuwanie typów jest dobrą rzeczą ... co jest sporne.
Stephen C

3
Boże pomóż nam, jeśli ta strona ma się przerodzić w pytania dotyczące wszystkiego, co kiedykolwiek zostało opublikowane na Twitterze. Przynajmniej zacytuj renomowane źródło dla twojego pytania.
Markiz Lorne

21
Dlaczego pytanie musi pochodzić z „renomowanego źródła”? To po prostu głupie.
vertti

4
Cóż, większość problemów tutaj nie pochodzi z ŻADNEGO źródła, poza tym, że pytający mają własny problem z określonym oprogramowaniem / językiem / czymkolwiek, i na szczęście jest wielu profesjonalistów, którzy „marnują” swój czas pomagając tym ludziom.
vertti

Odpowiedzi:


90

Typ wymazywania jest dobry

Trzymajmy się faktów

Wiele dotychczasowych odpowiedzi jest nadmiernie zaniepokojonych użytkownikami Twittera. Warto skupić się na wiadomościach, a nie na posłańcu. Jest dość spójna wiadomość, zawierająca nawet tylko wspomniane dotąd fragmenty:

To zabawne, gdy użytkownicy Javy narzekają na wymazywanie typów, co jest jedyną rzeczą, którą Java zrobiła dobrze, ignorując wszystkie rzeczy, które popełniły błąd.

Dostaję ogromne korzyści (np. Parametryczność) i zerowy koszt (rzekomy koszt to granica wyobraźni).

nowy T to zepsuty program. Jest izomorficzne z twierdzeniem, że „wszystkie zdania są prawdziwe”. Nie jestem w tym duży.

Cel: rozsądne programy

Te tweety odzwierciedlają perspektywę, która nie jest zainteresowana tym, czy możemy zmusić maszynę do zrobienia czegoś , ale bardziej tym, czy możemy uzasadnić, że maszyna zrobi coś, czego naprawdę chcemy. Dobre rozumowanie jest tego dowodem. Dowody można określić za pomocą notacji formalnej lub mniej formalnej. Niezależnie od języka specyfikacji muszą być jasne i rygorystyczne. Nieformalne specyfikacje nie są niemożliwe do poprawnej struktury, ale często zawierają błędy w praktycznym programowaniu. W końcu otrzymujemy środki zaradcze, takie jak testy automatyczne i eksploracyjne, aby nadrobić problemy, które mamy z nieformalnym rozumowaniem. Nie oznacza to, że testowanie jest z natury złym pomysłem, ale cytowany użytkownik Twittera sugeruje, że istnieje znacznie lepszy sposób.

Dlatego naszym celem jest posiadanie poprawnych programów, co do których możemy jasno i rygorystycznie rozumować, w sposób odpowiadający temu, jak maszyna faktycznie wykona program. Nie jest to jednak jedyny cel. Chcemy również, aby nasza logika miała pewien stopień ekspresji. Na przykład, tylko tyle możemy wyrazić za pomocą logiki zdań. Fajnie jest mieć uniwersalną (∀) i egzystencjalną (∃) kwantyfikację z czegoś w rodzaju logiki pierwszego rzędu.

Używanie systemów typów do rozumowania

Te cele można bardzo dobrze rozwiązać za pomocą systemów typów. Jest to szczególnie jasne ze względu na korespondencję Curry-Howard . Tę zgodność często wyraża się następującą analogią: typy są do programów, tak jak twierdzenia do dowodów.

Ta korespondencja jest dość głęboka. Możemy przyjąć logiczne wyrażenia i przetłumaczyć je poprzez korespondencję na typy. Następnie, jeśli mamy program o tej samej sygnaturze typu, który kompiluje, udowodniliśmy, że wyrażenie logiczne jest uniwersalnie prawdziwe (tautologia). Dzieje się tak, ponieważ korespondencja jest dwukierunkowa. Transformacja między typem / programem a światami twierdzeń / dowodów jest mechaniczna iw wielu przypadkach może być zautomatyzowana.

Curry-Howard dobrze wpisuje się w to, co chcielibyśmy zrobić ze specyfikacjami programu.

Czy systemy typów są przydatne w Javie?

Nawet rozumiejąc Curry-Howard, niektórym osobom łatwo jest zlekceważyć wartość systemu czcionek, gdy

  1. jest niezwykle ciężka w pracy
  2. odpowiada (poprzez Curry-Howard) logice o ograniczonej ekspresji
  3. jest uszkodzony (co powoduje scharakteryzowanie systemów jako „słabe” lub „silne”).

Jeśli chodzi o pierwszy punkt, być może IDE sprawiają, że system typów Javy jest wystarczająco łatwy w obsłudze (to wysoce subiektywne).

Jeśli chodzi o drugi punkt, zdarza się, że Java prawie odpowiada logice pierwszego rzędu. Typy generyczne wykorzystują system typów równoważny uniwersalnej kwantyfikacji. Niestety, symbole wieloznaczne dają nam tylko niewielki ułamek kwantyfikacji egzystencjalnej. Ale uniwersalna kwantyfikacja to całkiem dobry początek. Fajnie jest móc powiedzieć, że funkcje List<A>działają uniwersalnie dla wszystkich możliwych list, ponieważ A jest całkowicie nieograniczony. Prowadzi to do tego, o czym mówi użytkownik Twittera w odniesieniu do „parametryczności”.

Często cytowanym artykułem na temat parametryczności jest bezpłatne twierdzenie Philipa Wadlera ! . Interesujące w tym artykule jest to, że na podstawie samego podpisu typu możemy udowodnić kilka bardzo interesujących niezmienników. Gdybyśmy mieli napisać testy automatyczne dla tych niezmienników, marnowalibyśmy bardzo czas. Na przykład for List<A>, z samego podpisu typu forflatten

<A> List<A> flatten(List<List<A>> nestedLists);

możemy to uzasadnić

flatten(nestedList.map(l -> l.map(any_function)))
     flatten(nestList).map(any_function)

To prosty przykład i prawdopodobnie możesz uzasadnić to nieformalnie, ale jest jeszcze ładniej, gdy otrzymamy takie dowody formalnie za darmo z systemu typów i sprawdzone przez kompilator.

Brak usunięcia może prowadzić do nadużyć

Z punktu widzenia implementacji języka, typy generyczne Javy (które odpowiadają typom uniwersalnym) wpływają bardzo mocno na parametryczność wykorzystywaną do uzyskiwania dowodów na to, co robią nasze programy. To prowadzi do trzeciego wspomnianego problemu. Wszystkie te zyski dowodu i poprawności wymagają systemu typu dźwiękowego zaimplementowanego bez wad. Java z pewnością ma pewne funkcje językowe, które pozwalają nam zniszczyć nasze rozumowanie. Należą do nich między innymi:

  • skutki uboczne z systemem zewnętrznym
  • odbicie

Nieusunięte rodzaje ogólne są pod wieloma względami powiązane z refleksją. Bez wymazywania istnieją informacje o czasie wykonywania, które są przenoszone z implementacją, których możemy użyć do zaprojektowania naszych algorytmów. Oznacza to, że statycznie, gdy wnioskujemy o programach, nie mamy pełnego obrazu. Odbicie poważnie zagraża poprawności wszelkich dowodów, o których wnioskujemy statycznie. To nie przypadek, że refleksja prowadzi również do różnych trudnych defektów.

Więc w jaki sposób nieusunięte typy generyczne mogą być „przydatne”? Rozważmy użycie wspomniane w tweecie:

<T> T broken { return new T(); }

Co się stanie, jeśli T nie ma konstruktora bez argonu? W niektórych językach otrzymujesz wartość zerową. A może pomijasz wartość null i przechodzisz od razu do zgłaszania wyjątku (do którego wartości null wydają się prowadzić). Ponieważ nasz język jest kompletny w Turingu, nie można wywnioskować, które wywołania brokenbędą obejmować „bezpieczne” typy z konstruktorami bezargumentowymi, a które nie. Straciliśmy pewność, że nasz program działa uniwersalnie.

Wymazywanie oznacza, że ​​uzasadniliśmy (więc wymażmy)

Jeśli więc chcemy uzasadniać nasze programy, stanowczo odradzamy stosowanie funkcji językowych, które silnie zagrażają naszemu rozumowaniu. Kiedy już to zrobimy, dlaczego nie po prostu porzucić typów w czasie wykonywania? Nie są potrzebne. Możemy uzyskać pewną wydajność i prostotę z satysfakcją, że żadne rzutowanie nie zawiedzie lub że podczas wywołania może brakować metod.

Wymazywanie zachęca do rozumowania.


7
W czasie, gdy zbierałem tę odpowiedź, inne osoby odpowiedziały podobnymi odpowiedziami. Ale napisawszy tyle, zdecydowałem się go opublikować, zamiast go wyrzucić.
Sukant Hajra

32
Z całym szacunkiem, wymazywanie typów było nieudaną próbą przeniesienia funkcji do Javy bez zrywania wstecznej kompatybilności. To nie jest siła. Jeśli głównym problemem było to, że możesz spróbować utworzyć instancję obiektu bez konstruktora bez parametrów, właściwą odpowiedzią jest zezwolenie na ograniczenie „typów z konstruktorami bez parametrów”, a nie uszkodzenie funkcji języka.
Podstawowy

5
Po prostu, z szacunkiem, nie przedstawiasz spójnej argumentacji. Twierdzisz jedynie, że wymazywanie jest „paraliżujące”, z całkowitym lekceważeniem wartości dowodów kompilowanych w czasie jako nieodpartego narzędzia do rozumowania programów. Co więcej, korzyści te są całkowicie ortogonalne w stosunku do krzywej ścieżki i motywacji, jaką Java przyjęła do implementacji leków generycznych. Korzyści te dotyczą również języków o znacznie bardziej oświeconej implementacji wymazywania.
Sukant Hajra

44
Przedstawiony tutaj argument nie jest przeciwko wymazywaniu, jest przeciwko odbiciu (i ogólnie sprawdzaniu typu w czasie wykonywania) - zasadniczo stwierdza, że ​​odbicie jest złe, więc fakt, że wymazywanie nie działa z nimi dobrze, jest dobry. Sama w sobie jest to ważna kwestia (chociaż wiele osób by się z tym nie zgodziło); ale nie ma to większego sensu w kontekście, ponieważ Java ma już kontrole odbicia / środowiska uruchomieniowego, a one nie mają i nie znikną. Zatem posiadanie konstrukcji, która nie współgra z tą istniejącą, niezalecaną częścią języka, jest ograniczeniem, bez względu na to, jak na to spojrzysz.
Pavel Minaev

10
Twoja odpowiedź jest po prostu nie na temat. Wprowadzasz obszerne (choć interesujące na swój własny sposób) wyjaśnienie, dlaczego wymazywanie typów jako abstrakcyjne pojęcie jest ogólnie przydatne w projektowaniu systemów typów, ale tematem jest korzyść z konkretnej cechy generycznej języka Java oznaczonej jako „wymazywanie typów ”, która tylko powierzchownie przypomina opisywaną przez ciebie koncepcję, całkowicie zawodząc w dostarczaniu interesujących niezmienników i wprowadzając nieuzasadnione ograniczenia.
Marko Topolnik

41

Typy to konstrukcja używana do pisania programów w sposób, który umożliwia kompilatorowi sprawdzenie poprawności programu. Typ to propozycja wartości - kompilator sprawdza, czy ta propozycja jest prawdziwa.

Podczas wykonywania programu nie powinno być potrzeby podawania informacji o typie - zostało to już zweryfikowane przez kompilator. Kompilator powinien mieć możliwość odrzucenia tych informacji w celu optymalizacji kodu - przyspieszenia jego działania, wygenerowania mniejszego pliku binarnego itp. Ułatwia to wymazanie parametrów typu.

Java przerywa statyczne typowanie, umożliwiając odpytywanie informacji o typie w czasie wykonywania - odbicie, instancja itp. Pozwala to na tworzenie programów, których nie można zweryfikować statycznie - omijają one system typów. Pomija również możliwości statycznej optymalizacji.

Fakt, że parametry typu są usuwane, uniemożliwia skonstruowanie niektórych wystąpień tych niepoprawnych programów, jednak bardziej niepoprawne programy byłyby niedozwolone, gdyby usunięto więcej informacji o typie, a elementy odbicia i wystąpienia obiektów zostały usunięte.

Wymazywanie jest ważne dla zachowania właściwości „parametryczności” typu danych. Powiedzmy, że mam sparametryzowany typ „Lista” nad typem składnika T. tj. List <T>. Ten typ jest propozycją, że ten typ List działa identycznie dla każdego typu T. Fakt, że T jest abstrakcyjnym, nieograniczonym parametrem typu oznacza, że ​​nic nie wiemy o tym typie, dlatego nie możemy robić niczego specjalnego dla specjalnych przypadków T.

np. powiedz, że mam List xs = asList ("3"). Dodaję element: xs.add ("q"). W końcu otrzymuję ["3", "q"]. Ponieważ jest to parametryczne, mogę założyć, że List xs = asList (7); xs.add (8) kończy się na [7,8] Wiem z typu, że nie robi jednej rzeczy dla String, a jednej dla Int.

Ponadto wiem, że funkcja List.add nie może wymyślać wartości T z powietrza. Wiem, że jeśli do mojej asList („3”) dodano „7”, jedyne możliwe odpowiedzi byłyby konstruowane z wartości „3” i „7”. Nie ma możliwości dodania „2” lub „z” do listy, ponieważ funkcja nie mogłaby jej skonstruować. Żadna z tych innych wartości nie byłaby rozsądna do dodania, a parametryczność zapobiega konstruowaniu tych nieprawidłowych programów.

Zasadniczo kasowanie zapobiega jakimś sposobom naruszenia parametryczności, a tym samym eliminuje możliwości błędnych programów, co jest celem statycznego pisania.


15

(Chociaż już tutaj napisałem odpowiedź, wracając do tego pytania dwa lata później, zdaję sobie sprawę, że istnieje inny, zupełnie inny sposób odpowiedzi, więc pozostawiam poprzednią odpowiedź nietkniętą i dodam tę.)


Jest wysoce dyskusyjne, czy proces wykonywany w Javie Generics zasługuje na miano „wymazywania typu”. Ponieważ typy ogólne nie są usuwane, ale zastępowane ich surowymi odpowiednikami, lepszym wyborem wydaje się być „okaleczanie typów”.

Kwintesencją funkcji wymazywania typów w powszechnie rozumianym sensie jest wymuszanie, aby środowisko wykonawcze pozostawało w granicach statycznego systemu typów, czyniąc go „ślepym” na strukturę danych, do których uzyskuje dostęp. Daje to pełną moc kompilatorowi i pozwala mu dowodzić twierdzeń na podstawie samych typów statycznych. Pomaga również programiście, ograniczając stopnie swobody kodu, dając więcej mocy prostemu rozumowaniu.

Wymazywanie typu w Javie tego nie daje - uszkadza kompilator, jak w tym przykładzie:

void doStuff(List<Integer> collection) { 
}

void doStuff(List<String> collection) // ERROR: a method cannot have 
                   // overloads which only differ in type parameters

(Powyższe dwie deklaracje zwijają się do tego samego podpisu metody po usunięciu).

Z drugiej strony środowisko wykonawcze może nadal sprawdzać typ obiektu i uzasadniać go, ale ponieważ jego wgląd w prawdziwy typ jest utrudniony przez wymazanie, naruszenia typu statycznego są trywialne do osiągnięcia i trudne do uniknięcia.

Aby jeszcze bardziej skomplikować sprawę, oryginalne i usunięte sygnatury typów współistnieją i są rozważane równolegle podczas kompilacji. Dzieje się tak, ponieważ cały proces nie polega na usuwaniu informacji o typie ze środowiska wykonawczego, ale na tworzeniu systemu typów ogólnych w starszym systemie typów surowych w celu zachowania kompatybilności wstecznej. Ten klejnot jest klasycznym przykładem:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

(Nadmiarowy extends Objectmusiał zostać dodany, aby zachować wsteczną zgodność skasowanego podpisu).

Mając to na uwadze, powróćmy do cytatu:

To zabawne, gdy użytkownicy Javy narzekają na wymazywanie typów, które jest jedyną rzeczą, którą Java ma rację

Co dokładnie poprawiła Java? Czy to samo słowo, niezależnie od znaczenia? Dla kontrastu spójrz na skromny inttyp: żadne sprawdzenie typu w czasie wykonywania nie jest nigdy wykonywane ani nawet możliwe, a wykonanie jest zawsze całkowicie bezpieczne dla typów. Tak wygląda wymazywanie typów po prawidłowym wykonaniu: nawet nie wiesz, że tam jest.


10

Jedyną rzeczą, której w ogóle tu nie rozważam, jest to, że polimorfizm OOP w czasie wykonywania jest zasadniczo zależny od reifikacji typów w czasie wykonywania. Kiedy język, którego kręgosłup jest utrzymywany w miejscu przez odmienne typy, wprowadza główne rozszerzenie swojego systemu typów i opiera go na usuwaniu typów, dysonans poznawczy jest nieuniknionym rezultatem. Dokładnie to stało się ze społecznością Java; to dlatego wymazywanie typów wzbudziło tak wiele kontrowersji i ostatecznie planuje się cofnąć to w przyszłej wersji Javy . Znalezienie czegoś zabawnego w tej skardze użytkowników Javy zdradza albo szczere niezrozumienie ducha Javy, albo świadomy żart.

Twierdzenie „wymazanie jest jedyną rzeczą, którą Java ma rację” sugeruje twierdzenie, że „wszystkie języki oparte na dynamicznej wysyłce w porównaniu z argumentem funkcji typu runtime są zasadniczo wadliwe”. Chociaż samo w sobie z pewnością jest to uzasadnione twierdzenie, które można nawet uznać za uzasadnioną krytykę wszystkich języków OOP, w tym Javy, nie może stanowić punktu kluczowego, z którego można oceniać i krytykować funkcje w kontekście Javy , gdzie polimorfizm w czasie wykonywania jest aksjomatyczna.

Podsumowując, chociaż można słusznie stwierdzić, że „usuwanie typów jest sposobem na projektowanie języka”, pozycje obsługujące wymazywanie typów w Javie są niewłaściwe po prostu dlatego, że jest na to dużo, dużo za późno i tak było już w historycznym momencie kiedy Oak został objęty przez Sun i przemianowany na Java.



Jeśli chodzi o to, czy statyczne typowanie samo w sobie jest właściwym kierunkiem w projektowaniu języków programowania, pasuje to do znacznie szerszego kontekstu filozoficznego tego, co uważamy za aktywność programowania . Jedna szkoła myślenia, wyraźnie wywodząca się z klasycznej tradycji matematyki, postrzega programy jako przykłady jednej lub drugiej koncepcji matematycznej (zdania, funkcje itp.), Ale istnieje zupełnie inna klasa podejść, które postrzegają programowanie jako sposób na porozmawiaj z maszyną i wyjaśnij, czego od niej chcemy. Z tego punktu widzenia program jest dynamiczną, organicznie rozwijającą się istotą, dramatycznym przeciwieństwem starannie wzniesionej edycji programu zapisanego statycznie.

Wydawałoby się naturalne traktowanie języków dynamicznych jako kroku w tym kierunku: spójność programu wyłania się oddolnie, bez żadnych stałych a priori, które narzucałyby go odgórnie. Ten paradygmat można postrzegać jako krok w kierunku modelowania procesu, w którym my, ludzie, stajemy się tym, czym jesteśmy, poprzez rozwój i uczenie się.


Dziękuję za to. Jest to dokładny rodzaj głębszego zrozumienia filozofii, którego oczekiwałem w przypadku tego pytania.
Daniel Langdon

Dynamiczna wysyłka nie jest uzależniona od typów zreifikowanych. Często opiera się na tym, co w literaturze dotyczącej programowania nazywa się „tagami”, ale nawet wtedy nie musi używać tagów. Jeśli tworzysz interfejs (dla obiektu) i tworzysz anonimowe instancje tego interfejsu, to wykonujesz dynamiczne wysyłanie bez potrzeby stosowania typów reifikowanych.
Sukant Hajra

@sukanthajra To tylko rozdwajanie włosów. Znacznik jest informacją o czasie wykonywania potrzebną do rozwiązania problemu z zachowaniem instancji. Jest poza zasięgiem statycznego systemu typów i na tym polega istota omawianej koncepcji.
Marko Topolnik

Istnieje bardzo statyczny typ zaprojektowany do tego właśnie rozumowania, zwany „typem sumarycznym”. Nazywa się to również „typem wariantowym” (w bardzo dobrej książce Benjamina Pierce'a o typach i językach programowania). Więc to wcale nie jest rozdwajanie włosów. Możesz także użyć katamorfizmu / zagięcia / gościa, aby uzyskać całkowicie pozbawione tagów podejście.
Sukant Hajra

Dziękuję za lekcję teorii, ale nie widzę znaczenia. Naszym tematem jest system typów Javy.
Marko Topolnik

9

Kolejny post tego samego użytkownika w tej samej rozmowie:

nowy T to zepsuty program. Jest to izomorficzne z twierdzeniem „wszystkie zdania są prawdziwe”. Nie jestem w tym duży.

(Było to odpowiedzią na oświadczenie innego użytkownika, a mianowicie, że „wydaje się, że w niektórych sytuacjach„ nowe T ”byłoby lepsze”, przy czym pomysł new T()jest niemożliwy z powodu wymazania typu. (Jest to dyskusyjne - nawet jeśli Tbyły dostępne pod adresem środowiska uruchomieniowego, może to być klasa abstrakcyjna lub interfejs, może to być Void, lub może brakować konstruktora no-arg, albo jego konstruktor no-arg może być prywatny (np. ponieważ przypuszczalnie jest to klasa pojedyncza) lub Konstruktor no-arg może określić sprawdzony wyjątek, którego metoda ogólna nie wychwytuje ani nie określa - ale taka była przesłanka. Niezależnie od tego, prawdą jest, że bez wymazywania można przynajmniej napisać T.class.newInstance(), który obsługuje te problemy.))

Ten pogląd, że typy są izomorficzne ze zdaniami, sugeruje, że użytkownik ma podstawy w formalnej teorii typów. (S) najprawdopodobniej nie lubi "typów dynamicznych" lub "typów uruchomieniowych" i wolałby Javę bez ograniczeń instanceofi refleksji i tak dalej. (Pomyśl o języku takim jak Standard ML, który ma bardzo bogaty (statyczny) system typów i którego dynamiczna semantyka nie zależy od żadnych informacji o typach.)

Warto pamiętać, przy okazji, że użytkownik trolluje: podczas gdy on (e) prawdopodobnie szczerze woli (statycznie) typowane języki, (s) nie próbuje szczerze przekonać innych do tego poglądu. Głównym celem oryginalnego tweeta było raczej wyśmiewanie tych, którzy się nie zgadzają, a po tym, jak niektórzy z nich się nie zgadzają, użytkownik opublikował kolejne tweety, takie jak „powodem, dla którego java wymazuje typ, jest to, że Wadler i inni wiedzą, co robią, w przeciwieństwie do użytkowników java ”. Niestety, utrudnia to ustalenie, o czym właściwie myśli; ale na szczęście prawdopodobnie oznacza to, że nie jest to bardzo ważne. Ludzie z rzeczywistą głębią swoich poglądów na ogół nie uciekają się do trolli, które są dość pozbawione treści.


2
Nie kompiluje się w Javie, ponieważ ma wymazywanie typu
Mark Rotteveel,

1
@MarkRotteveel: Dzięki; Dodałem akapit, aby to wyjaśnić (i skomentować).
ruakh

1
Sądząc po mieszance głosów za i przeciw, jest to najbardziej kontrowersyjna odpowiedź, jaką kiedykolwiek opublikowałem. Jestem dziwnie dumny.
ruakh

6

Jedną dobrą rzeczą jest to, że nie było potrzeby zmiany maszyny JVM, gdy wprowadzono produkty generyczne. Java implementuje typy ogólne tylko na poziomie kompilatora.


1
JVM zmienia się w porównaniu z czym? Szablony również nie wymagałyby zmian JVM.
Markiz Lorne

5

Powodem, dla którego wymazywanie typu jest dobre, jest to, że rzeczy, które uniemożliwia, są szkodliwe. Zapobieganie inspekcji argumentów typu w czasie wykonywania ułatwia zrozumienie i wnioskowanie na temat programów.

Spostrzeżenie, które uznałem za nieco sprzeczne z intuicją, jest takie, że kiedy sygnatury funkcji są bardziej ogólne, stają się łatwiejsze do zrozumienia. Dzieje się tak, ponieważ zmniejsza się liczba możliwych implementacji. Rozważ metodę z tym podpisem, o której wiemy, że nie ma skutków ubocznych:

public List<Integer> XXX(final List<Integer> l);

Jakie są możliwe implementacje tej funkcji? Bardzo dużo. Niewiele można powiedzieć o tym, co robi ta funkcja. Może to być odwrócenie listy wejść. Może to być łączenie ze sobą intów, sumowanie ich i zwracanie listy o połowę mniejszej. Istnieje wiele innych możliwości, które można sobie wyobrazić. Rozważmy teraz:

public <T> List<T> XXX(final List<T> l);

Ile jest implementacji tej funkcji? Ponieważ implementacja nie może znać typu elementów, można teraz wykluczyć ogromną liczbę implementacji: elementów nie można łączyć, dodawać do listy ani odfiltrowywać i in. Jesteśmy ograniczeni do takich rzeczy, jak: tożsamość (brak zmian na liście), upuszczanie elementów lub odwracanie listy. Ta funkcja jest łatwiejsza do rozważenia na podstawie samego jej podpisu.

Tylko że… w Javie zawsze możesz oszukać system typów. Ponieważ implementacja tej ogólnej metody może używać takich rzeczy, jak instanceofsprawdzenia i / lub rzutowania na dowolne typy, nasze rozumowanie oparte na sygnaturze typu można łatwo uczynić bezużytecznymi. Funkcja mogłaby sprawdzić typ elementów i wykonać dowolną liczbę rzeczy w oparciu o wynik. Jeśli te hacki w czasie wykonywania są dozwolone, sparametryzowane sygnatury metod stają się dla nas znacznie mniej przydatne.

Gdyby Java nie miała wymazywania typów (to znaczy argumenty typów byłyby reifikowane w czasie wykonywania), to po prostu pozwoliłoby na więcej takich sztuczek utrudniających rozumowanie. W powyższym przykładzie implementacja może naruszyć oczekiwania określone przez sygnaturę typu tylko wtedy, gdy lista zawiera przynajmniej jeden element; ale jeśli Tzostał zreifikowany, mógłby to zrobić, nawet jeśli lista byłaby pusta. Typy zreifikowane po prostu zwiększyłyby (już bardzo wiele) możliwości utrudniania nam zrozumienia kodu.

Usuwanie typów sprawia, że ​​język jest mniej „potężny”. Ale niektóre formy „władzy” są w rzeczywistości szkodliwe.


1
Wygląda na to, że albo argumentujesz, że leki generyczne są zbyt złożone, aby programiści Java nie mogli „zrozumieć i uzasadnić”, albo że nie można im ufać, że nie strzelą sobie w stopę, jeśli tylko da się im szansę. To ostatnie wydaje się być zsynchronizowane z etosem Javy (bez typów niepodpisanych itp.), Ale wydaje się trochę protekcjonalne dla programistów
Basic

1
@Basic Musiałem wyrazić się bardzo słabo, ponieważ wcale nie o to mi chodziło. Problem nie polega na tym, że instanceoftypy generyczne są złożone, ale na tym, że rzeczy takie jak rzutowanie i utrudniają nam zdolność rozumowania, co robi kod na podstawie typów. Jeśli Java miałaby zreifikować argumenty typu, tylko pogorszyłoby to problem. Wymazywanie typów w czasie wykonywania powoduje zwiększenie użyteczności systemu typów.
Lachlan

@lachlan miało to dla mnie sens i nie sądzę, by normalna lektura była obrazą dla programistów Java.
Rob Grant

3

To nie jest bezpośrednia odpowiedź (OP zapytał „jakie są korzyści”, a ja odpowiadam „jakie są wady”)

W porównaniu z systemem typu C # wymazywanie typów w Javie jest prawdziwym problemem dla dwóch powodów

Nie możesz dwukrotnie zaimplementować interfejsu

W C # można zaimplementować oba IEnumerable<T1>i IEnumerable<T2>bezpiecznie, zwłaszcza jeśli te dwa typy nie mają wspólnego przodka (tj. Ich przodek jest Object ).

Praktyczny przykład: w Spring Framework nie można implementować ApplicationListener<? extends ApplicationEvent>wielokrotnie. Jeśli potrzebujesz różnych zachowań w oparciu o Tto, musisz przetestowaćinstanceof

Nie możesz zrobić nowego T ()

(i potrzebujesz odniesienia do Class, aby to zrobić)

Jak komentowali inni, wykonanie odpowiednika new T()może być wykonane tylko poprzez odbicie, tylko przez wywołanie wystąpienia Class<T>, upewniając się co do parametrów wymaganych przez konstruktora. C # umożliwia to new T() tylko w przypadku ograniczenia Tdo konstruktora bez parametrów. Jeśli Tnie przestrzega tego ograniczenia, zgłaszany jest błąd kompilacji .

W Javie często będziesz zmuszony pisać metody, które wyglądają jak poniżej

public <T> T create(....params, Class<T> classOfT)
{

    ... whatever you do
    ... you will end up
    T = classOfT.newInstance();


    ... or more advanced reflection
    Constructor<T> parameterizedConstructorThatYouKnowAbout = classOfT.getConstructor(...,...);
}

Wady powyższego kodu to:

  • Class.newInstance działa tylko z konstruktorem bez parametrów. Jeśli żaden nie jest dostępny, ReflectiveOperationExceptionjest generowany w czasie wykonywania
  • Konstruktor odbity nie wyróżnia problemów w czasie kompilacji, takich jak powyżej. Jeśli dokonasz refaktoryzacji, zamienisz argumenty, dowiesz się tylko w czasie wykonywania

Gdybym był autorem C #, wprowadziłbym możliwość określenia jednego lub więcej ograniczeń konstruktora, które są łatwe do zweryfikowania w czasie kompilacji (więc mogę wymagać np. Konstruktora z parametrami string,string). Ale ostatnia to spekulacja


2

Dodatkowy punkt, który wydaje się być brany pod uwagę w żadnej innej odpowiedzi: jeśli naprawdę potrzebujesz typów ogólnych z pisaniem w czasie wykonywania, możesz zaimplementować to samodzielnie w następujący sposób:

public class GenericClass<T>
{
     private Class<T> targetClass;
     public GenericClass(Class<T> targetClass)
     {
          this.targetClass = targetClass;
     }

Ta klasa jest następnie w stanie robić wszystkie rzeczy, które byłyby osiągalne domyślnie, gdyby Java nie używała wymazywania: może przydzielać nowe Ts (zakładając, że Tma konstruktor, który pasuje do wzorca, którego oczekuje, że ma użyć) lub tablice Ts, może dynamicznie testuj w czasie wykonywania, czy określony obiekt jest Ti zmieniaj zachowanie w zależności od tego i tak dalej.

Na przykład:

     public T newT () { 
         try {
             return targetClass.newInstance(); 
         } catch(/* I forget which exceptions can be thrown here */) { ... }
     }

     private T value;
     /** @throws ClassCastException if object is not a T */
     public void setValueFromObject (Object object) {
         value = targetClass.cast(object);
     }
}

Czy ktoś chciałby wyjaśnić powód głosowania przeciw? O ile wiem, jest to doskonale wyjaśnienie, dlaczego domniemana wada systemu wymazywania typów w Javie nie jest w rzeczywistości prawdziwym ograniczeniem. Więc w czym problem?
Jules

Głosuję za. Nie żebym wspierał sprawdzanie typu w czasie wykonywania, ale ta sztuczka może być przydatna w kopalniach soli.
techtangents

3
Java to język kopalni soli, co sprawia, że ​​jest to absolutna konieczność, biorąc pod uwagę brak informacji o typie środowiska wykonawczego. Miejmy nadzieję, że wymazywanie typów zostanie cofnięte w przyszłej wersji Javy, dzięki czemu typy generyczne będą znacznie bardziej użyteczną funkcją. Obecnie konsensus jest taki, że ich stosunek ciągu do masy jest poniżej 1.
Marko Topolnik,

Cóż, jasne ... O ile Twój TargetType nie używa samych typów ogólnych. Ale to wydaje się dość ograniczające.
Podstawowy

Działa również z typami ogólnymi. Jedynym ograniczeniem jest to, że jeśli chcesz utworzyć instancję, potrzebujesz konstruktora z właściwymi typami argumentów, ale każdy inny znany mi język ma to samo ograniczenie, więc nie widzę ograniczenia.
Jules

0

unika nadmiaru kodu, podobnie jak w C ++, ponieważ ten sam kod jest używany dla wielu typów; jednak wymazywanie typów wymaga wirtualnego wysyłania, podczas gdy podejście C ++ - nadwyżki kodu może wykonywać niewirtualnie wysyłane typy generyczne


3
Jednak większość tych wirtualnych wysyłek jest konwertowana na statyczne w czasie wykonywania, więc nie jest tak źle, jak mogłoby się wydawać.
Piotr Kołaczkowski

1
To prawda, dostępność kompilatora w czasie wykonywania pozwala Javie robić wiele fajnych rzeczy (i osiągnąć wysoką wydajność) w porównaniu z C ++.
nekromanta

wow, zapiszę to gdzieś, gdzie java osiąga wysoką wydajność w stosunku do cpp ..
jungle_mole

1
@jungle_mole tak, dziękuję. niewiele osób zdaje sobie sprawę z korzyści płynących z posiadania kompilatora dostępnego w czasie wykonywania, aby ponownie skompilować kod po profilowaniu. (lub że zbieranie śmieci jest wielkie-och (nie-śmieci), a nie naiwne i niepoprawne przekonanie, że zbieranie śmieci jest duże-och (śmieci), (lub że chociaż zbieranie śmieci jest niewielkim wysiłkiem, rekompensuje to, dopuszczając zerowe koszty przydział)). To smutny świat, w którym ludzie ślepo przyjmują to, w co wierzy ich społeczność, i przestają rozumować od podstawowych zasad.
nekromanta

0

Większość odpowiedzi dotyczy bardziej filozofii programowania niż rzeczywistych szczegółów technicznych.

I chociaż to pytanie ma więcej niż 5 lat, nadal pozostaje pytanie: dlaczego z technicznego punktu widzenia pożądane jest usuwanie czcionek? Ostatecznie odpowiedź jest raczej prosta (na wyższym poziomie): https://en.wikipedia.org/wiki/Type_erasure

Szablony C ++ nie istnieją w czasie wykonywania. Kompilator emituje w pełni zoptymalizowaną wersję dla każdego wywołania, co oznacza, że ​​wykonanie nie zależy od informacji o typie. Ale jak JIT radzi sobie z różnymi wersjami tej samej funkcji? Czy nie byłoby lepiej mieć tylko jedną funkcję? Nie chciałbym, aby JIT musiał optymalizować wszystkie jego różne wersje. Cóż, ale co z bezpieczeństwem typów? Zgadnij, że to musi wyjść przez okno.

Ale chwileczkę: jak to robi .NET? Odbicie! W ten sposób muszą zoptymalizować tylko jedną funkcję, a także uzyskać informacje o typie środowiska wykonawczego. I dlatego generyczne .NET były kiedyś wolniejsze (chociaż stały się znacznie lepsze). Nie twierdzę, że to nie jest wygodne! Ale jest drogi i nie powinien być używany, gdy nie jest to absolutnie konieczne (nie jest uważany za drogi w językach dynamicznie typowanych, ponieważ kompilator / interpreter i tak opiera się na refleksji).

W ten sposób programowanie ogólne z wymazywaniem typu jest bliskie zeru (niektóre sprawdzenia / rzutowania w czasie wykonywania są nadal wymagane): https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.