Java generics forklaret i kort form med eksempler

Se venligst denne FAQ for mere information:

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

Her i kort form nogle vigtige punkter fra denne FAQ.

Eksempel (af en generisk type):

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; }
}

Klassen Par har to typeparametre X og Y .  De erstattes af typeargumenter, når den generiske type Par er instansieret. For eksempel i erklæringen Pair<String, Date> typeparameteren X erstattes af typeargumentet Snor og Y is replaced by  Dato .
Omfanget af identifikatorerne X og Y er hele definitionen af ​​klassen.  I dette omfang de to type parametre X og Y bruges, som de var typer (med nogle begrænsninger). I eksemplet ovenfor bruges typeparametrene som argument- og returtype for instansmetoder og typer instansfelter.
Typeparametre kan erklæres med grænser. Bounds giver adgang til metoder af den ukendte type, som typeparameteren står for. I vores eksempel påberåber vi os ingen metoder af ukendte typer X og Y . Af denne grund er de to typeparametre ubegrænsede.

Eksempel (af en konkret parameteriseret type):

public void printPair( Par<String,Long> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<String,Long> grænse = ny Par<String,Long> ("maksimum", 1024L);
printPair(grænse);

Eksempel (på en jokertegnsparametriseret type):

public void printPair( Par<?,?> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<?,?> grænse = ny Par<String,Long> ("maksimum", 1024L);
printPair(grænse);

Alle forekomster af en generisk type deler den samme runtime type repræsentation, nemlig repræsentationen af ​​den rå type. For eksempel instansieringer af en generisk type Liste , som f.eks Liste<Date> , Liste<String> , Liste<Long> , osv. har forskellige statiske typer på kompileringstidspunktet, men den samme dynamiske type Liste ved kørsel.

En rollebesætning består af to dele:

  • en statisk typekontrol udført af compileren på kompileringstidspunktet og
  • et dynamisk typetjek udført af den virtuelle maskine under kørsel.

Den statiske del sorterer useriøse casts fra, som ikke kan lykkes, såsom castet fra Snor at Dato eller fra Liste<String> at Liste<Date> .

Den dynamiske del bruger runtime type information og udfører en type check på runtime.  Det rejser en ClassCastException hvis den dynamiske type af objektet ikke er måltypen (eller en undertype af måltypen) af rollebesætningen. Eksempler på casts med en dynamisk del er castet fra Objekt at Snor eller fra Objekt at Liste<String> .  Det er de såkaldte downcasts, fra en supertype ned til en undertype.

Eksempler på konkrete parameteriserede typer er Liste<String> , Map<String,Date> , men ikke Liste<? udvider Number> eller Map<String,?> .

Det forventes nogle gange, at en Liste<Object> ville være en supertype af en Liste<String> , fordi Objekt er en supertype af Snor .  This expectation stems from the fact that such a type relationship exists for arrays:  Objekt[] er en supertype af Snor[] , fordi Objekt er en supertype af Snor . (Denne type forhold er kendt som kovarians .) Super-subtype-relationen af ​​komponenttyperne strækker sig ind i de tilsvarende array-typer. Der eksisterer ikke et sådant typeforhold for instansieringer af generiske typer. (Parameteriserede typer er ikke kovariant.)

Manglen på et super-undertype-forhold mellem instansieringer af samme generiske type har forskellige konsekvenser.  Her er et eksempel.

Eksempel:

void printAll(ArrayList<Object> c) {
  for (Object o : c) 
    System.out.println(o);
} ArrayList<String> liste = ny ArrayList<String>();
... udfyld listen ...
printAll(liste);   // fejl

EN ArrayList<String> objekt kan ikke videregives som argument til en metode, der beder om en ArrayList<Object> fordi de to typer er instansieringer af samme generiske type, men for anderledes typeargumenter, og af denne grund er de ikke kompatible med hinanden.

Betonparametriserede typer er konkrete instansieringer af en generisk type.  De er næsten som typer; der er kun få begrænsninger.  De kan IKKE bruges til følgende formål:

  • til oprettelse af arrays
  • i undtagelseshåndtering
  • i en klasse bogstavelig
  • i en forekomst af udtryk

Arrays, der indeholder elementer, hvis type er en konkret parameteriseret type, er ulovlige.

Eksempel (på ulovlig matrixtype):

static void test() {
  Pair<Integer,Integer>[] intPairArr = ny Par<Hel,Hel>[10] ; // fejl
  addElements(intPairArr); 
  Pair<Integer,Integer> pair = intPairArr[1];
  Integer i = pair.getFirst();
  pair.setSecond(i);
}
static void addElements( Objekt[] objArr) {
  objArr[0] = new Pair<Integer,Integer>(0,0);
  objArr[1] = new Pair<String,String>("","");      // should fail with ArrayStoreException
}

Compileren forbyder oprettelse af arrays, hvis komponenttype er en konkret parameteriseret type, som f.eks. Par<Heltal,Hel> i vores eksempel.  Vi diskuterede i den foregående post, hvorfor det er rimeligt, at compileren kvalificerer en Pair<Integer,Integer>[] som ulovligt.  Nøgleproblemet er, at compileren og runtime-systemet skal sikre, at et array er en homogen række af elementer af samme type.  En af typekontrollerne, nemlig array-store-check, der udføres af den virtuelle maskine under kørsel, opdager ikke den fornærmende indsættelse af et fremmedelement. I eksemplet den anden indsættelse i addElements metoden skulle mislykkes, fordi de tilføjede et par strenge til en matrix af integrale værdier, men den fejler ikke som forventet. Årsagerne blev diskuteret i det foregående indlæg.

Hvis vi ikke kan bruge arrays med elementer, hvis type er en konkret parameteriseret type, hvad bruger vi så som en løsning?

Lad os overveje 3 tænkelige løsninger:

  • række råtyper
  • matrix af ubegrænset jokertegn parametriseret type
  • samling i stedet for array

Mens arrays af konkrete parameteriserede typer er ulovlige, er samlinger af konkrete parameteriserede typer tilladt.

En jokertegnsparameteriseret type er en instansiering af en generisk type, hvor mindst ét ​​typeargument er et jokertegn.  Eksempler på parametriserede jokertegntyper er Samling<?> , Liste<? udvider Number> , Komparator <? super String> og Par<String,?> . En jokertegn-parametriseret type betegner en familie af typer, der omfatter konkrete instansieringer af en generisk type.  Typen af ​​det anvendte jokertegn bestemmer, hvilke konkrete parametriserede typer der tilhører familien.  For eksempel den parametriserede jokertegntype Samling<?> betegner familien af ​​alle instantiationer af Samling grænseflade uanset typeargumentet.  Den parametriserede jokertegntype Liste<? udvider Number> denotes the family of all list types where the element type is a subtype of  Antal .  Den parametriserede jokertegntype Komparator <? super String> er familien af ​​alle instansieringer af Komparator grænseflade for type argumenttyper, der er supertyper af Snor.

En jokertegn-parametriseret type er ikke en type i almindelig forstand (forskellig fra en ikke-parameteriseret klasse/grænseflade eller en rå type).

Parametriserede jokertegntyper kan bruges til indtastning (såsom ikke-parametriserede klasser og grænseflader):

  • som argument- og returtyper af metoder
  • som type af et felt eller lokal referencevariabel
  • som komponenttype af et arrays-argument af andre parameteriserede typer
  • som måltype i afstøbninger

Ikke kun typer kan være generiske, men metoder kan også være generiske. Statiske og ikke-statiske metoder samt konstruktører kan have typeparametre. Syntaksen for deklaration af de formelle typeparametre svarer til syntaksen for generiske typer. Typeparameterafsnittet er afgrænset af vinkelparenteser og vises før metodens returtype. Dens syntaks og betydning er identisk med typeparameterlisten for en generisk type.

Her er eksemplet på en generisk max metode, der beregner den største værdi i en samling af elementer af en ukendt type EN .

Eksempel (på en generisk metode):

klassesamlinger {
  public static <A udvider Sammenlignelig <A>> A max(Collection< EN > xs) {
    Iterator< EN > xi = xs.iterator();
    EN w = xi.næste();
    while (xi.hasNext()) {
      EN x = xi.næste();
      if (w.compareTo(x) < 0) w = x;
    }
    return w;
  }
} 

De max metoden har én type parameter, navngivet EN .  Det er en pladsholder til den elementtype af samlingen, som metoden arbejder på. Typeparameteren har en grænse; det skal være en type, der er en undertype af Sammenlignelig <A>

Eksempel (af samme generiske type, denne gang med grænser):

offentlig grænseflade Sammenlignelig<T> {
  public int compareTo(T arg);
}
offentlig klasse TreeMap<Key udvider Comparable<Key> ,Data>{
  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. sammenlignTil(s.tast) ; 
       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;
  }
  ...
}

I eksemplet ovenfor er typeparameteren Nøgle har bundet Sammenlignelig <Key> .  Specifikation af en binding har to effekter:

  • Det giver adgang til de metoder, som bunden specificerer .  I eksemplet er bundet Sammenlignelig <Key> giver adgang til sammenligneTil metode, som vi ønsker at påberåbe os i implementeringen af ​​vores Trækort klasse.
  • Kun typer “inden for grænserne” kan bruges til instansiering af den generiske type.   In the example, a parameterized type such as Trækort<Number,String> ville blive afvist, fordi typen Antal er ikke en undertype af Sammenlignelig <Number> .  En parameteriseret type som TreeMap<String,String> ville blive accepteret, fordi typen String er inden for grænserne, dvs. er en undertype af Sammenlignelig <String> .

Alle klasser, grænseflader og enum-typer kan bruges som typeparameterbundet, inklusive indlejrede og indre typer.  Hverken primitive typer eller matrixtyper bruges som typeparameterbundet.

Eksempler (på typeparametergrænser):

klasse X0 <T forlænges int > { ... }      // fejl
klasse X1 <T forlænges Objekt[] > { ... } // fejl
klasse X2 <T forlænges Antal > { ... }
klasse X3 <T forlænges Snor > { ... }
klasse X4 <T forlænges Kørbar > { ... }
klasse X5 <T forlænges Tråd.Stat > { ... }
klasse X6 <T forlænges Liste > { ... }
klasse X7 <T forlænges Liste<String> > { ... }
klasse X8 <T forlænges Liste<? udvider Number> > { ... }
klasse X9 <T forlænges Sammenlignelig <? super Number> > { ... }
klasse X10<T forlænges Map.Entry<?,?> > { ... }

Kodeeksemplet viser, at primitive typer som f.eks int og matrixtyper som f.eks Objekt[] er ikke tilladt som typeparameterbundet.

Klassetyper, som f.eks Antal eller Snor , og grænsefladetyper, som f.eks Kørbar , er tilladt som type parameter bundet.

Enum typer, som f.eks Tråd.Stat er også tilladt som typeparameterbundet. Tråd.Stat er et eksempel på en indlejret type, der bruges som bundet typeparameter.  Ikke-statiske indre typer er også tilladt.

Råtyper er tilladt som typeparameterbundet; Liste er et eksempel.

Parameteriserede typer er tilladt som typeparameterbundne, herunder konkrete parameteriserede typer som f.eks Liste<String> , afgrænsede jokertegn parametriserede typer som f.eks Liste<? udvider Number> og Sammenlignelig <? super Long> , og ubegrænsede jokertegn parametriserede typer som f.eks Map.Entry<?,?> .  En bound, der er en jokertegnsparametriseret type, tillader som typeargument alle typer, der hører til den typefamilie, som jokertegnet angiver.  Den parametriserede jokertegnstype bundet giver kun begrænset adgang til felter og metoder; begrænsningerne afhænger af typen af ​​jokertegn.

Eksempel (på jokertegn parametriseret type som type parameter bundet):

klasse X< T forlænger List<? udvider Number> > {
  public void someMethod(T t) {
    t.add(new Long(0L));    // fejl
    Number n = t.remove(0);
  }
}
klasse Test {
  public static void main(String[] args) {
     X<ArrayList< Lang >>   x1 = new X<ArrayList<Long>>(); 
     X<ArrayList< Snor >> x2 = ny X<ArrayList<String>>(); // fejl
  }
}

Referencevariabler af typen T (typeparameteren) behandles som referencevariabler af en jokertegntype (typeparameteren bundet).  I vores eksempel er konsekvensen, at compileren afviser påkaldelse af metoder, der tager et argument af den “ukendte” type, som typeparameteren står for, som f.eks. Liste.add , fordi grænsen er en jokertegnsparameteriseret type med en øvre grænse.
Samtidig det bundne Liste<? udvider Number> bestemmer de typer, der kan bruges som typeargumenter. Compileren accepterer alle typeargumenter, der hører til typefamilien Liste<? udvider Number> , det vil sige alle undertyper af Liste med et typeargument, der er en undertype af Antal .

En typeparameter kan bruges som grænse for en anden typeparameter.

Eksempel (på en typeparameter brugt som en typeparameterbundet):

klasse Triple <T> {
  private T fst, snd, trd;
  public < U forlænger T , V forlænger T , W forlænger T > Triple(U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

I dette eksempel er typeparameteren T af den parametriserede klasse bruges som bundet af typeparametrene U ,  V og W af en parameteriseret instansmetode af den klasse.

Yderligere muligheder for at bruge typeparametre som grænser for andre typeparametre omfatter situationer, hvor en indlejret type er defineret inde i en generisk type, eller en lokal klasse er defineret i en generisk metode.  Det er endda tilladt at bruge en typeparameter som bundet af en anden typeparameter i samme typeparameterafsnit.

En bound, der er en klasse, giver adgang til alle dens offentlige medlemmer, det vil sige offentlige felter, metoder og indlejrede typer. Kun konstruktører gøres ikke tilgængelige, fordi der ikke er nogen garanti for, at en underklasse af bundet har de samme konstruktører som bundet.

Eksempel (på en klasse brugt som bundet til en typeparameter):

offentlig klasse Superklasse {     // 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; 

    ...
}

offentlig slutklasse SomeClass<T forlænger Superklasse > {
    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 
        ;
    }
}

Den bundne Superklasse giver adgang til dets indlejrede typer, statiske felter og metoder og ikke-statiske felter og metoder.  Kun konstruktøren er ikke tilgængelig.  Dette skyldes, at konstruktører ikke nedarves. Hver underklasse definerer sine egne konstruktører og behøver ikke at understøtte sin superklasses konstruktører.  Derfor er der ingen garanti for, at en underklasse af Superklasse vil have samme konstruktør som sin superklasse.

Den sammenhæng, hvori Enum<E forlænger Enum<E>> ” vises er erklæringen fra Enum klasse i pakke java.lang :

offentlig abstrakt klasse Enum<E forlænger Enum<E>> {
  ...
}

Typen Enum er den fælles basisklasse for alle opregningstyper.  I Java en opregningstype som f.eks Farve er oversat til en klasse Farve der strækker sig Enum<Color> . Formålet med superklassen Enum er at levere funktionalitet, der er fælles for alle opregningstyper.

Her er en skitse af klassen Enum :

offentlig abstrakt klasse Enum< E forlænger Enum<E>> implementerer Comparable< E >, kan serialiseres {
  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( E o) { ... }

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

Det overraskende træk i erklæringen “ Enum<E forlænger Enum<E>> ” er det faktum, at den nyligt definerede klasse Enum og dens nyligt definerede typeparameter E vises i grænsen for den samme typeparameter.  Det betyder, at Enum type skal instansieres for en af ​​dens undertyper.  For at forstå, hvorfor dette giver mening, skal du overveje, at hver enum-type er oversat til en undertype af Enum .

Her er den konstruerede enum-type Farve :

enum Farve {RØD, BLÅ, GRØN}

Compileren oversætter det til følgende klasse:

offentlig sidste klasse Farve strækker sig Enum<Color> {
  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 });
  }
}

Arven har den virkning, at den Farve type arver alle metoderne implementeret i Enum<Color> .  Blandt dem er sammenligneTil metode.  De Farve. sammenligneTil metode skal nok tage en Farve som et argument.  For at få dette til at ske klasse Enum er generisk og Enum.compareTo metode tager Enum ‘s type parameter E som et argument.  Som et resultat, skriv Farve stammer fra Enum<Color> arver en sammenligneTil metode, der tager en Farve som og argument, præcis som det skal.
Hvis vi dissekerer erklæringen “ Enum<E forlænger Enum<E>> “Vi kan se, at dette mønster har flere aspekter.

For det første er der det faktum, at typeparameteren bundet er selve typen: “ Enum <E forlænger Enum <E>> “. Det sikrer, at kun undertyper af type Enum er tilladt som typeargumenter. (Teoretisk, skriv Enum kunne instantieres på sig selv, som i Enum<Enum> , men dette er bestemt ikke meningen, og det er svært at forestille sig en situation, hvor en sådan instansiering ville være nyttig.)

For det andet er der det faktum, at den parameterbundne type er den parameteriserede type Enum <E> , som bruger typeparameteren E som typeargumentet for bundet. Denne erklæring sikrer, at arveforholdet mellem en undertype og en instansiering af Enum er af formen X forlænger Enum<X> “. En undertype som “ X forlænger Enum<Y> ” kan ikke erklæres på grund af typeargumentet Y ville ikke være inden for grænserne; kun undertyper af Enum<X> er inden for grænserne.

For det tredje er der det faktum, at Enum er generisk i første omgang.  Det betyder, at nogle af klassens metoder Enum tage et argument eller returnere en værdi af en ukendt type (eller på anden måde afhænge af en ukendt type). Som vi allerede ved, vil denne ukendte type senere være en undertype X af Enum<X> .  Derfor i den parametriserede type Enum<X> , involverer disse metoder undertypen X , og de nedarves til undertypen X .  De sammenligneTil metode er et eksempel på en sådan metode; den nedarves fra superklassen til hver underklasse og har en underklassespecifik signatur i hvert tilfælde.

For at opsummere det, erklæringen “ Enum<E forlænger Enum<E>> ” kan tydes som: Enum er en generisk type, der kun kan instansieres for dens undertyper, og disse undertyper vil arve nogle nyttige metoder, hvoraf nogle tager undertypespecifikke argumenter (eller på anden måde afhænger af undertypen).

Nedre grænsetypeparametre for klasser

Typeparametre kan have flere grænser, som f.eks klasse Box<T udvider Number> {...} . Men en typeparameter kan ikke have nogen nedre grænse, det vil sige en konstruktion som f.eks klasse Box<T stor Number> {...} er ikke tilladt.  Hvorfor ikke?  Svaret er: det er meningsløst, fordi det ikke ville købe dig noget, hvis det var tilladt.  Lad os se, hvorfor nedre grænsetypeparametre for klasser er forvirrende ved at undersøge, hvad en øvre grænse på en typeparameter betyder.

Den øvre grænse for en typeparameter har tre effekter:

    1. Begrænset instansiering. Den øvre grænse begrænser det sæt af typer, der kan bruges til instansiering af den generiske type.  Hvis vi erklærer en klasse Box<T udvider Number> {...} så ville compileren sikre, at kun undertyper af Antal kan bruges som type argument.  Det vil sige en Box<Number> eller a Box<Long> er tilladt, men a Box<Object> eller Box<String> ville blive afvist

Eksempel (på begrænset instansiering på grund af en øvre grænse for en typeparameter):
class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   ...
}  klasse Test { 
   public static void main(String[] args) { 
       Box<Long>    boxOfLong   = new Box<Long>(0L);   // fint
       Box<String>  boxOfString = new Box<String>(""); // fejl: Streng er ikke inden for grænserne
   }
}

  1. Adgang til ikke-statiske medlemmer. Den øvre grænse giver adgang til alle offentlige ikke-statiske metoder og felter i den øvre grænse.  I gennemførelsen af ​​vores klasse Box<T udvider Number> {...} vi kan påberåbe alle offentlige ikke-statiske metoder defineret i klassen Antal , som f.eks intValue() for eksempel.  Uden den øvre grænse ville compileren afvise enhver sådan påkaldelse. Eksempel (på adgang til ikke-statiske medlemmer på grund af en øvre grænse på en typeparameter):

class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   public int increment() { return value.intValue()+1; } // <= ville være en fejl uden nummeret bundet
   ...
} 

    1. Skriv sletning. Den øverste grænse længst til venstre bruges til typesletning og erstatter typeparameteren i bytekoden.  I vores klasse Box<T udvider Number> {...} alle forekomster af T ville blive erstattet af den øvre grænse Antal .  For eksempel hvis klasse Boks har et privat typefelt T og en metode void sæt (T indhold) for at indstille dette private felt, så ville feltet være af typen Antal efter tekstsletning, og metoden ville blive oversat til en metode void indstillet (nummerindhold) .

Eksempel (på brug af øvre grænse på en typeparameter i typesletning – før type sletning):
klasse Box< T udvider nummer > { 
   privat T værdi; 
   public Box( T t) { værdi = t; } 
   ...
} 

      Eksempel (på brug af øvre grænse på en typeparameter i typesletning –

efter

      type sletning):

 

klasse Box { 
   privat Antal værdi; 
   public Box( Antal t) { værdi = t; } 
   ...
}

    Derudover vises den øverste grænse længst til venstre på andre steder, såsom automatisk indsatte afstøbninger og brometoder.

Hvis nedre grænser var tilladt for typeparametre, hvilke bivirkninger ville eller skulle de have? Hvis en konstruktion som f.eks klasse Box<T stor Number> {...} var tilladt, hvad ville det betyde?  Hvad ville de 3 bivirkninger af en øvre type parametergrænse – begrænset instansiering, adgang til ikke-statisk medlem, type sletning – betyde for en nedre grænse?

Begrænsede instantiationer. Compileren kunne begrænse det sæt af typer, der kan bruges til instansiering af den generiske type med en lavere bundet typeparameter.  For eksempel kunne compileren tillade instansieringer som f.eks Box<Number> og Box<Object> fra en Box<T stor Nummer> og afvise instansieringer som f.eks Box<Long> eller Box<Short> .  Dette ville være en effekt på linje med den restriktive bivirkning, der er beskrevet for øvre type parametergrænser.

Adgang til ikke-statiske medlemmer. En lavere type parametergrænse giver ikke adgang til bestemte metoder ud over dem, der er nedarvet fra klassen Objekt . I eksemplet med Box<T stor Nummer> supertyperne af Antal har intet til fælles, bortset fra at de er referencetyper og derfor undertyper af Objekt .  Compileren kan ikke antage, at feltet type T er af typen Antal eller en undertype deraf.  I stedet for typefeltet T kan være af enhver supertype Antal , som f.eks Serialiserbar eller Objekt .  Påkaldelsen af ​​en metode som f.eks intValue() er ikke længere typesikker, og compileren bliver nødt til at afvise den. Som en konsekvens ville den lavere type parametergrænse ikke give adgang til et ikke-statisk medlem ud over dem, der er defineret i klassen Objekt og har dermed samme effekt som “no bound”.

Skriv sletning .  Efter denne logiske linje giver det ikke mening at erstatte forekomsterne af typeparameteren med dens nederste grænse længst til venstre.  Erklære en metode som konstruktøren Boks( T t) som konstruktør Boks( Antal t) giver ikke mening i betragtning af det T er erstattet af en supertype af Antal .  An Objekt kan med rette videregives til konstruktøren på et øjeblik Box<Object> og konstruktøren ville afvise det.  Det her ville være helt forkert.   Så typesletning ville erstatte alle forekomster af typevariablen T efter type Objekt , og ikke ved sin nedre grænse.  Igen vil den nedre grænse have samme effekt som “ingen grænse”.

Vil du finde ud af, hvad det ville betyde, hvis begge lavere _og_ var øvre grænser tilladt?   Personligt gider jeg ikke engang tænke på det og foretrækker at arkivere det under “ikke håndterbart”, hvis du tillader det.

Den nederste linje er: alt det en “ stor ” bundet ville købe dig er den begrænsning, som kun supertyper af Antal kan bruges som typeargumenter.  Og selv det bliver ofte misforstået.  Det ville det IKKE betyde klasse Box<T super Number> {...} indeholder kun forekomster af supertyper af Antal .  Tværtimod – som eksemplet nedenfor viser!

Eksempel (på brug af øvre grænse på en typeparameter i typesletning – før type sletning):

klasse Box< T supernummer > {
   privat T værdi;
   public Box( T t) { værdi = t; }
   ...
}

Eksempel (på brug af øvre grænse på en typeparameter i typesletning – efter type sletning):

klasse Box {
   privat Objekt værdi;
   public Box( Objekt t) { værdi = t; }
   ...
}

EN klasse Box<T stor Number> {...} ville blive oversat ved type sletning til en Boks indeholdende en Objekt felt og dets konstruktør ville blive oversat til Boks (objekt t) . Det er fundamentalt anderledes end en klasse Box<T strækker sig Number> {...} , som ville blive oversat til en Boks indeholdende en Antal felt og dets konstruktør ville blive oversat til Boks (nummer t) . Følgelig er en Box<Number> instansieret fra en klasse Box<T udvider Number> {...} ville være anderledes end en Box<Number> instansiering fra en klasse Box<T super Number> {...} , hvilket sandsynligvis vil skabe forvirring.  Af denne grund giver nedre grænser ikke mening på typeparametre for klasser.

 


 

Nedre grænsetypeparametre for metoder

I forbindelse med metoder og deres argumenttyper kan en typeparameter med en nedre grænse af og til være nyttig.

Eksempel (på en metode, der ville have gavn af en typeparameter med en nedre grænse):

klasse Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public < Et super X , B super Y > B addToMap(Map<A,B> map) { // fejl: type parameter kan ikke have en nedre grænse
      return map.put(first, second);
   }
}
klasse Test { 
   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);
   }
}

De addToMap() metode tilføjer indholdet af parret til et kort.  Ethvert kort, der kan rumme supertyper af X og Y ville gøre.  Kortets sætte() metoden returnerer værdien fundet i kortet for den givne nøgle, hvis der allerede er en nøgleværdiindtastning for nøglen i kortet.  Returværdien af ​​kortets sætte() metoden returneres fra addToMap() metode.  Under disse omstændigheder vil man gerne erklære metoden som vist ovenfor: Kortet er parametriseret med supertyper af parrets typeparametre og addToMap() metodens returtype er kortets værditype.

Da compileren ikke tillader nedre grænser for typeparametre, har vi brug for en work-around.

En løsning, der kommer til at tænke på, er brugen af ​​et jokertegn, fordi jokertegn kan have en lavere grænse.  Her er en løsning med et jokertegn.

Eksempel (på en work-around for det foregående eksempel ved hjælp af jokertegn):

klasse Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public Objekt addToMap(Map< ? Super X , ? super Y > kort) { 
      return map.put(first, second);
   }
}
klasse Test { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = (Antal) pair.addToMap(map);
   }
}

Det virker, bortset fra at der ikke er nogen måde at erklære returtypen som ønsket.  Det ville være supertypen af Y at compileren fanger fra korttypen, men der er ingen syntaks til at specificere den.  Vi må ikke erklære returtypen en “ ? super Y “, fordi” ? super Y ” er et jokertegn og ikke en type og derfor ikke tilladt som returtype. Vi har intet valg og skal bruge Objekt i stedet som vores metodes returtype.  Denne ret uspecifikke returtype tvinger til gengæld opkaldere af addToMap() metode til at støbe returværdien ned fra Objekt til sin egentlige type.  Det var ikke lige det, vi havde i tankerne.

En anden løsning er brugen af ​​statiske metoder.  Her er en work-around med en statisk i stedet for en ikke-statisk metode.

Eksempel (på en work-around for det foregående eksempel ved brug af en statisk metode):

klasse Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public statisk < EN , B ,X udvider A ,Y udvider B > B addToMap(Pair<X,Y> par, Map< EN , B > kort) {
      return map.put(pair.first,pair.second);
   }
}
klasse Test { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = Par .addToMap( par ,kort);
   }
}

Det generiske addToMap() metoden har fire typeparametre: to pladsholdere X og Y for parrets type og to pladsholdere EN og B for kortets type. EN og B er supertyper af X og Y , fordi X og Y   are declared with EN og B som deres øvre grænser. (Bemærk, at den generiske metodes typeparametre X og Y har intet med at gøre Par klasses X og Y parametre.  Navnene X og Y genbruges til den generiske metode for at gøre dem let genkendelige som parrets typeparametre. Ved at bruge fire typeparametre kan vi deklarere den præcise returtype som ønsket: det er den samme type som værditypen på kortet.

Eksempel (på ulovlig brug af typeparameter i statisk kontekst af en generisk klasse):

klasse SomeClass <T> {
  // statisk initialisering, statisk felt, statisk metode
  statisk {
    SomeClass< T > test = ny SomeClass< T >(); // fejl
  }
  privat statisk T globalInfo;          // fejl
  public  statisk T getGlobalInfo() {        // fejl
    return globalInfo;
  }
  // ikke-statisk initialisering, ikke-statisk felt, ikke-statisk metode
  { 
    SomeClass< T > test = ny SomeClass< T >();
  }
  privat        T lokalInfo;
  public         T getLocalInfo() { 
    return localInfo;
  }
  // statiske indlejrede typer
  public statisk klasse Fejl forlænger Undtagelse {
    private final T info;           // fejl
    public Failure( T t) { info = t; }      // fejl
    public T getInfo() { return info; }    // fejl
   }
  privat interface Kopierbar {
    T copy();                       // fejl
  }
  privat enum Stat {
    VALID, INVALID;
    privat T info; // fejl
    public void setInfo( T t) { info = t; } // fejl
    public T getInfo() { return info; }    // fejl
  }
  // ikke-statiske indlejrede typer
  public klasse Accessor {
    public T getInfo() { return localInfo; }
  }
}

Eksemplet illustrerer, at typeparameteren ikke kan bruges i den statiske kontekst af en generisk klasse.  Det viser også, at indlejrede grænseflader og enumtyper betragtes som statiske typer medlemmer af klassen.  Kun indre klasser, det vil sige ikke-statiske indlejrede klasser, kan bruge typeparameteren for den omsluttende generiske klasse.

Omfanget af en grænseflades typeparameter er hele definitionen af ​​grænsefladen, undtagen eventuelle felter eller indlejrede typer.  Dette skyldes, at felter og indlejrede typer defineret i en grænseflade implicit er statiske.

Eksempel (på ulovlig brug af typeparameter i en generisk grænseflade):

interface SomeInterface <T> {
  // felt
  SomeClass< T > værdi = ny SomeClass< T >();  // fejl
  // indlejret type
  klasse Accessor {
    public T getInfo() {            // fejl
      return value.getGlobalInfo();
    }
  }
  // metoder
  T getValue();
}

Eksemplet viser, at felter i en grænseflade implicit er statiske, således at typeparameteren ikke kan bruges nogen steder i deklarationen af ​​et felt i en generisk grænseflade.  På samme måde betragtes den indlejrede klasse som en statisk indlejret klasse, ikke en indre klasse, og af denne grund er brug af typeparameteren overalt i den indlejrede klasse ulovlig.

Løsning for indlejrede statiske klasser og grænseflader:

I tilfælde af statiske indlejrede klasser og grænseflader er dette ikke en større ulykke.  Som en løsning kan vi generere den statiske klasse eller selve grænsefladen.

Eksempel (løsning – generer den indlejrede statiske type):

klasse Indpakning <T> {
  private final T objektet;

  public Wrapper( T t) { objektet = t; }
  public T getWrapper() { return theObject; }   public Immutable <T> makeImmutable() {
        return new Immutable <T> (objektet);
  }
  public Mutable <T> makeMutable() {
        return new Mutable <T> (objektet);
  }
  private static <A> A makeClone(A theObject) { ... }

  public static final class Immutable <A> { // er en generisk klasse nu
        private final EN objektet;

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

Der er ingen sådan løsning for indlejrede enum-typer, fordi de ikke kan være generiske.

Løsning for indlejrede statiske klasser:

Hvis den indlejrede statiske type er en klasse, ville en alternativ løsning være at omdanne den statiske klasse til en ikke-statisk indre klasse.

Eksempel (løsning – brug en indre klasse):

klasse Indpakning <T> {
  private final T objektet;

  public Wrapper( T t) { objektet = t; }
  public T getWrapper() { return theObject; }   public Mutable makeMutable() {
      return denne. ny Mutable(objektet);
  }
  public Immutable makeImmutable() {
      return denne. ny uforanderlig(objektet);
  }
  private static <A> A makeClone(A theObject) { ... }

  public final class Immutable {  // er ikke længere en statisk klasse
      private final T objektet;

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

Denne løsning kommer med en vis mængde overhead, fordi alle indre klasser har en skjult reference til en instans af den ydre type, som de i dette eksempel hverken har brug for eller udnytter; den skjulte reference er kun bagage.  Ofte kombineres indre klasser med grænseflader for at holde den indre klasse en privat implementeringsdetalje af den omsluttende klasse.  Vi kan gøre det samme her.

Eksempel (den tidligere løsning forfinet):

klasse Indpakning <T> {
  private final T objektet;

  public Wrapper( T t) { objektet = t; }
  public T getWrapper() { return theObject; }   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) { ... }

  offentlig grænseflade Immutable<S> {
      S getWrappedItem();
  }
  offentlig grænseflade Mutable<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  privat sidste klasse ImmutableImplementation implementerer Immutable<T> { 

      private final T theObject;

      public ImmutableImplementation(T arg) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  privat klasse MutableImplementation implementerer Mutable<T> { 
        ... similar ...
  }
}

Som du kan se, skal de indlejrede offentlige grænseflader være generiske, mens de indre private klasser kan bruge den omsluttende klasses typeparameter.

 

 

 

 

 

Efterlad et svar

Oplev mere fra Velislav Krastev Blog

Abonner nu for at fortsætte med at læse og få adgang til det fulde arkiv.

Fortsæt med at læse