Generyki w Javie
Rodzaje to narzędzie programowania ogólnego , które zostało dodane do języka programowania Java w 2004 roku w wersji J2SE 5.0. Zostały zaprojektowane w celu rozszerzenia systemu typów Javy , aby umożliwić „typowi lub metodzie działanie na obiektach różnych typów, zapewniając jednocześnie bezpieczeństwo typów w czasie kompilacji”. Aspekt bezpieczeństwa typu czasu kompilacji nie został w pełni osiągnięty, ponieważ w 2016 roku wykazano, że nie jest on gwarantowany we wszystkich przypadkach.
Środowisko kolekcji Java obsługuje typy generyczne w celu określenia typu obiektów przechowywanych w instancji kolekcji.
W 1998 roku Gilad Bracha , Martin Odersky , David Stoutamire i Philip Wadler stworzyli Generic Java, rozszerzenie języka Java obsługujące typy ogólne. Generic Java została włączona do Java z dodatkiem symboli wieloznacznych .
Hierarchia i klasyfikacja
Zgodnie ze specyfikacją języka Java :
- Zmienna typu jest niekwalifikowanym identyfikatorem. Zmienne typu są wprowadzane przez ogólne deklaracje klas, ogólne deklaracje interfejsów, ogólne deklaracje metod i ogólne deklaracje konstruktorów.
- Klasa jest ogólna , jeśli deklaruje jedną lub więcej zmiennych typu. Te zmienne typu są znane jako parametry typu klasy. Definiuje jedną lub więcej zmiennych typu, które działają jako parametry. Deklaracja klasy ogólnej definiuje zestaw typów sparametryzowanych, po jednym dla każdego możliwego wywołania sekcji parametrów typu. Wszystkie te sparametryzowane typy współdzielą tę samą klasę w czasie wykonywania.
- Interfejs jest ogólny , jeśli deklaruje jedną lub więcej zmiennych typu. Te zmienne typu są znane jako parametry typu interfejsu. Definiuje jedną lub więcej zmiennych typu, które działają jako parametry. Ogólna deklaracja interfejsu definiuje zestaw typów, po jednym dla każdego możliwego wywołania sekcji parametru typu. Wszystkie sparametryzowane typy współdzielą ten sam interfejs w czasie wykonywania.
- Metoda jest ogólna , jeśli deklaruje jedną lub więcej zmiennych typu. Te zmienne typu są znane jako formalne parametry typu metody. Forma formalnej listy parametrów typu jest identyczna z listą parametrów typu klasy lub interfejsu.
- Konstruktor można zadeklarować jako generyczny, niezależnie od tego, czy klasa, w której konstruktor jest zadeklarowany, jest generyczna . Konstruktor jest ogólny, jeśli deklaruje jedną lub więcej zmiennych typu. Te zmienne typu są znane jako formalne parametry typu konstruktora. Forma listy parametrów typu formalnego jest identyczna z listą parametrów typu klasy ogólnej lub interfejsu.
Motywacja
Poniższy blok kodu Java ilustruje problem, który występuje, gdy nie są używane generyczne. Najpierw deklaruje ArrayList
typu Object
. Następnie dodaje String
do ArrayList
. Na koniec próbuje pobrać dodany ciąg
i rzucić go na liczbę całkowitą
— błąd logiczny, ponieważ generalnie nie jest możliwe rzutowanie dowolnego ciągu na liczbę całkowitą.
0 Lista v = nowa lista tablic (); w . dodaj ( "test" ); // Łańcuch, którego nie można rzutować na liczbę całkowitą Integer i = ( Integer ) v . dostać ( ); // Błąd czasu wykonywania
Chociaż kod jest kompilowany bez błędów, zgłasza wyjątek środowiska wykonawczego ( java.lang.ClassCastException
) podczas wykonywania trzeciego wiersza kodu. Ten typ błędu logicznego można wykryć podczas kompilacji za pomocą typów ogólnych i jest to główna motywacja do ich używania.
Powyższy fragment kodu można przepisać przy użyciu generycznych w następujący sposób:
0 Lista < Łańcuch > v = nowy ArrayList < Łańcuch > (); w . dodaj ( "test" ); liczba całkowita i = ( liczba całkowita ) v . dostać ( ); // (błąd typu) błąd czasu kompilacji
Parametr typu String
w nawiasach ostrych deklaruje, że ArrayList
składa się z String
(potomka ogólnych składników Object klasy
ArrayList
). W przypadku generycznych nie jest już konieczne rzutowanie trzeciego wiersza na konkretny typ, ponieważ wynik v.get(0)
jest zdefiniowany jako String
przez kod generowany przez kompilator.
Błąd logiczny w trzecim wierszu tego fragmentu zostanie wykryty jako błąd w czasie kompilacji (z J2SE 5.0 lub nowszym), ponieważ kompilator wykryje, że v.get(0)
zwraca String
zamiast Integer
. Bardziej rozbudowany przykład można znaleźć w odnośniku.
Oto mały fragment definicji interfejsów List
i Iterator
w pakiecie java.util
:
lista interfejsów publicznych < E > { void add ( E x ); Iterator < E > iterator (); } publiczny interfejs Iterator < E > { E następny (); wartość logiczna hasNext (); }
Wpisz symbole wieloznaczne
Argument typu dla typu sparametryzowanego nie jest ograniczony do konkretnej klasy lub interfejsu. Java pozwala na użycie symboli wieloznacznych typu jako argumentów typu dla typów sparametryzowanych. Symbole wieloznaczne to argumenty typu w postaci " <?>
"; opcjonalnie z górną lub dolną granicą . Biorąc pod uwagę, że dokładny typ reprezentowany przez symbol wieloznaczny jest nieznany, nakładane są ograniczenia na typ metod, które mogą być wywoływane na obiekcie, który używa typów sparametryzowanych.
Oto przykład, w którym typ elementu Collection<E>
jest sparametryzowany za pomocą symbolu wieloznacznego:
Kolekcja <?> c = new ArrayList < String > (); do . dodaj ( nowy obiekt ()); // błąd czasu kompilacji c . dodaj ( null ); // dozwolony
Ponieważ nie wiemy, co oznacza typ elementu c , nie możemy dodawać do niego obiektów.
Metoda add()
przyjmuje argumenty typu E
, typu elementu ogólnego interfejsu Collection<E>
. Kiedy rzeczywistym argumentem typu jest ?
, oznacza jakiś nieznany typ. Każda wartość argumentu metody, którą przekazujemy add()
musiałaby być podtypem tego nieznanego typu. Ponieważ nie wiemy, jaki to typ, nie możemy niczego przekazać. Jedynym wyjątkiem jest null ; który jest członkiem każdego typu.
Aby określić górną granicę symbolu wieloznacznego typu, słowo kluczowe extends
służy do wskazania, że argument typu jest podtypem klasy ograniczającej. Więc lista<? extends Number>
oznacza, że dana lista zawiera obiekty nieznanego typu, które rozszerzają klasę Number
. Na przykład lista może mieć postać List<Float>
lub List<Number>
. Odczytanie elementu z listy zwróci Number
. Dodawanie elementów zerowych jest również dozwolone.
Użycie powyższych symboli wieloznacznych zwiększa elastyczność, ponieważ nie ma żadnej relacji dziedziczenia między dowolnymi dwoma sparametryzowanymi typami z konkretnym typem jako argumentem typu. Ani List<Number>
ani List<Integer>
nie jest podtypem drugiego; mimo że Integer
jest podtypem Number
. Tak więc żadna metoda, która przyjmuje List<Number>
jako parametr, nie akceptuje argumentu List<Integer>
. Gdyby tak było, możliwe byłoby wstawienie liczby
, która nie jest liczbą całkowitą
w tym; co narusza bezpieczeństwo typów. Oto przykład pokazujący, w jaki sposób naruszono by bezpieczeństwo typu, gdyby List<Integer>
był podtypem List<Number>
:
Lista < liczba całkowita > ints = nowa lista tablic < liczba całkowita > (); int . dodać ( 2 ); Lista < Liczba > nums = ints ; // poprawna, jeśli List<Integer> była podtypem List<Number> zgodnie z regułą podstawienia. liczby . dodać ( 3.14 ); Liczba całkowita x = liczba całkowita . Dostawać ( 1 ); // teraz 3.14 jest przypisane do zmiennej typu Integer!
Rozwiązanie z symbolami wieloznacznymi działa, ponieważ uniemożliwia operacje, które naruszałyby bezpieczeństwo typu:
Lista <? rozszerza Liczbę > nums = ints ; // OK numery . dodać ( 3.14 ); // liczba błędów w czasie kompilacji . dodaj ( null ); // dozwolony
Aby określić dolną klasę ograniczającą symbolu wieloznacznego typu, używane jest słowo kluczowe super .
To słowo kluczowe wskazuje, że argument typu jest nadtypem klasy ograniczającej. Więc, List<? super Number>
może reprezentować List<Number>
lub List<Object>
. Czytanie z listy zdefiniowanej jako List<? super Number>
zwraca elementy typu Object
. Dodanie do takiej listy wymaga elementów typu Number
, dowolnego podtypu Number
lub null (który jest członkiem każdego typu).
z książki Effective Java Joshua Blocha daje łatwy sposób na zapamiętanie, kiedy używać symboli wieloznacznych (odpowiadających kowariancji i kontrawariancji ) w Javie.
Ogólne definicje klas
Oto przykład ogólnej klasy Java, której można użyć do reprezentowania poszczególnych wpisów (odwzorowań klucza na wartość) na mapie :
wpis klasy publicznej < typ klucza , typ wartości > { prywatny końcowy klucz typu klucza ; prywatna końcowa wartość typu ValueType ; wpis publiczny ( klucz KeyType , wartość ValueType ) { to . klucz = klucz ; to . wartość = wartość ; } publiczne
Typ klucza getKey () { klucz powrotu ; } public ValueType getValue () { wartość zwracana ; } public String toString () { return "(" + klucz + ", " + wartość + ")" ; } }
Ta ogólna klasa może być używana na przykład w następujący sposób:
Wpis < Ciąg , Ciąg > ocena = nowy Wpis < Ciąg , Ciąg > ( "Mike" , "A" ); Entry < String , Integer > mark = new Entry < String , Integer > ( "Mike" , 100 ); systemu .
na zewnątrz println ( "ocena: " + ocena ); systemu . na zewnątrz println ( "znak: " + znak ); Wpis < Integer , Boolean > prime = new Entry < Integer , Boolean > ( 13 , true ); if ( prim . getValue
()) Układu . na zewnątrz println ( prim . getKey () + "jest liczbą pierwszą." ); inny system . na zewnątrz println ( prim . getKey () + "nie jest liczbą pierwszą." );
Wyprowadza:
ocena: (Mike, A) ocena: (Mike, 100) 13 to liczba pierwsza.
Operator diamentu
Dzięki wnioskowaniu o typie , Java SE 7 i nowsze wersje pozwalają programiście na zastąpienie pustej pary nawiasów ostrych ( <>
, zwanego operatorem rombowym ) pary nawiasów ostrych zawierających jeden lub więcej parametrów typu, które implikuje wystarczająco ścisły kontekst . Tak więc powyższy przykład kodu wykorzystujący Entry
można przepisać jako:
Wpis < Ciąg , Ciąg > ocena = nowy Wpis <> ( "Mike" , "A" ); Wpis < Ciąg znaków , Liczba całkowita > znak = nowy Wpis <> ( "Mike" , 100 ); systemu . na zewnątrz println ( "ocena: " + ocena
); systemu . na zewnątrz println ( "znak: " + znak ); Wpis < Liczba całkowita , Boolean > liczba pierwsza = nowy Wpis <> ( 13 , prawda ); if ( prime . getValue ()) System . na zewnątrz println ( prime . getKey () +
„jest pierwszorzędny”. ); inaczej System . na zewnątrz println ( prim . getKey () + "nie jest liczbą pierwszą." );
Ogólne definicje metod
Oto przykład metody ogólnej wykorzystującej powyższą klasę ogólną:
public static <Typ> Wpis < Typ , Typ > dwa razy ( Wpisz wartość ) { return new Entry < Typ , Typ > ( wartość , wartość ) ; }
Uwaga: Jeśli usuniemy pierwszy <Typ>
w powyższej metodzie, otrzymamy błąd kompilacji (nie można znaleźć symbolu „Typ”), ponieważ reprezentuje on deklarację symbolu.
W wielu przypadkach użytkownik metody nie musi wskazywać parametrów typu, gdyż można je wywnioskować:
Wpis < Ciąg , Ciąg > para = Wpis . dwa razy ( "Cześć" );
W razie potrzeby parametry można jawnie dodać:
Wpis < Ciąg , Ciąg > para = Wpis . < String > dwa razy ( "Cześć" );
Używanie typów pierwotnych jest niedozwolone, zamiast tego należy używać wersji pudełkowych :
Wpis < int , int > para ; // Kompilacja nie powiodła się. Zamiast tego użyj liczby całkowitej.
Istnieje również możliwość tworzenia metod generycznych na podstawie zadanych parametrów.
public <Typ> Wpisz [ ] toArray ( Typ ... elementy ) { zwracaj elementy ; _ }
W takich przypadkach nie można również używać typów pierwotnych, np.:
Liczba całkowita [] array = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );
Generics w klauzuli throws
Chociaż same wyjątki nie mogą być ogólne, parametry ogólne mogą pojawić się w klauzuli throws:
public < T extends Throwable > void throwMeConditional ( boolean warunkowy , T wyjątek ) throws T { if ( conditional ) { throw wyjątek ; } }
Problemy z usuwaniem czcionek
Generyki są sprawdzane w czasie kompilacji pod kątem poprawności typu. Informacje o typie ogólnym są następnie usuwane w procesie zwanym wymazywaniem typu . Na przykład List<Integer>
zostanie przekonwertowany na typ nieogólny List
, który zwykle zawiera dowolne obiekty. Kontrola w czasie kompilacji gwarantuje, że wynikowy kod jest poprawny pod względem typu.
Ze względu na wymazanie typu nie można określić parametrów typu w czasie wykonywania. Na przykład, gdy ArrayList
jest sprawdzany w czasie wykonywania, nie ma ogólnego sposobu określenia, czy przed wymazaniem typu był to ArrayList <Integer>
czy ArrayList<Float>
. Wiele osób jest niezadowolonych z tego ograniczenia. Istnieją podejścia częściowe. Na przykład poszczególne elementy mogą zostać zbadane w celu określenia typu, do którego należą; na przykład, jeśli ArrayList
zawiera Integer
, ta ArrayList mogła zostać sparametryzowana za pomocą Integer
(jednak mogła zostać sparametryzowana z dowolnym rodzicem Integer
, takim jak Number
lub Object
).
Demonstrując ten punkt, następujący kod wyświetla „Equal”:
ArrayList < Integer > li = new ArrayList < Integer > (); ArrayList < Float > lf = new ArrayList < Float > (); if ( li . getClass () == lf . getClass ()) { // zwraca wartość true System . na zewnątrz println (
„Równy” ); }
Innym skutkiem wymazania typu jest to, że klasa generyczna nie może w żaden sposób rozszerzać klasy Throwable, bezpośrednio lub pośrednio:
klasa publiczna GenericException < T > rozszerza wyjątek
Powodem, dla którego nie jest to obsługiwane, jest wymazanie typu:
spróbuj { wyrzuć nowy GenericException <Integer> ( ) ; } catch ( GenericException < Integer > e ) { System . błąd . println ( "liczba całkowita" ); } catch ( GenericException < String > e ) { System . błąd . println
( "Ciąg znaków" ); }
Ze względu na wymazanie typu środowisko wykonawcze nie będzie wiedziało, który blok catch wykonać, więc jest to zabronione przez kompilator.
Typy generyczne Javy różnią się od szablonów C++ . Generyki Java generują tylko jedną skompilowaną wersję klasy ogólnej lub funkcji, niezależnie od liczby użytych typów parametryzujących. Ponadto środowisko wykonawcze Java nie musi wiedzieć, który typ sparametryzowany jest używany, ponieważ informacje o typie są sprawdzane w czasie kompilacji i nie są uwzględniane w kompilowanym kodzie. W związku z tym tworzenie instancji klasy Java typu sparametryzowanego jest niemożliwe, ponieważ tworzenie instancji wymaga wywołania konstruktora, który jest niedostępny, jeśli typ jest nieznany.
Na przykład następujący kod nie może zostać skompilowany:
< T > T instantiateElementType ( List < T > arg ) { return new T (); //powoduje błąd kompilacji }
Ponieważ w czasie wykonywania istnieje tylko jedna kopia na klasę generyczną, zmienne statyczne są współużytkowane przez wszystkie instancje klasy, niezależnie od ich parametru typu. W związku z tym parametr typu nie może być używany w deklaracji zmiennych statycznych ani w metodach statycznych.
Projekt dotyczący leków generycznych
Project Valhalla to projekt eksperymentalny mający na celu inkubację ulepszonych typów ogólnych i funkcji języka Java dla przyszłych wersji, potencjalnie począwszy od wersji Java 10. Potencjalne ulepszenia obejmują:
- specjalizacja ogólna , np. List<int>
- zreifikowane leki generyczne ; udostępnianie rzeczywistych typów w czasie wykonywania.
Zobacz też
- Programowanie ogólne
- Metaprogramowanie szablonów
- Symbol wieloznaczny (Java)
- Porównanie C# i Javy
- Porównanie Javy i C++
- ^ Język programowania Java
- ^ Wyjątek ClassCastException można zgłosić nawet w przypadku braku rzutowań lub wartości null. „Systemy typów Java i Scali są wadliwe” (PDF) .
- ^ GJ: Ogólna Java
- ^ Specyfikacja języka Java, wydanie trzecie, James Gosling, Bill Joy, Guy Steele, Gilad Bracha - Prentice Hall PTR 2005
- ^ Gilad Bracha (5 lipca 2004). „Ogólne w języku programowania Java” (PDF) . www.oracle.com .
- ^ Gilad Bracha (5 lipca 2004). „Ogólne w języku programowania Java” (PDF) . www.oracle.com . P. 5.
-
Bibliografia
_ _ „Wildcards > Bonus > Generics” . Samouczki Java™ . Wyrocznia.
... Jedynym wyjątkiem jest wartość null, która należy do każdego typu...
- ^ „Wnioskowanie o typie do tworzenia instancji ogólnych” .
- ^ Gafter, Neal (2006-11-05). „Reified Generics for Java” . Źródło 2010-04-20 .
- ^ „Specyfikacja języka Java, sekcja 8.1.2” . Wyrocznia . Źródło 24 października 2015 r .
- Bibliografia _ „Witamy w Walhalli!” . Archiwum poczty OpenJDK . OpenJDK . Źródło 12 sierpnia 2014 r .