Generyki języka Java wyjaśnione w skrócie z przykładami

Aby uzyskać więcej informacji, zobacz często zadawane pytania:

http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html#SECTION1

Oto w skrócie kilka ważnych punktów z tego FAQ.

Przykład (typu ogólnego):

class Pair<X,Y>  { 
  private X first;
  private Y second;   public Pair(X a1, Y a2) {
    first  = a1;
    second = a2;
  }
  public X getFirst()  { return first; }
  public Y getSecond() { return second; }
  public void setFirst(X arg)  { first = arg; }
  public void setSecond(Y arg) { second = arg; }
}

Klasa Para ma dwa parametry typu X I Y .  Są one zastępowane argumentami typu, gdy jest to typ ogólny Para jest instancja. Na przykład w deklaracji Para<String, Data> parametr typu X zostaje zastąpiony argumentem typu Smyczkowy I Y is replaced by  Data .
Zakres identyfikatorów X I Y Jest całą definicję klasy.  W tym zakresie dwa parametry typu X I Y są używane tak, jak były typami (z pewnymi ograniczeniami). W powyższym przykładzie parametry typu służą jako argument i typ zwracany metod instancji oraz typy pól instancji.
Parametry typu można deklarować z granicami. Granice dają dostęp do metod nieznanego typu, który reprezentuje parametr type. W naszym przykładzie nie wywołujemy żadnych metod nieznanego typu X I Y . Z tego powodu te dwa parametry typu są nieograniczone.

Przykład (konkretnego typu sparametryzowanego):

publiczna nieważna printPair( Para<String,Long> para) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Para<String,Long> limit = nowy Para<String,Long> („maksimum”, 1024L);
printPair(limit);

Przykład (typu sparametryzowanego za pomocą symboli wieloznacznych):

publiczna nieważna printPair( Para<?,?> para) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Para<?,?> limit = nowy Para<String,Long> („maksimum”, 1024L);
printPair(limit);

Wszystkie wystąpienia typu ogólnego mają tę samą reprezentację typu środowiska wykonawczego, a mianowicie reprezentację typu surowego. Na przykład instancje typu ogólnego Lista , takie jak Lista<Data> , Lista<String> , Lista<Długi> itp. mają różne typy statyczne w czasie kompilacji, ale ten sam typ dynamiczny Lista w czasie wykonywania.

Obsada składa się z dwóch części:

  • statyczna kontrola typu wykonywana przez kompilator w czasie kompilacji i
  • dynamiczna kontrola typu wykonywana przez maszynę wirtualną w czasie wykonywania.

Część statyczna sortuje bezsensowne rzuty, które nie mogą się powieść, takie jak rzutowanie z Smyczkowy To Data lub z Lista<String> To Lista<Data> .

Część dynamiczna wykorzystuje informacje o typie środowiska wykonawczego i przeprowadza kontrolę typu w czasie wykonywania.  Podnosi A Wyjątek ClassCast jeśli typ dynamiczny obiektu nie jest typem docelowym (lub podtypem typu docelowego) rzutowania. Przykładami odlewów z częścią dynamiczną są odlewy Obiekt To Smyczkowy lub z Obiekt To Lista<String> .  Są to tak zwane downcasty, od nadtypu do podtypu.

Przykładami konkretnych typów sparametryzowanych są Lista<String> , Mapa<String,Data> , ale nie Lista<? rozszerza numer > Lub Mapa<String,?> .

Czasami oczekuje się, że a Lista<Object> byłby nadtypem a Lista<String> , ponieważ Obiekt jest nadtypem Smyczkowy .  This expectation stems from the fact that such a type relationship exists for arrays:  Obiekt[] jest nadtypem Smyczkowy[] , ponieważ Obiekt jest nadtypem Smyczkowy . (Ten typ relacji jest znany jako kowariancja .) Relacja superpodtypów typów komponentów rozciąga się na odpowiednie typy tablic. Nie istnieje taka relacja typu dla instancji typów ogólnych. (Typy sparametryzowane to nie kowariant.)

Brak relacji superpodtypu między instancjami tego samego typu ogólnego ma różne konsekwencje.  Oto przykład.

Przykład:

void printAll(ArrayList<Object> c) {
  for (Object o : c) 
    System.out.println(o);
} Lista ArrayList<String> = nowa lista ArrayList<String>();
... wypełnij listę ...
printAll(lista);   // błąd

A ArrayList<String> obiektu nie można przekazać jako argumentu do metody żądającej a ArrayList<Object> ponieważ te dwa typy są instancjami tego samego typu ogólnego, ale for różny argumenty typu i z tego powodu nie są one ze sobą kompatybilne.

Konkretne typy sparametryzowane to konkretne instancje typu ogólnego.  Są prawie jak typy; istnieje tylko kilka ograniczeń.  NIE można ich używać do następujących celów:

  • do tworzenia tablic
  • w obsłudze wyjątków
  • w dosłowności klasowej
  • w instancja wyrażenie

Tablice zawierające elementy, których typ jest konkretnym typem sparametryzowanym, są nielegalne.

Przykład (niedozwolony typ tablicy):

statyczny test nieważności() {
  Para<Integer,Integer>[] intPairArr = nowy Para<Integer,Integer>[10] ; // błąd
  addElements(intPairArr); 
  Pair<Integer,Integer> pair = intPairArr[1];
  Integer i = pair.getFirst();
  pair.setSecond(i);
}
statyczne puste elementy addElements( Obiekt[] objArr) {
  objArr[0] = new Pair<Integer,Integer>(0,0);
  objArr[1] = new Pair<String,String>("","");      // should fail with ArrayStoreException
}

Kompilator zabrania tworzenia tablic, których typem komponentu jest konkretny typ sparametryzowany, np Para<Integer,Integer> w naszym przykładzie.  W poprzednim wpisie omówiliśmy, dlaczego uzasadnione jest, aby kompilator kwalifikował a Para<Integer,Integer>[] jako nielegalne.  Kluczowym problemem jest to, że kompilator i system wykonawczy muszą zapewnić, że tablica jest tablicą typu a jednorodny sekwencja elementów tego samego typu.  Jedna ze kontroli typu, a mianowicie kontrola magazynu tablic wykonywana przez maszynę wirtualną w czasie wykonywania, nie wykrywa nieprawidłowego wstawienia obcego elementu. W przykładzie drugie wstawienie w dodaj elementy metoda powinna zakończyć się niepowodzeniem, ponieważ dodawana była para ciągów do tablicy wartości całkowitych, ale nie zakończyła się niepowodzeniem zgodnie z oczekiwaniami. Powody zostały omówione w poprzednim wpisie.

Jeśli nie możemy używać tablic przechowujących elementy, których typ jest konkretnym typem sparametryzowanym, czego używamy jako obejścia?

Rozważmy 3 możliwe obejścia:

  • szereg surowych typów
  • tablica nieograniczonego typu parametrycznego z symbolami wieloznacznymi
  • kolekcja zamiast tablicy

Chociaż tablice konkretnych typów sparametryzowanych są nielegalne, kolekcje konkretnych typów sparametryzowanych są dozwolone.

Typ sparametryzowany za pomocą symboli wieloznacznych jest instancją typu ogólnego, w którym co najmniej jeden argument typu jest symbolem wieloznacznym.  Przykładami typów sparametryzowanych za pomocą symboli wieloznacznych są Kolekcja<?> , Lista<? rozszerza numer > , Komparator<? super String> I Para<String,?> . Typ sparametryzowany za pomocą symbolu wieloznacznego oznacza rodzinę typów zawierającą konkretne instancje typu ogólnego.  Rodzaj użytego symbolu wieloznacznego określa, które konkretne sparametryzowane typy należą do rodziny.  Na przykład typ sparametryzowany symbolem wieloznacznym Kolekcja<?> oznacza rodzinę wszystkich instancji Kolekcja interfejs niezależnie od argumentu typu.  Typ sparametryzowany symbolem wieloznacznym Lista<? rozszerza numer > denotes the family of all list types where the element type is a subtype of  Numer .  Typ sparametryzowany symbolem wieloznacznym Komparator<? super String> jest rodziną wszystkich instancji Komparator interfejs dla typów argumentów typu, które są nadtypami Smyczkowy.

Typ sparametryzowany za pomocą symboli wieloznacznych nie jest typem w zwykłym sensie (innym niż niesparametryzowana klasa/interfejs lub typ surowy).

Do wpisywania można używać typów sparametryzowanych za pomocą symboli wieloznacznych (takich jak niesparametryzowane klasy i interfejsy):

  • jako argumenty i metody zwracane
  • jako typ pola lub lokalna zmienna referencyjna
  • jako typ komponentu tablicy jako argument typu innych sparametryzowanych typów
  • jako typ celu w rzutach

Nie tylko typy mogą być ogólne, ale metody również mogą być ogólne. Metody statyczne i niestatyczne, a także konstruktory mogą mieć parametry typu. Składnia deklaracji parametrów typu formalnego jest podobna do składni typów ogólnych. Sekcja parametru typu jest ograniczona nawiasami ostrymi i pojawia się przed typem zwracanym przez metodę. Jego składnia i znaczenie są identyczne z listą parametrów typu ogólnego.

Oto przykład rodzaju ogólnego maks metoda obliczająca największą wartość w kolekcji elementów nieznanego typu A .

Przykład (metody ogólnej):

Kolekcje klas {
  public static <A rozszerza Porównywalny<A>> A max(Kolekcja< A > xs) {
    Iterator< A > xi = xs.iterator();
    A w = xi.next();
    while (xi.hasNext()) {
      A x = xi.następny();
      if (w.compareTo(x) < 0) w = x;
    }
    return w;
  }
} 

The maks metoda ma jeden parametr typu o nazwie A .  Jest to symbol zastępczy typu elementu kolekcji, na którym działa metoda. Parametr typu ma ograniczenie; musi to być typ będący podtypem Porównywalne<A>

Przykład (tego samego typu ogólnego, tym razem z miedza):

interfejs publiczny Porównywalny<T> {
  public int compareTo(T arg);
}
klasa publiczna TreeMap<Key rozszerza Comparable<Key> ,Dane>{
  private static class Entry<K,V> {
     K key;
     V value;
     Entry<K,V> left = null;
     Entry<K,V> right = null;
     Entry<K,V> parent;
  }
  private transient Entry<Key,Data> root = null;
  ...
  private Entry<Key,Data> getEntry(Key key) {
     Entry<Key,Data> p = root;
     Key k = key;
     while (p != null) {
       int cmp = k. porównajTo(p.key) ; 
       if (cmp == 0)
         return p;
       else if (cmp < 0)
         p = p.left;
       else 
         p = p.right; 
     }
     return null;
  }
  public boolean containsKey(Key key) {
     return getEntry(key) != null;
  }
  ...
}

W powyższym przykładzie parametr type Klawisz ma granicę Porównywalny<Key> .  Specyfikacja granicy ma dwa skutki:

  • Daje dostęp do metod określonych przez powiązanie .  W przykładzie związany Porównywalny<Key> daje dostęp do porównaj metodę, którą chcemy wywołać przy implementacji naszego Mapa Drzewa klasa.
  • Do tworzenia instancji typu ogólnego można używać tylko typów „w granicach”.   In the example, a parameterized type such as TreeMap<Number,String> zostałby odrzucony, ponieważ typ Numer nie jest podtypem Porównywalny<Numer> .  Typ sparametryzowany, taki jak TreeMap<String,String> zostałby zaakceptowany, ponieważ typ String mieści się w określonych granicach, tj. jest podtypem Porównywalne<String> .

Wszystkie klasy, interfejsy i typy wyliczeniowe mogą być używane jako powiązane parametry typu, w tym typy zagnieżdżone i wewnętrzne.  Ani typy pierwotne, ani typy tablicowe nie mogą być używane jako powiązane parametry typu.

Przykłady (ograniczeń parametrów typu):

rozszerza się klasa X0 <T wew > { ... }      // błąd
rozszerza się klasa X1 <T Obiekt[] > { ... } // błąd
rozszerza się klasa X2 <T Numer > { ... }
rozszerza się klasa X3 <T Smyczkowy > { ... }
rozszerza się klasa X4 <T Możliwość uruchomienia > { ... }
rozszerza się klasa X5 <T Stan wątku > { ... }
rozszerza się klasa X6 <T Lista > { ... }
rozszerza się klasa X7 <T Lista<String> > { ... }
rozszerza się klasa X8 <T Lista<? rozszerza numer > > { ... }
rozszerza się klasa X9 <T Porównywalny<? super numer> > { ... }
rozszerza się klasa X10<T Mapa.Wpis<?,?> > { ... }

Przykładowy kod pokazuje, że typy pierwotne, takie jak wew i typy tablic, takie jak Obiekt[] nie są dozwolone jako powiązane z parametrem typu.

Typy klas, np Numer Lub Smyczkowy i typy interfejsów, takie jak Możliwość uruchomienia , są dozwolone jako powiązane z parametrem typu.

Typy wyliczeniowe, takie jak Stan wątku są również dozwolone jako powiązane z parametrami typu. Stan wątku jest przykładem typu zagnieżdżonego używanego jako powiązany parametr typu.  Dozwolone są również niestatyczne typy wewnętrzne.

Typy surowe są dozwolone jako powiązane z parametrami typu; Lista jest przykładem.

Typy parametryczne są dozwolone jako powiązane z parametrami typu, w tym konkretne typy sparametryzowane, takie jak Lista<String> , ograniczone typy parametryczne z symbolami wieloznacznymi, takie jak Lista<? rozszerza numer > I Porównywalny<? super Long> i nieograniczone typy parametryczne z symbolami wieloznacznymi, takie jak Mapa.Wpis<?,?> .  Powiązanie, które jest typem sparametryzowanym za pomocą symbolu wieloznacznego, dopuszcza jako argument typu wszystkie typy należące do rodziny typów oznaczanej przez symbol wieloznaczny.  Powiązanie typu sparametryzowanego za pomocą symbolu wieloznacznego zapewnia jedynie ograniczony dostęp do pól i metod; ograniczenia zależą od rodzaju symbolu wieloznacznego.

Przykład (typu sparametryzowanego za pomocą symboli wieloznacznych jako powiązany parametr typu):

klasa X< T rozszerza List<? rozszerza numer > > {
  public void someMethod(T t) {
    t.add(new Long(0L));    // błąd
    Number n = t.remove(0);
  }
}
próba klasowa {
  public static void main(String[] args) {
     X<ArrayList< Długi >>   x1 = new X<ArrayList<Long>>(); 
     X<ArrayList< Smyczkowy >> x2 = nowy X<ArrayList<String>>(); // błąd
  }
}

Zmienne referencyjne typu T (parametr typu) są traktowane jak zmienne referencyjne typu wieloznacznego (powiązanie parametru typu).  W naszym przykładzie konsekwencją jest to, że kompilator odrzuca wywołanie metod, które przyjmują argument „nieznanego” typu, który oznacza parametr typu, np. Lista.dodaj , ponieważ granica jest typem sparametryzowanym za pomocą symboli wieloznacznych z górną granicą.
Jednocześnie związany Lista<? rozszerza numer > określa typy, które mogą być używane jako argumenty typu. Kompilator akceptuje wszystkie argumenty typu należące do rodziny typów Lista<? rozszerza numer > , czyli wszystkie podtypy Lista z argumentem typu, który jest podtypem Numer .

Parametr typu może służyć jako granica innego parametru typu.

Przykład (parametru typu użytego jako powiązanie parametru typu):

klasa Trójka <T> {
  private T fst, snd, trd;
  public < U rozciąga T , V rozciąga T , W rozciąga T > Potrójny(U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

W tym przykładzie parametr typu T klasy sparametryzowanej jest używana jako granica parametrów typu U ,  V I W sparametryzowanej metody instancji tej klasy.

Dalsze możliwości użycia parametrów typu jako granic innych parametrów typu obejmują sytuacje, w których typ zagnieżdżony jest zdefiniowany w typie ogólnym lub klasa lokalna jest zdefiniowana w metodzie ogólnej.  Dozwolone jest nawet użycie parametru typu jako powiązania innego parametru typu w tej samej sekcji parametru typu.

Powiązanie będące klasą zapewnia dostęp do wszystkich jej publicznych elementów członkowskich, czyli publicznych pól, metod i typów zagnieżdżonych. Nie są dostępne tylko konstruktory, ponieważ nie ma gwarancji, że podklasa powiązania ma te same konstruktory co powiązanie.

Przykład (klasy użytej jako granica parametru typu):

klasa publiczna Superklasa {     // static members
    public enum EnumType {THIS, THAT}
    public static Object staticField;
    public static void staticMethod() { ... }

    // non-static members
    public class InnerClass { ... }
    public Object nonStaticField;
    public void nonStaticMethod() { ... }

    // constructors
    public SuperClass() { ... }

    // private members
    private Object privateField; 

    ...
}

publiczna klasa końcowa SomeClass<T rozszerza się Superklasa > {
    private T object;
    public SomeClass(T t) { object = t; } 

    public String toString() {
        return
         "static nested type    : "+T.EnumType.class+"\n" 
        +"static field          : "+T.staticField+"\n"
        +"static method         : "+T.staticMethod()+"\n"
        +"non-static nested type: "+T.InnerClass.class+"\n"
        +"non-static field      : "+object.nonStaticField+"\n"
        +"non-static method     : "+object.nonStaticMethod()+"\n"
        +"constructor           : "+(new T())+"\n"                    // error
        +"private member        : "+object.privateField+"\n"          // error 
        ;
    }
}

Związany Superklasa daje dostęp do typów zagnieżdżonych, pól i metod statycznych oraz pól i metod niestatycznych.  Tylko konstruktor nie jest dostępny.  Dzieje się tak, ponieważ konstruktory nie są dziedziczone. Każda podklasa definiuje swoje własne konstruktory i nie musi obsługiwać konstruktorów swojej nadklasy.  Dlatego nie ma gwarancji, że podklasa Superklasa będzie miał tego samego konstruktora, co jego nadklasa.

Kontekst, w którym Enum<E rozszerza Enum<E>> ” pojawia się deklaracja Wyliczenie klasa w pakiecie java.lang :

publiczna klasa abstrakcyjna Enum<E rozszerza Enum<E>> {
  ...
}

Typ Wyliczenie jest wspólną klasą bazową wszystkich typów wyliczeniowych.  W Javie typ wyliczeniowy, taki jak Kolor jest tłumaczone na klasę Kolor to się rozciąga Wyliczenie<Kolor> . Cel nadklasy Wyliczenie jest zapewnienie funkcjonalności wspólnej dla wszystkich typów wyliczeń.

Oto szkic zajęć Wyliczenie :

publiczna klasa abstrakcyjna Enum< mi rozszerza Enum<E>> implementuje Comparable< mi >, serializowany {
  private final String name;
  public  final String name() { ... }   private final int ordinal;
  public  final int ordinal() { ... }

  protected Enum(String name, int ordinal) { ... }

  public String           toString() { ... }
  public final boolean    equals(Object other) { ... }
  public final int        hashCode() { ... }
  protected final Object  clone() throws CloneNotSupportedException { ... }
  public final int        compareTo( mi o) { ... }

  public final Class< mi > getDeclaringClass() { ... }
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}

Zaskakująca cecha w deklaracji „ Enum<E rozszerza Enum<E>> ” jest faktem, że nowo zdefiniowana klasa Wyliczenie i jego nowo zdefiniowany parametr typu mi pojawiają się w granicy tego samego parametru typu.  Oznacza to, że Wyliczenie typ musi zostać utworzony dla jednego z jego podtypów.  Aby zrozumieć, dlaczego ma to sens, należy wziąć pod uwagę, że każdy typ wyliczeniowy jest tłumaczony na podtyp Wyliczenie .

Oto wymyślony typ wyliczeniowy Kolor :

wyliczenie Kolor {CZERWONY, NIEBIESKI, ZIELONY}

Kompilator tłumaczy to na następującą klasę:

publiczne zajęcia końcowe Kolor rozciąga się Wyliczenie<Kolor> {
  public static final Color[] values() { return (Color[])$VALUES.clone(); }
  public static Color valueOf(String name) { ... }   private Color(String s, int i) { super(s, i); }

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  private static final Color $VALUES[];

  static {
    RED = new Color("RED", 0);
    BLUE = new Color("BLUE", 1);
    GREEN = new Color("GREEN", 2);
    $VALUES = (new Color[] { RED, BLUE, GREEN });
  }
}

Dziedziczenie ma ten skutek, że Kolor typ dziedziczy wszystkie metody zaimplementowane w Wyliczenie<Kolor> .  Wśród nich jest porównaj metoda.  The Kolor. porównaj metoda powinna prawdopodobnie zająć a Kolor jako argument.  Aby tak się stało, klasa Wyliczenie jest ogólny i Wylicz.porównajTo metoda trwa Wyliczenie parametr typu mi jako argument.  W rezultacie wpisz Kolor pochodzące z Wyliczenie<Kolor> dziedziczy A porównaj metoda, która wymaga a Kolor i argumentacja, dokładnie tak, jak powinna.
Jeśli przeanalizujemy deklarację „ Enum<E rozszerza Enum<E>> „Widzimy, że ten wzór ma kilka aspektów.

Po pierwsze, związanym parametrem typu jest sam typ: “ Wyliczenie <E rozszerza się Wyliczenie <E>> “. Zapewnia, że ​​tylko podtypy typu Wyliczenie są dozwolone jako argumenty typu. (Teoretycznie wpisz Wyliczenie można utworzyć instancję samą w sobie, jak in Wyliczenie<Enum> , ale z pewnością nie jest to zamierzone i trudno sobie wyobrazić sytuację, w której taka instancja byłaby użyteczna.)

Po drugie, istnieje fakt, że typ związany z parametrami jest typem sparametryzowanym Wyliczenie <E> , który używa parametru type mi jako argument typu powiązania. Ta deklaracja gwarantuje, że relacja dziedziczenia między podtypem a instancją Wyliczenie jest w formie X rozszerza Enum<X> „. Podtyp taki jak „ X rozszerza Enum<Y> ” nie można zadeklarować ze względu na argument typu Y nie mieściłoby się w granicach; tylko podtypy Wyliczenie<X> mieszczą się w granicach.

Po trzecie, faktem jest, że Wyliczenie ma przede wszystkim charakter ogólny.  Oznacza to, że niektóre metody klasy Wyliczenie przyjąć argument lub zwrócić wartość nieznanego typu (lub w inny sposób zależeć od nieznanego typu). Jak już wiemy, ten nieznany typ będzie później podtypem X z Wyliczenie<X> .  Stąd w typie sparametryzowanym Wyliczenie<X> , metody te obejmują podtyp X i są dziedziczone do podtypu X .  The porównaj metoda jest przykładem takiej metody; jest dziedziczona z nadklasy do każdej podklasy i w każdym przypadku ma sygnaturę specyficzną dla podklasy.

Podsumowując, oświadczenie „ Enum<E rozszerza Enum<E>> ” można rozszyfrować jako: Wyliczenie to typ ogólny, który można utworzyć tylko dla jego podtypów, a te podtypy odziedziczą kilka przydatnych metod, z których niektóre przyjmują argumenty specyficzne dla podtypu (lub w inny sposób zależą od podtypu).

Parametry typu dolnej granicy klas

Parametry typu mogą mieć kilka ograniczeń, np. in klasa Box<T rozszerza Number> {...} . Ale parametr typu nie może mieć dolnej granicy, to znaczy konstrukcji takiej jak klasa Box<T Świetnie Numer> {...} jest niedozwolone.  Dlaczego nie?  Odpowiedź brzmi: nie ma to sensu, bo nic by nie kupiło, gdyby było dozwolone.  Zobaczmy, dlaczego dolna granica parametrów typu klas jest myląca, badając, co oznacza górna granica parametru typu.

Górna granica parametru typu ma trzy skutki:

    1. Ograniczona instancja. Górna granica ogranicza zestaw typów, których można użyć do utworzenia instancji typu ogólnego.  Jeżeli zadeklarujemy a klasa Box<T rozszerza Number> {...} wtedy kompilator upewniłby się, że tylko podtypy Numer może być użyty jako argument typu.  To znaczy, Pudełko<Numer> lub Pudełko<Długie> jest dozwolone, ale a Pole<Object> Lub Pudełko<String> zostałoby odrzucone

Przykład (ograniczonego tworzenia instancji ze względu na górną granicę parametru typu):
class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   ...
}  próba klasowa { 
   public static void main(String[] args) { 
       Box<Long>    boxOfLong   = new Box<Long>(0L);   // Cienki
       Box<String>  boxOfString = new Box<String>(""); // błąd: ciąg znaków nie mieści się w granicach
   }
}

  1. Dostęp do członków niestatycznych. Górna granica daje dostęp do wszystkich publicznych niestatycznych metod i pól górnej granicy.  W realizacji naszego klasa Box<T rozszerza Number> {...} możemy wywołać wszystkie publiczne metody niestatyczne zdefiniowane w klasie Numer , takie jak intValue() na przykład.  Bez górnej granicy kompilator odrzuciłby każde takie wywołanie. Przykład (dostępu do elementów niestatycznych ze względu na górną granicę parametru typu):

class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   public int increment() { return value.intValue()+1; } // <= byłoby błędem bez ograniczenia liczby
   ...
} 

    1. Wpisz Usuń. Górna granica znajdująca się najbardziej po lewej stronie służy do usuwania typu i zastępuje parametr type w kodzie bajtowym.  w naszym klasa Box<T rozszerza Number> {...} wszystkie wystąpienia T zostanie zastąpiona górną granicą Numer .  Na przykład, jeśli klasa Skrzynka ma prywatne pole typu T i metoda zestaw pusty (zawartość T) do ustawienia tego pola prywatnego, wówczas pole będzie typu Numer po usunięciu typu, a metoda zostanie przetłumaczona na metodę zestaw pusty (zawartość liczbowa) .

Przykład (użycia górnej granicy parametru typu przy kasowaniu typu – zanim usunięcie typu):
klasa Box< T rozszerza numer > { 
   prywatny T wartość; 
   public Box( T t) { wartość = t; } 
   ...
} 

      Przykład (użycia górnej granicy parametru typu przy kasowaniu typu –

Po

      usunięcie typu):

 

pole klasy { 
   prywatny Numer wartość; 
   public Box( Numer t) { wartość = t; } 
   ...
}

    Ponadto skrajna lewa górna granica pojawia się w dalszych lokalizacjach, takich jak automatycznie wstawiane odlewy i metody mostów.

Jeśli dolne ograniczenia byłyby dozwolone dla parametrów typu, jakie skutki uboczne miałyby lub powinny mieć? Jeśli konstrukcja taka jak klasa Box<T Świetnie Numer> {...} były dozwolone, co by to oznaczało?  Jakie 3 skutki uboczne górnego ograniczenia parametru typu — ograniczone tworzenie instancji, dostęp do niestatycznego elementu członkowskiego, usuwanie typu — oznaczałyby dla dolnej granicy?

Ograniczone instancje. Kompilator może ograniczyć zestaw typów, których można użyć do utworzenia instancji typu ogólnego, za pomocą parametru typu dolnej granicy.  Na przykład kompilator może zezwolić na tworzenie instancji, takich jak Pudełko<Numer> I Pole<Object> z Pudełko<T Świetnie Numer> i odrzucaj instancje takie jak Pudełko<Długie> Lub Pudełko<Short> .  Byłby to efekt zgodny z restrykcyjnym efektem ubocznym opisanym dla górnych granic parametrów typu.

Dostęp do członków niestatycznych. Dolna granica parametru typu nie daje dostępu do żadnych konkretnych metod poza tymi odziedziczonymi z klasy Obiekt . Na przykładzie Pudełko<T Świetnie Numer> supertypy Numer nie mają ze sobą nic wspólnego poza tym, że są typami referencyjnymi, a zatem podtypami Obiekt .  Kompilator nie może założyć, że pole type T jest typu Numer lub jego podtyp.  Zamiast tego pole typu T może być dowolnego nadtypu Numer , takie jak Możliwość serializacji Lub Obiekt .  Wywołanie metody np intValue() nie jest już bezpieczny dla typu i kompilator musiałby go odrzucić. W konsekwencji dolna granica parametru typu nie zapewni dostępu do elementów niestatycznych poza tymi zdefiniowanymi w klasie Obiekt i dlatego ma taki sam skutek jak „bez ograniczeń”.

Wpisz Usuń .  Zgodnie z tą logiką nie ma sensu zastępować wystąpień parametru type jego dolną granicą skrajną z lewej strony.  Deklarowanie metody takiej jak konstruktor Skrzynka( T T) jako konstruktor Skrzynka( Numer T) nie ma sensu, biorąc to pod uwagę T zostaje zastąpiony nadtypem Numer .  Jakiś Obiekt można słusznie przekazać konstruktorowi w jednej instancji Pole<Object> i konstruktor by to odrzucił.  To byłoby całkowicie błędne.   Zatem usunięcie typu zastąpiłoby wszystkie wystąpienia zmiennej typu T według typu Obiekt , a nie jego dolną granicę.  Ponownie dolna granica miałaby taki sam skutek jak „brak ograniczenia”.

Czy chcesz dowiedzieć się, co by to oznaczało, gdyby oba były niższe _I_ górne granice były dozwolone?   Osobiście nawet nie chcę o tym myśleć i wolałbym umieścić to w kategorii „nie do zarządzania”, jeśli pozwolisz.

Konkluzja jest taka: wszystko to „ Świetnie „ograniczony by cię kupił” to ograniczenie, które dotyczy tylko supertypów Numer mogą być używane jako argumenty typu.  A nawet to często jest źle rozumiane.  NIE oznaczałoby to tego klasa Box<T super Numer> {...} zawiera tylko wystąpienia nadtypów Numer .  Wręcz odwrotnie – jak pokazuje poniższy przykład!

Przykład (użycia górnej granicy parametru typu przy kasowaniu typu – zanim usunięcie typu):

klasa Box< T super liczba > {
   prywatny T wartość;
   public Box( T t) { wartość = t; }
   ...
}

Przykład (użycia górnej granicy parametru typu przy kasowaniu typu – Po usunięcie typu):

pole klasy {
   prywatny Obiekt wartość;
   public Box( Obiekt t) { wartość = t; }
   ...
}

A klasa Box<T Świetnie Numer> {...} zostanie przetłumaczony przez usunięcie typu na a Skrzynka zawierający Obiekt pole i jego konstruktor zostanie przetłumaczony na Pudełko (obiekt t) . To zasadniczo różni się od klasa Box<T rozciąga się Numer> {...} , co byłoby przetłumaczone na a Skrzynka zawierający A Numer pole i jego konstruktor zostanie przetłumaczony na Pudełko (liczba t) . W związku z tym A Pudełko<Numer> utworzone z a klasa Box<T rozszerza Number> {...} różniłoby się od A Pudełko<Numer> instancja z a klasa Box<T super Numer> {...} , co może powodować zamieszanie.  Z tego powodu dolne granice nie mają sensu w przypadku parametrów typu klas.

 


 

Parametry typu dolnej granicy metod

W połączeniu z metodami i ich typami argumentów czasami przydatny może być parametr typu z dolną granicą.

Przykład (metody, w której przydałby się parametr typu z dolną granicą):

klasa Para<X,Y> {
   private X first;
   private Y second;
   ...
   public < Super X , B super Y > B addToMap(Map<A,B> map) { // błąd: parametr typu nie może mieć dolnej granicy
      return map.put(first, second);
   }
}
próba klasowa { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = pair.addToMap(map);
   }
}

The dodajDoMapy() Metoda dodaje zawartość pary do mapy.  Dowolna mapa, która może przechowywać supertypy X I Y zrobiłby.  Mapa umieścić() Metoda zwraca wartość znalezioną na mapie dla danego klucza, jeśli istnieje już wpis klucz-wartość dla klucza na mapie.  Wartość zwracana przez mapę umieścić() metoda zostanie zwrócona z metody dodajDoMapy() metoda.  W tych okolicznościach należałoby zadeklarować metodę jak pokazano powyżej: Mapa jest sparametryzowana nadtypami parametrów typu pary i dodajDoMapy() Typ zwracany przez metodę jest typem wartości mapy.

Ponieważ kompilator nie pozwala na dolne ograniczenia parametrów typu, potrzebujemy obejścia.

Jednym z rozwiązań, które przychodzi na myśl, jest użycie symboli wieloznacznych, ponieważ symbole wieloznaczne mogą mieć dolną granicę.  Oto obejście problemu z użyciem symbolu wieloznacznego.

Przykład (obejścia poprzedniego przykładu przy użyciu symboli wieloznacznych):

klasa Para<X,Y> {
   private X first;
   private Y second;
   ...
   publiczny Obiekt addToMap(Map< ? SuperX , ? super Y mapa >) { 
      return map.put(first, second);
   }
}
próba klasowa { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = (Numer) pair.addToMap(mapa);
   }
}

Działa, z tą różnicą, że nie ma możliwości zadeklarowania żądanego typu zwrotu.  Byłby to supertyp Y który kompilator przechwytuje z typu mapy, ale nie ma składni umożliwiającej jego określenie.  Nie możemy deklarować typu zwrotu „ ? super Y “, ponieważ “ ? super Y ” jest symbolem wieloznacznym, a nie typem i dlatego nie jest dozwolony jako typ zwracany. Nie mamy wyboru i musimy go użyć Obiekt zamiast tego jako typ zwracany przez naszą metodę.  Ten raczej niespecyficzny typ powrotu wymusza z kolei wywołanie metody dodajDoMapy() metodę na rzutowanie wartości zwracanej w dół Obiekt do jego rzeczywistego typu.  To nie jest dokładnie to, co mieliśmy na myśli.

Innym obejściem jest użycie metod statycznych.  Oto obejście z metodą statyczną zamiast metody niestatycznej.

Przykład (obejścia poprzedniego przykładu przy użyciu metody statycznej):

klasa Para<X,Y> {
   private X first;
   private Y second;
   ...
   publiczny statyczny < A , B ,X rozciąga A ,Y rozciąga B > B addToMap (para<X, para Y>, Map< A , B mapa >) {
      return map.put(pair.first,pair.second);
   }
}
próba klasowa { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = Para .addToMap( para ,mapa);
   }
}

Ogólny dodajDoMapy() metoda ma cztery parametry typu: dwa symbole zastępcze X I Y dla typu pary i dwóch symboli zastępczych A I B dla typu mapy. A I B są supertypami X I Y , ponieważ X I Y   are declared with A I B jako ich górne granice. (Zauważ, że parametry typu metody ogólnej X I Y nie mają nic wspólnego z Para klasa X I Y parametry.  Imiona X I Y są ponownie wykorzystywane w metodzie ogólnej, aby były łatwo rozpoznawalne jako parametry typu pary. Używając czterech parametrów typu, możemy zadeklarować dokładny typ zwracany według potrzeb: jest to ten sam typ, co typ wartości mapy.

Przykład (nielegalnego użycia parametru typu w kontekście statycznym klasy ogólnej):

klasa SomeClass <T> {
  // inicjator statyczny, pole statyczne, metoda statyczna
  statyczny {
    SomeClass< T Test > = nowa SomeClass< T >(); // błąd
  }
  prywatny statyczny T globalInfo;          // błąd
  publiczny  statyczny T getGlobalInfo() {        // błąd
    return globalInfo;
  }
  // inicjator niestatyczny, pole niestatyczne, metoda niestatyczna
  { 
    SomeClass< T Test > = nowa SomeClass< T >();
  }
  prywatny        T informacje lokalne;
  publiczny         T getLocalInfo() { 
    return localInfo;
  }
  // statyczne typy zagnieżdżone
  publiczny statyczny klasa Awaria rozszerza wyjątek {
    private final T info;           // błąd
    public Failure( T t) { informacja = t; }      // błąd
    publiczny T getInfo() { return info; }    // błąd
   }
  prywatny interfejs Możliwość kopiowania {
    T copy();                       // błąd
  }
  prywatny wyliczenie Państwo {
    VALID, INVALID;
    prywatny T informacje; // błąd
    public void setInfo( T t) { informacja = t; } // błąd
    publiczny T getInfo() { return info; }    // błąd
  }
  // niestatyczne typy zagnieżdżone
  publiczny klasa Akcesor {
    publiczny T getInfo() { zwróć informacje lokalne; }
  }
}

Przykład ilustruje, że parametru type nie można użyć w kontekście statycznym klasy ogólnej.  Pokazuje również, że zagnieżdżone interfejsy i typy wyliczeniowe są uważane za elementy statyczne klasy.  Tylko klasy wewnętrzne, czyli niestatyczne klasy zagnieżdżone, mogą używać parametru type otaczającej klasy ogólnej.

Zakres parametru typu interfejsu to cała definicja interfejsu, z wyjątkiem wszelkich pól i typów zagnieżdżonych.  Dzieje się tak, ponieważ pola i typy zagnieżdżone zdefiniowane w interfejsie są domyślnie statyczne.

Przykład (nielegalnego użycia parametru type w interfejsie ogólnym):

interfejs SomeInterface <T> {
  // pole
  SomeClass< T Wartość > = nowa SomeClass< T >();  // błąd
  // typ zagnieżdżony
  klasa Akcesor {
    publiczny T getInfo() {            // błąd
      return value.getGlobalInfo();
    }
  }
  // metody
  T pobierzWartość();
}

Przykład pokazuje, że pola interfejsu są domyślnie statyczne, więc parametru type nie można użyć w żadnym miejscu w deklaracji pola interfejsu ogólnego.  Podobnie klasa zagnieżdżona jest uważana za statyczną klasę zagnieżdżoną, a nie klasę wewnętrzną i z tego powodu użycie parametru type w dowolnym miejscu klasy zagnieżdżonej jest nielegalne.

Obejście dla zagnieżdżonych klas statycznych i interfejsów:

W przypadku statycznie zagnieżdżonych klas i interfejsów nie jest to poważna katastrofa.  Jako obejście możemy wygenerować klasę statyczną lub sam interfejs.

Przykład (obejście – wygeneruj zagnieżdżony typ statyczny):

Opakowanie klasy <T> {
  private final T Obiekt;

  public Wrapper( T t) {Obiekt = t; }
  publiczny T getWrapper() { zwróć obiekt; }   public Immutable <T> makeImmutable() {
        return new Immutable <T> (Obiekt);
  }
  public Mutable <T> makeMutable() {
        return new Mutable <T> (Obiekt);
  }
  private static <A> A makeClone(A theObject) { ... }

  public static final class Immutable <A> { // jest teraz klasą ogólną
        private final A Obiekt;

        public Immutable( A argument) {
            theObject = makeClone(arg);
        }
        publiczny A getWrappedItem() {
            return makeClone(theObject);
        }
  }
  public static class Mutable <A> {
        ... similar ...
  }
}

Nie ma takiego obejścia dla zagnieżdżonych typów wyliczeniowych, ponieważ nie mogą one być ogólne.

Obejście dla zagnieżdżonych klas statycznych:

Jeśli zagnieżdżonym typem statycznym jest klasa, alternatywnym obejściem byłoby przekształcenie klasy statycznej w niestatyczną klasę wewnętrzną.

Przykład (obejście – użyj klasy wewnętrznej):

Opakowanie klasy <T> {
  private final T Obiekt;

  public Wrapper( T t) {Obiekt = t; }
  publiczny T getWrapper() { zwróć obiekt; }   public Mutable makeMutable() {
      return Ten. nowy Mutable(theObject);
  }
  public Immutable makeImmutable() {
      return Ten. nowy Niezmienny(obiekt);
  }
  private static <A> A makeClone(A theObject) { ... }

  public final class Immutable {  // nie jest już klasą statyczną
      private final T Obiekt;

      public Immutable( T argument) {
          theObject = makeClone(arg);
      }
      publiczny T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  public class Mutable 
        ... similar ...
  }
}

To obejście wiąże się z pewnym obciążeniem, ponieważ wszystkie klasy wewnętrzne mają ukryte odniesienie do instancji typu zewnętrznego, którego w tym przykładzie nie potrzebują ani z którego nie korzystają; ukryte odniesienie to tylko bagaż.  Często klasy wewnętrzne są łączone z interfejsami, aby klasa wewnętrzna była prywatnym szczegółem implementacji klasy otaczającej.  Tutaj możemy zrobić to samo.

Przykład (poprawione poprzednie obejście):

Opakowanie klasy <T> {
  private final T Obiekt;

  public Wrapper( T t) {Obiekt = t; }
  publiczny T getWrapper() { zwróć obiekt; }   public Mutable <T> makeMutable() {
      return this.new Mutable(theObject);
  }
  public Immutable <T> makeImmutable() {
      return this.new Immutable(theObject);
  }
  private static <A> A makeClone(A theObject) { ... }

  interfejs publiczny Niezmienny<S> {
      S getWrappedItem();
  }
  interfejs publiczny Mutable<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  prywatny klasa końcowa ImmutableImplementation implementuje Immutable<T> { 

      private final T theObject;

      public ImmutableImplementation(T arg) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  prywatny klasa MutableImplementation implementuje Mutable<T> { 
        ... similar ...
  }
}

Jak widać, zagnieżdżone interfejsy publiczne muszą być ogólne, podczas gdy wewnętrzne klasy prywatne mogą używać parametru typu otaczającej je klasy.

 

 

 

 

 

Zostaw odpowiedź

Dowiedz się więcej na blogu Velislava Krasteva

Zapisz się teraz, aby czytać dalej i uzyskać dostęp do pełnego archiwum.

Kontynuuj czytanie