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ą:

Zobacz też

  1. ^ Język programowania Java
  2. ^ 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) .
  3. ^ GJ: Ogólna Java
  4. ^ Specyfikacja języka Java, wydanie trzecie, James Gosling, Bill Joy, Guy Steele, Gilad Bracha - Prentice Hall PTR 2005
  5. ^ Gilad Bracha (5 lipca 2004). „Ogólne w języku programowania Java” (PDF) . www.oracle.com .
  6. ^ Gilad Bracha (5 lipca 2004). „Ogólne w języku programowania Java” (PDF) . www.oracle.com . P. 5.
  7. Bibliografia _ _ „Wildcards > Bonus > Generics” . Samouczki Java™ . Wyrocznia. ... Jedynym wyjątkiem jest wartość null, która należy do każdego typu...
  8. ^ „Wnioskowanie o typie do tworzenia instancji ogólnych” .
  9. ^ Gafter, Neal (2006-11-05). „Reified Generics for Java” . Źródło 2010-04-20 .
  10. ^ „Specyfikacja języka Java, sekcja 8.1.2” . Wyrocznia . Źródło 24 października 2015 r .
  11. Bibliografia _ „Witamy w Walhalli!” . Archiwum poczty OpenJDK . OpenJDK . Źródło 12 sierpnia 2014 r .