Dlaczego powinno mnie obchodzić, że Java nie ma reified generics?

102

Pojawiło się jako pytanie, które zadałem niedawno w wywiadzie, jako coś, co kandydat chciał, aby zostało dodane do języka Java. Powszechnie uważa się, że problem z tym, że Java nie ma zreifikowanych typów generycznych, ale gdy został popchnięty, kandydat nie mógł mi powiedzieć, jakie rzeczy mógłby osiągnąć, gdyby tam byli.

Oczywiście, ponieważ surowe typy są dozwolone w Javie (i niebezpieczne testy), możliwe jest obalenie typów generycznych i otrzymanie List<Integer>tego (na przykład) faktycznie zawiera Strings. Oczywiście mogłoby to być niemożliwe, gdyby informacje o typie zostały zreifikowane; ale musi być coś więcej !

Czy ludzie mogliby publikować przykłady rzeczy, które naprawdę chcieliby zrobić , gdyby dostępne były reifikowane leki generyczne? To znaczy, oczywiście możesz uzyskać typ a Listw czasie wykonywania - ale co byś z tym zrobił?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDYCJA : Szybka aktualizacja, ponieważ odpowiedzi wydają się być głównie zaniepokojone potrzebą przekazania a Classjako parametru (na przykład EnumSet.noneOf(TimeUnit.class)). Szukałem czegoś podobnego do tego, gdzie to po prostu nie jest możliwe . Na przykład:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?
oxbow_lakes
źródło
Czy oznaczałoby to, że w czasie wykonywania można uzyskać klasę typu ogólnego? (jeśli tak, mam przykład!)
James B
Myślę, że większość pragnień związanych z reifable generykami pochodzi od ludzi, którzy używają generyków głównie w kolekcjach i chcą, aby te kolekcje zachowywały się bardziej jak tablice.
kdgregory
2
Bardziej interesujące pytanie (dla mnie): co należałoby zrobić, aby zaimplementować elementy generyczne stylu C ++ w Javie? Z pewnością wydaje się to możliwe w czasie wykonywania, ale zepsułoby wszystkie istniejące programy ładujące klasy (ponieważ findClass()musiałoby zignorować parametryzację, ale defineClass()nie mógł). A jak wiemy, moce, które są, są najważniejsze, aby zachować zgodność wsteczną.
kdgregory
1
W rzeczywistości Java dostarcza reified generics w bardzo ograniczony sposób . Więcej szczegółów podam w tym wątku SO: stackoverflow.com/questions/879855/…
Richard Gomes.
JavaOne Keynote wskazuje, że Java 9 wesprze reifikacji.
Rangi Keen

Odpowiedzi:

81

Od kilku razy, kiedy natrafiłem na tę „potrzebę”, ostatecznie sprowadza się ona do tej konstrukcji:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

To działa w C # przy założeniu, że Tma domyślnego konstruktora. Możesz nawet pobrać typ środowiska wykonawczego typeof(T)i konstruktory przez Type.GetConstructor().

Typowym rozwiązaniem w Javie byłoby przekazanie Class<T>argumentu as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(nie musi być koniecznie przekazywany jako argument konstruktora, ponieważ argument metody jest również w porządku, powyższe jest tylko przykładem, również try-catchjest pomijane dla zwięzłości)

W przypadku wszystkich innych konstrukcji typu ogólnego rzeczywisty typ można łatwo rozwiązać przy niewielkiej pomocy refleksji. Poniższe pytania i odpowiedzi ilustrują przypadki użycia i możliwości:

BalusC
źródło
5
To działa (na przykład) w C #? Skąd wiesz, że T ma domyślnego konstruktora?
Thomas Jung
4
Tak, to prawda. A to tylko podstawowy przykład (załóżmy, że pracujemy z javabeanami). Chodzi o to, że w Javie nie można pobrać klasy w czasie wykonywania przez T.classlub T.getClass(), aby mieć dostęp do wszystkich jej pól, konstruktorów i metod. Uniemożliwia również budowę.
BalusC
3
Wydaje mi się to bardzo słabe jako „duży problem”, zwłaszcza, że ​​może się przydać tylko w połączeniu z bardzo kruchą refleksją wokół konstruktorów / parametrów itp.
oxbow_lakes
33
Kompiluje się w C # pod warunkiem, że zadeklarujesz typ jako: public class Foo <T>, gdzie T: new (). Co ograniczy prawidłowe typy T do tych, które zawierają konstruktor bez parametrów.
Martin Harris,
3
@Colin W rzeczywistości możesz powiedzieć javie, że dany typ ogólny jest wymagany do rozszerzenia więcej niż jednej klasy lub interfejsu. Zobacz sekcję „Multiple Bounds” na docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Oczywiście nie można powiedzieć, że obiekt będzie jednym z określonego zestawu, który nie będzie współdzielił metod, które można by wyabstrahować w interfejsie, co może być w pewnym stopniu zaimplementowane w C ++ przy użyciu specjalizacji szablonowej.)
JAB
99

To, co najczęściej mnie gryzie, to niemożność skorzystania z wielokrotnych wysyłek w wielu typach ogólnych. Poniższe nie są możliwe i istnieje wiele przypadków, w których byłoby to najlepsze rozwiązanie:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }
RHSeeger
źródło
5
Nie ma absolutnie potrzeby reifikacji, aby móc to zrobić. Wybór metody jest wykonywany w czasie kompilacji, gdy dostępne są informacje o typie kompilacji.
Tom Hawtin - tackline
26
@Tom: to nawet się nie kompiluje z powodu wymazania typu. Oba są kompilowane jako public void my_method(List input) {}. Jednak nigdy nie spotkałem się z tą potrzebą, po prostu dlatego, że nie mieliby tej samej nazwy. Jeśli mają to samo imię, zapytałbym, czy public <T extends Object> void my_method(List<T> input) {}nie jest to lepszy pomysł.
BalusC
1
Hm, starałbym się unikać przeładowania identyczną liczbą parametrów w ogóle i wolałbym coś takiego, myStringsMethod(List<String> input)a myIntegersMethod(List<Integer> input)nawet gdyby przeciążenie w takim przypadku było możliwe w Javie.
Fabian Steeg
4
@Fabian: Co oznacza, że ​​musisz mieć oddzielny kod i zapobiegać tego rodzaju korzyściom, jakie daje <algorithm>C ++.
David Thornley
Jestem zdezorientowany, to w zasadzie to samo, co mój drugi punkt, ale mam 2 głosy przeciw? Czy ktoś chce mnie oświecić?
rsp
34

Przychodzi mi na myśl bezpieczeństwo typów . Downcasting do sparametryzowanego typu zawsze będzie niebezpieczny bez reified generics:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Ponadto abstrakcje wyciekałyby mniej - przynajmniej te, które mogą być zainteresowane informacjami w czasie wykonywania o ich parametrach typu. Dziś, jeśli potrzebujesz jakichkolwiek informacji o typie jednego z parametrów ogólnych, musisz je również przekazać Class. W ten sposób twój zewnętrzny interfejs zależy od twojej implementacji (czy używasz RTTI do swoich parametrów, czy nie).

gustafc
źródło
1
Tak - mam na to sposób, ponieważ tworzę plik, ParametrizedListktóry kopiuje dane w kolekcji źródłowej, sprawdzając typy. To trochę jak, Collections.checkedListale na początek można je obsiać kolekcją.
oxbow_lakes
@tackline - cóż, kilka abstrakcji wyciekłoby mniej. Jeśli potrzebujesz dostępu do wpisywania metadanych w swojej implementacji, zewnętrzny interfejs poinformuje Cię o tym, ponieważ klienci muszą wysłać Ci obiekt klasy.
gustafc
... co oznacza, że ​​w przypadku reified generics można dodawać rzeczy takie jak T.class.getAnnotation(MyAnnotation.class)(gdzie Tjest typem ogólnym) bez zmiany interfejsu zewnętrznego.
gustafc
@gustafc: jeśli myślisz, że szablony C ++ zapewniają pełne bezpieczeństwo typów, przeczytaj to: kdgregory.com/index.php?page=java.generics.cpp
kdgregory
@kdgregory: Nigdy nie powiedziałem, że C ++ jest w 100% bezpieczny dla typów - tylko to wymazanie szkodzi bezpieczeństwu typów. Jak sam mówisz, „Okazuje się, że C ++ ma swoją własną formę wymazywania typu, znaną jako rzutowanie wskaźnika w stylu C.” Ale Java wykonuje tylko rzutowania dynamiczne (nie reinterpretuje), więc reifikacja włączyłaby to wszystko do systemu typów.
gustafc
26

Byłbyś w stanie tworzyć ogólne tablice w swoim kodzie.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}
Turnor
źródło
co jest nie tak z Object? Tablica obiektów i tak jest tablicą odniesień. To nie jest tak, że dane obiektu znajdują się na stosie - wszystko jest na stercie.
Ran Biron
19
Bezpieczeństwo typów. Mogę umieścić cokolwiek zechcę w Object [], ale tylko Strings w String [].
Turnor
3
Ran: Bez sarkastów: możesz chcieć używać języka skryptowego zamiast Javy, wtedy masz elastyczność bez typu zmiennych wszędzie!
latające owce
2
Tablice są kowariantne (a zatem nie są bezpieczne dla typów) w obu językach. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Kompiluje się dobrze w obu językach. Działa niezbyt dobrze.
Martijn,
16

To jest stare pytanie, jest mnóstwo odpowiedzi, ale myślę, że istniejące odpowiedzi są chybione.

„zreifikowany” oznacza po prostu prawdziwy i zwykle oznacza po prostu przeciwieństwo wymazania typu.

Duży problem związany z generykami Java:

  • To okropne wymaganie dotyczące boksu i rozłączenie między typami pierwotnymi i referencyjnymi. Nie jest to bezpośrednio związane z reifikacją ani usuwaniem typów. C # / Scala napraw to.
  • Żadnych typów jaźni. Z tego powodu JavaFX 8 musiał usunąć „budowniczych”. Absolutnie nie ma to nic wspólnego z usuwaniem czcionek. Scala rozwiązuje ten problem, nie jestem pewien co do C #.
  • Brak wariancji typu po stronie deklaracji. C # 4.0 / Scala mają to. Absolutnie nie ma to nic wspólnego z usuwaniem typów.
  • Nie można przeciążać void method(List<A> l)i method(List<B> l). Wynika to z wymazywania czcionek, ale jest bardzo drobiazgowe.
  • Brak obsługi odbicia typu środowiska uruchomieniowego. To jest sedno usuwania czcionek. Jeśli lubisz super zaawansowane kompilatory, które weryfikują i udowadniają jak najwięcej logiki programu w czasie kompilacji, powinieneś używać refleksji tak mało, jak to możliwe, a tego typu wymazywanie nie powinno Ci przeszkadzać. Jeśli lubisz bardziej niejednolite, skryptowe, dynamiczne programowanie typów i nie przejmujesz się zbytnio kompilatorem, który udowadnia, że ​​twoja logika jest poprawna w jak największym stopniu, to chcesz mieć lepszą refleksję i naprawienie wymazywania typów jest ważne.
user2684301
źródło
1
Przeważnie jest to trudne w przypadku serializacji. Często chciałbyś mieć możliwość wyszukania typów klas ogólnych rzeczy, które są serializowane, ale jesteś zatrzymywany krótko z powodu wymazywania typów. To utrudnia zrobienie czegoś takiegodeserialize(thingy, List<Integer>.class)
Cogman,
3
Myślę, że to najlepsza odpowiedź. Zwłaszcza części opisujące, jakie problemy są tak naprawdę fundamentalnie spowodowane wymazywaniem typów i jakie są tylko problemy z projektowaniem języka Java. Pierwszą rzeczą, którą mówię ludziom zaczynającym narzekanie na wymazywanie, jest to, że Scala i Haskell również pracują według typu.
aemxdp
13

Serializacja byłaby prostsza w przypadku reifikacji. To, czego chcielibyśmy, to

deserialize(thingy, List<Integer>.class);

Musimy zrobić

deserialize(thing, new TypeReference<List<Integer>>(){});

wygląda brzydko i działa dziwnie.

Są też przypadki, w których naprawdę pomocne byłoby powiedzenie czegoś takiego

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Te rzeczy nie gryzą często, ale gryzą, gdy się zdarzają.

Cogman
źródło
To. Tysiąc razy to. Za każdym razem, gdy próbuję napisać kod deserializacji w Javie, spędzam dzień narzekając, że nie pracuję nad czymś innym
Podstawowy
11

Moje narażenie na Java Geneircs jest dość ograniczone i poza punktami, o których wspomniały już inne odpowiedzi, istnieje scenariusz wyjaśniony w książce Java Generics and Collections , autorstwa Maurice'a Naftalina i Philipa Waldera, w której użyteczne są reifikowane typy generyczne.

Ponieważ typów nie można reifable, nie można mieć sparametryzowanych wyjątków.

Na przykład deklaracja poniższego formularza jest nieważna.

class ParametericException<T> extends Exception // compile error

Dzieje się tak, ponieważ klauzula catch sprawdza, czy zgłoszony wyjątek pasuje do danego typu. To sprawdzenie jest takie samo jak sprawdzenie przeprowadzane przez test instancji, a ponieważ typu nie można ponowić, powyższa forma instrukcji jest nieprawidłowa.

Gdyby powyższy kod był prawidłowy, obsługa wyjątków w poniższy sposób byłaby możliwa:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

W książce wspomniano również, że jeśli typy generyczne języka Java są zdefiniowane podobnie do sposobu definiowania szablonów C ++ (rozszerzenie), może to prowadzić do wydajniejszej implementacji, ponieważ oferuje to więcej możliwości optymalizacji. Ale nie oferuje więcej wyjaśnień, więc wszelkie wyjaśnienia (wskazówki) od znających się na rzeczy ludzi byłyby pomocne.

sateesh
źródło
1
To ważny punkt, ale nie jestem do końca pewien, dlaczego tak sparametryzowana klasa wyjątku byłaby przydatna. Czy mógłbyś zmodyfikować swoją odpowiedź, aby zawierała krótki przykład, kiedy może to być przydatne?
oxbow_lakes
@oxbow_lakes: Przepraszamy. Moja wiedza na temat generycznych Java jest dość ograniczona i próbuję ją ulepszyć. Więc teraz nie jestem w stanie wymyślić żadnego przykładu, w którym sparametryzowany wyjątek mógłby być przydatny. Spróbuję o tym pomyśleć. Dzięki.
sateesh
Może działać jako substytut wielokrotnego dziedziczenia typów wyjątków.
meriton
Ulepszenie wydajności polega na tym, że obecnie parametr typu musi dziedziczyć po Object, wymagając pakowania typów pierwotnych, co narzuca narzut wykonania i pamięci.
meriton
8

Tablice prawdopodobnie działałyby znacznie lepiej z rodzajami, gdyby były reifikowane.

Hank Gay
źródło
2
Jasne, ale nadal mieliby problemy. List<String>nie jest List<Object>.
Tom Hawtin - tackline
Zgadzam się - ale tylko dla prymitywów (Integer, Long itp.). W przypadku „zwykłego” obiektu jest tak samo. Ponieważ prymitywy nie mogą być sparametryzowanym typem (znacznie poważniejszy problem, przynajmniej IMHO), nie uważam tego za prawdziwy ból.
Ran Biron
3
Problem z tablicami polega na ich kowariancji, nie ma nic wspólnego z reifikacją.
Powtórzenie
5

Mam opakowanie, które przedstawia zestaw wyników jdbc jako iterator (oznacza to, że mogę dużo łatwiej testować jednostkowe operacje pochodzące z bazy danych dzięki iniekcji zależności).

Wygląda na to API Iterator<T> jest typem, który można skonstruować przy użyciu tylko ciągów znaków w konstruktorze. Następnie Iterator sprawdza ciągi zwracane przez zapytanie sql, a następnie próbuje dopasować je do konstruktora typu T.

W obecnym sposobie implementacji typów generycznych muszę również przekazać klasę obiektów, które będę tworzyć z mojego zestawu wyników. Jeśli dobrze rozumiem, gdyby typy generyczne zostały zreifikowane, mógłbym po prostu wywołać funkcję T.getClass (), aby uzyskać jej konstruktory, a następnie nie musieć rzutować wyniku Class.newInstance (), co byłoby znacznie bardziej estetyczne.

Zasadniczo uważam, że ułatwia pisanie interfejsów API (w przeciwieństwie do zwykłego pisania aplikacji), ponieważ z obiektów można wywnioskować znacznie więcej, a tym samym mniej konfiguracji będzie potrzebnych ... Nie doceniałem implikacji adnotacji, dopóki nie widziałem ich użycie w rzeczach takich jak spring lub xstream zamiast ryz w config.

James B.
źródło
Ale przechodzenie w klasie wydaje mi się bardziej bezpieczne. W każdym razie refleksyjne tworzenie wystąpień z zapytań bazy danych jest niezwykle kruche w przypadku zmian, takich jak refaktoryzacja i tak (zarówno w kodzie, jak iw bazie danych). Chyba szukałem rzeczy, przez które po prostu nie da się zapewnić zajęć
oxbow_lakes
5

Jedną z fajnych rzeczy byłoby unikanie boksu dla typów pierwotnych (wartościowych). Jest to w pewnym stopniu związane z zarzutem dotyczącym tablicy, który podnieśli inni, aw przypadkach, gdy użycie pamięci jest ograniczone, może to faktycznie spowodować znaczącą różnicę.

Istnieje również kilka typów problemów podczas pisania frameworka, w których ważna jest możliwość odzwierciedlenia sparametryzowanego typu. Oczywiście można to obejść, przekazując obiekt klasy w czasie wykonywania, ale to przesłania API i nakłada dodatkowe obciążenie na użytkownika frameworka.

kvb
źródło
2
Zasadniczo sprowadza się to do możliwości zrobienia tego, new T[]gdzie T jest typu prymitywnego!
oxbow_lakes
2

Nie chodzi o to, że osiągniesz coś niezwykłego. Po prostu łatwiej będzie to zrozumieć. Wymazywanie typów wydaje się być trudnym czasem dla początkujących i ostatecznie wymaga zrozumienia sposobu działania kompilatora.

Uważam, że generyczne są po prostu dodatkami, które oszczędzają wiele zbędnych rzutów.

Bozho
źródło
To z pewnością są generyczne w Javie . W języku C # i innych językach to potężne narzędzie
Basic
1

Coś, czego pominęły wszystkie odpowiedzi tutaj, a co jest dla mnie nieustannie bólem głowy, jest takie, że ponieważ typy są usuwane, nie można dwukrotnie dziedziczyć ogólnego interfejsu. Może to stanowić problem, gdy chcesz tworzyć precyzyjne interfejsy.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!
ExCodeCowboy
źródło
0

Oto jeden, który mnie dzisiaj złapał: bez reifikacji, jeśli napiszesz metodę, która akceptuje listę elementów ogólnych typu varargs ... wywołujący mogą MYŚLIĆ, że są bezpieczni dla typów, ale przypadkowo przekazują jakąkolwiek starą crud i wysadzają twoją metodę.

Wydaje się mało prawdopodobne, że tak się stanie? ... Jasne, dopóki ... nie użyjesz Class jako swojego typu danych. W tym momencie dzwoniący z radością wyśle ​​Ci wiele obiektów Class, ale prosta literówka wyśle ​​Ci obiekty Class, które nie są zgodne z T, i zdarzy się katastrofa.

(Uwaga: być może popełniłem tutaj błąd, ale przeglądając w Google „warargi generyczne”, powyższe wydaje się być właśnie tym, czego można się spodziewać. Rzeczą, która sprawia, że ​​jest to praktyczny problem, jest, jak sądzę, korzystanie z Class - wydaje się, że rozmówcy być mniej ostrożnym :()


Na przykład używam paradygmatu, który używa obiektów klasy jako klucza w mapach (jest bardziej złożony niż prosta mapa - ale koncepcyjnie tak właśnie się dzieje).

np. to działa świetnie w Java Generics (trywialny przykład):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

np. bez reifikacji w Java Generics, ten akceptuje KAŻDY obiekt "Class". A to tylko malutkie rozszerzenie poprzedniego kodu:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Powyższe metody muszą być zapisywane tysiące razy w indywidualnym projekcie - więc prawdopodobieństwo błędu ludzkiego staje się wysokie. Błędy w debugowaniu okazują się „niefajne”. Obecnie próbuję znaleźć alternatywę, ale nie mam nadziei.

Adam
źródło