Java-generieken uitgelegd in korte vorm met voorbeelden

Raadpleeg deze veelgestelde vraag voor meer informatie:

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

Hier volgen enkele belangrijke punten uit deze FAQ.

Voorbeeld (van een generiek type):

klasse Paar<X,Y> { 
  privé X eerst;
  privé Y seconde;   openbaar paar(X a1, Y a2) {
    eerste = a1;
    tweede = a2;
  }
  public X getFirst() { return first; }
  public Y getSecond() { return tweede; }
  public void setFirst(X arg)  { eerste = arg; }
  public void setSecond(Y arg) { seconde = arg; }
}

De klas Paar heeft twee typeparameters X En Y .  Ze worden vervangen door type-argumenten als het om een ​​generiek type gaat Paar wordt geïnstantieerd. In de aangifte bijvoorbeeld Paar<String, Datum> de typeparameter X wordt vervangen door het argument type Snaar En Y wordt vervangen door  Datum .
Het bereik van de id’s X En Y is de volledige definitie van de klasse.  In dit kader de twee typeparameters X En Y worden gebruikt alsof het typen zijn (met enkele beperkingen). In het bovenstaande voorbeeld worden de typeparameters gebruikt als argument en retourneringstype van instantemethoden en de typen instantevelden.
Typeparameters kunnen met grenzen worden gedeclareerd. Grenzen geeft toegang tot methoden van het onbekende type waarvoor het parametertype staat. In ons voorbeal roepen we geen methoden van het onbekende type aan X En Y . Om deze reden zijn de twee typeparameters onbegrensd.

Voorbeeld (van een beton geparametriseerd type):

openbare leegte printPair( Paar<String,Long> paar) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Paar<String,Long> limiet = nieuw Paar<String,Long> ("maximum",1024L);
printPair(limiet);

Voorbeeld (van een met jokertekens geparametriseerd type):

openbare leegte printPair( Paar<?,?> paar) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Paar<?,?> limiet = nieuw Paar<String,Long> ("maximum",1024L);
printPair(limiet);

Alle instantiaties van een generiek type delen dezelfde runtime-typerepresentatie, namelijk de representatie van het onbewerkte type. Bijvoorbeeld de instantiaties van een generiek type Lijst , zoals Lijst<Datum> , Lijst<String> , Lijst<Lang> , enz. hebben tijdens het compileren verschillende statische typen, maar hetzelfde dynamische type Lijst tijdens looptijd.

Een cast bestaat uit twee delen:

  • een statische typecontrole uitgevoerd door de compiler tijdens het compileren en
  • een dynamische typecontrole die tijdens runtime door de virtuele machine wordt uitgevoerd.

Het statische deel sorteert onzinnige casts, die niet kunnen slagen, zoals de cast uit Snaar Dat Datum of van Lijst<String> Dat Lijst<Datum> .

Het dynamische deel gebruikt de runtime-type-informatie en voert tijdens runtime een typecontrole uit.  Het bracht er één op ClassCastException als het dynamische type van het object niet het doeltype (of een subtype van het doeltype) van de cast is. Voorbeelden van casts met een dynamische rol zijn de cast uit Voorwerp Dat Snaar of van Voorwerp Dat Lijst<String> .  Dit zijn de zogenaamde downcasts, van een supertype naar een subtype.

Voorbeelden van betongeparametriseerde typen zijn Lijst<String> , Kaart<String, Datum> , maar niet Lijst<? breidt Nummer> uit van Kaart<String,?> .

Soms wordt verwacht dat a Lijst<Object> zou een supertype zijn van a Lijst<String> , omdat Voorwerp is een supertype van Snaar .  Deze verwachting komt voort uit het feit dat een dergelijk soort relatie bestaat voor arrays:  Voorwerp[] is een supertype van Snaar[] , omdat Voorwerp is een supertype van Snaar . (Dit type relatie staat bekend als covariantie .) De super-subtype-relatie van de componenttypen strekt zich uit tot de overeenkomstige arraytypen. Een dergelijke typerelatie bestaat niet voor instantiaties van generieke typen. (Geparametriseerde typen zijn niet covariant.)

Het ontbreken van een supersubtype-relatie tussen instantiaties van hetzelfde type generieke heeft de volgende gevolgen.  Hier is een voorbeeld.

Voorbeeld:

void printAll(ArrayList<Object> c) {
  voor (Object o: c) 
    Systeem.out.println(o);
} ArrayList<String> lijst = nieuwe ArrayList<String>();
... lijst invullen ...
printAll(lijst);   // fout

A ArrayLijst<String> object kan niet als argument worden doorgegeven aan een methode die om a vraagt ArrayLijst<Object> omdat de twee typen instantiaties zijn van hetzelfde generieke type, maar voor verschillend type argumenten, en om deze reden zijn ze niet compatibel met elkaar.

Concrete geparametriseerde typen zijn concrete instantiaties van een algemeen type.  Het likken bijna typen er zijn slechts enkele beperkingen.  Ze mogen NOOIT voor de volgende doeleinden worden gebruikt:

  • voor het maken van arrays
  • bij het afhandelen van uitzonderingen
  • in een klasse letterlijk
  • in een exemplaarvan uitdrukking

Arrays die elementen bevatten waar een concreet geparameteriseerd type is, zijn illegaal.

Voorbeeld (van illegaal arraytype):

statische leegtetest() {
  Paar<Geheel getal,Geheel getal>[] intPairArr = nieuw Paar<Geheel getal,Geheel getal>[10] ; // fout
  addElements(intPairArr); 
  Paar<Geheel getal,Geheel getal> paar = intPairArr[1];
  Geheel getal i = paar.getFirst();
  paar.setSecond(i);
}
statische leegte addElements( Voorwerp[] objArr) {
  objArr[0] = nieuw Paar<Geheel getal,Geheel getal>(0,0);
  objArr[1] = nieuw Pair<String,String>("","");      // zou moeten mislukken met ArrayStoreException
}

De compiler verbiedt het maken van arrays waarvan het componenttype een concreet geparametriseerd type is, zoals Paar<Geheel getal,Geheel getal> in ons voorbeeld.  We hebben in het voorgaande artikel besproken waarom het redelijk is dat de compiler een kwalificeert Paar<Geheel getal,Geheel getal>[] als illegaal.  Het belangrijkste probleem is dat de compiler en het runtimesysteem ervoor moeten zorgen dat een array een homogeen reeks elementen van hetzelfde type.  Eén van de typecontroles, namelijk de array-store-check die tijdens runtime door de virtuele machine wordt uitgevoerd, slaagt er niet in de inbreukmakende invoeging van een buitenaards element te detecteren. In het voorbeeld is de tweede invoeging in de elementen toevoegen methode zou moeten bomsmken, omdat ze een paar strings aan een array van integrale waarden toevoegden, maar het bomskt niet zoals vergegt. De redenen staan ​​in het vorige bericht.

Als we geen arrays kunnen gebruiken die elementen bevatten waaran het type een concreet geparameteriseerd type is, wat gebruiken we dan als tejdijnse oplossing?

Laten we eens kijken naar drie mogelijke oplossingen:

  • reeks ruwe soorten
  • array van een onbegrensd, met jokertekens geparametriseerd type
  • verzameling in plaats van array

Hoewel arrays van typen met concrete parameters illegaal zijn, zijn verzamelingen van typen met concrete parameters toegestaan.

Een met jokertekens geparametriseerd type is een instantiatie van een generiek type waarbij ten minste een typeargument een jokerteken is.  Voorbeelden van wildcards zijn geparametriseerde typen Collectie<?> , Lijst<? breidt Nummer> uit , Vergelijker<? superString> En Paar<String,?> . Een met jokertekens geparametriseerd type duidt een familie van typen aan die concrete instantiaties van een generiek type omvat.  Het soort jokerteken dat wordt gebruikt, bepaalt welke concrete geparametriseerde typen tot de familie behoren.  Bijvoorbeeld het met jokertekens geparametriseerde type Collectie<?> geeft de familie aan van alle instantiaties van de Verzameling interface, ongeacht het typeargument.  Het met jokertekens geparametriseerde type Lijst<? breidt Nummer> uit geeft de familie van alle lijsttypen aan waarvan het elementtype een subtype is  Nummer .  Het met jokertekens geparametriseerde type Vergelijker<? superString> is de familie van alle instantiaties van de Comparator interface voor type-argumenttypen die supertypes zijn van Snaar.

Een met jokertekens geparametriseerd type is geen type in de reguliere zin (anders dan een niet-geparametriseerde klasse/interface of een onbewerkt type).

Geparametriseerde typen met jokertekens kunnen worden gebruikt voor typen (zoals niet-geparametriseerde klassen en interfaces):

  • als argument- en return-methoden
  • als type veld of lokale referentievariabele
  • als componenttype van een arrayas-typeargument van andere geparametriseerde typen
  • als doeltype bij afgietsels

Niet alleen typen kunnen generiek zijn, maar methoden kunnen ook generiek zijn. Statische en niet-statische methoden, evenals constructors, kunnen typeparameters hebben. De syntaxis voor het declareren van formele typeparameters is vergelijkbaar met de syntaxis voor het generieke type. De sectie voor typeparameters wordt begrensd door punthaken en verchicht vóór het retourtype van de methode. De syntaxis en bekentes zijn identiek aan de typeparameterlijst van een algemeen type.

Hier is het voorbeel van een algemene maximaal methode die de grootste waarde berekent in een verzameling elementen van een onbekend type A .

Voorbeeld (van een generieke methode):

klasse Collecties {
  publiek statisch <A breidt Vergelijkbaar<A>> A uit max(Verzameling< A > xs) {
    Iterator< A > xi = xs.iterator();
    A w = xi.volgende();
    terwijl (xi.hasNext()) {
      A x = xi.volgende();
      als (w.vergelijk(x) < 0) w = x;
    }
    retour w;
  }
} 

De maximaal methode heeft één typeparameter, genaamd A .  Het is een tijdelijke aanduiding voor het elementtype van de verzameling waarop de methode werkt. De parameter type heeft een grens; het moet een type zijn dat een subtype is van Vergelijkbaar<A>

Voorbeeld (van hetzelfde generieke type, deze keer met grenzen):

openbare interface Vergelijkbaar<T> {
  public int CompareTo(T arg);
}
openbare klasse TreeMap<Key breidt Vergelijkbare<Key> uit ,Gegevens>{
  privé statische klasse Entry<K,V> {
     K-sleutel;
     V-waarde;
     Invoer<K,V> links = nul;
     Invoer<K,V> rechts = nul;
     Vermelding<K,V> ouder;
  }
  privé tijdelijke Entry<Key,Data> root = null;
  ...
  privé-invoer<Key,Data> getEntry(sleutelsleutel) {
     Invoer<Sleutel,Gegevens> p = root;
     Sleutel k = sleutel;
     terwijl (p!= nul) {
       int cmp = k. vergelijkTo(p.key) ; 
       als (cmp == 0)
         retour p;
       anders als (cmp < 0)
         p = p.links;
       anders 
         p = p.rechts; 
     }
     retourneer nul;
  }
  public boolean containKey(Sleutelsleutel) {
     return getEntry(sleutel) != null;
  }
  ...
}

In het bovenstaande voorbeeld is dit de parameter type Sleutel heeft de grens Vergelijkbaar<Sleutel> .  Het specificeren van een grens heeft twee effecten:

  • Het geeft toegang tot de methoden die de grens specificeert .  In het voorbeeld de grens Vergelijkbaar<Sleutel> geeft toegang tot de vergelijkTo methode die we willen gebruiken bij de implementatie van onze Boomkaart klas.
  • Alleen typen “binnen de grenzen” kunnen worden gebruikt voor het instantiëren van het algemene type.   In het voorbeeld is een geparametriseerd type zoals Boomkaart<Number,String> zou worden afgewezen, omdat het type Nummer is geen subtype van Vergelijkbaar<Nummer> .  Een geparametriseerd type zoals Boomkaart<String,String> zou worden geaccepteerd, omdat het type String binnen de grenzen valt, d.w.z. een subtype is van Vergelijkbaar<String> .

Alle klassen, interfaces en enum-typen kunnen worden gebruikt als type-parametergebonden, inclusief geneste en interne typen.  Noch primitieve typen, noch arraytypen mogen worden gebruikt als typeparametergrens.

Voorbeelden (van type parameterlimieten):

klasse X0 <T breidt uit int > { ... }      // fout
klasse X1 <T breidt zich uit Voorwerp[] > { ... } // fout
klasse X2 <T breidt zich uit Nummer > { ... }
klasse X3 <T breidt zich uit Snaar > { ... }
klasse X4 <T breidt uit Uitvoerbaar > { ... }
klasse X5 <T breidt zich uit Draad.State > { ... }
klasse X6 <T breidt zich uit Lijst > { ... }
klasse X7 <T breidt zich uit Lijst<String> > { ... }
klasse X8 <T breidt zich uit Lijst<? breidt Nummer> uit > { ... }
klasse X9 <T breidt zich uit Vergelijkbaar<? supernummer> > { ... }
klasse X10<T breidt zich uit Kaart.Invoer<?,?> > { ... }

Het codevoorbeeld laat zien dat primitieve typen zoals int en arraytypen zoals Voorwerp[] zijn niet verwekt als typeparametergebonden.

Klassetypen, zoals Nummer van Snaar en interfacetypen, zoals Uitvoerbaar , zijn bedoeld als typeparametergebonden.

Enum-typen, zoals Draad.State zijn ook toegestaan ​​als typeparametergebonden. Draad.State is een voorbeel van een genest type dat wordt gebruikt als typeparametergebonden.  Ook niet-statische binnenuitvoeringen zijn begeanten.

Ruwe typen zijn ontstaan ​​als typeparametergebonden; Lijst is een voorbeeld.

Geparametriseeerde typen zijn begeanten als typeparametergebonden, inclusief beton geparametriseeerde typen zoals Lijst<String> , begrensde typen met jokertekens, zoals Lijst<? breidt Nummer> uit En Vergelijkbaar<? superLong> en onbegrensde typen met jokertekens, zoals Kaart.Invoer<?,?> .  Een grens die een met jokertekens geparametriseerd type is, staat als typeargument alle typen toe die horen tot de typefamilie die door het jokerteken wordt anjärnten.  De met jokertekens geparametriseerde typegrens geeft slechts toegang tot velden en methoden; De beperkingen zijn afhankelijk van het soort wildcard.

Voorbeeld (van met jokertekens geparametriseerd type als type parameter gebonden):

klasse X< T breidt Lijst< uit? breidt Nummer> uit > {
  public void someMethod(T t) {
    t.add(nieuw lang(0L));    // fout
    Getal n = t.verwijder(0);
  }
}
klasse Test {
  public static void main(String[] args) {
     X<ArrayLijst< Lang >> x1 = nieuwe X<ArrayList<Long>>(); 
     X<ArrayLijst< Snaar >> x2 = nieuwe X<ArrayList<String>>(); // fout
  }
}

Referentievariabelen van het type T (de typeparameter) wordt behandeld als referencievariabelen van het jokertekentype (de typeparameter gebonden).  In ons voorbeeld is de consequentie dat de compiler het aanroepen van methoden afwijst die een argument aanvaardt van het “onbekende” type waarvoor het parametertype staat, zoals Lijst.toevoegen , omdat de grens een met jokertekens geparametriseerd type is met een bovengrens.
uiteindelijk de grens Lijst<? breidt Nummer> uit bepaalt de typen die kunnen worden gebruikt als typeargumenten. De compiler accepteert alle typeargumenten die tot de typefamilie behoren Lijst<? breidt Nummer> uit , dat wil zeggen, alle subtypen van Lijst met een type-argument dat een subtype is van Nummer .

Een typeparameter kan worden gebruikt als grens voor een andere typeparameter.

Voorbeeld (van een typeparameter die wordt gebruikt als een typeparametergebonden):

klasse Drievoudig <T> {
  privé T fst, snd, trd;
  openbare < U verlengt T , V verlengt T , W verlengt T > Drievoudig(U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

In dit voorbeeld de parameter type T van de geparametriseerde klasse wordt gebruikt als grens van de typeparameters U ,  V En W van een geparametriseerde instantemethode van die klasse.

Verdere mogelijkheden voor het gebruik van typeparameters als grenzen van andere typeparameters zijn onder meer situaties waarten een genest type is gedefinieerd binnen een generiek type of een locale klasse is gedefinieerd binnen een generieke methode.  Het is zelfs toegestaan ​​om een ​​typeparameter te gebruiken als grens van een typeparameter in dezelfde typeparametersectie.

Een grens die een klasse is, geeft toegang tot al zijn openbeare leden, dat wil zeggen openbeare velden, methoden en geneste typen. Alleen constructors mogen niet worden gemaakt, omdat er geen garantie is dat een subklasse van de gebonden dezelfde constructors als de gebonden heeft.

Voorbeeld (van een klasse die wordt gebruikt als grens van een typeparameter):

openbare klasse SuperKlasse {     // statische leden
    openbare opsomming EnumType {DIT, DAT}
    publiek statisch Object staticField;
    openbare statische leegte staticMethod() { ... }

    // niet-statische leden
    openbare klasse InnerClass { ... }
    openbaar object nonStaticField;
    public void nonStaticMethod() { ... }

    // constructeurs
    openbare SuperKlasse() { ... }

    // privé leden
    privé-object privateField; 

    ...
}

openbare eindklasse SomeClass<T breidt zich uit SuperKlasse > {
    privé T-object;
    publiek SomeClass(T t) {object = t; } 

    openbare tekenreeks toString() {
        opbrengst
         "statisch genest type   : "+T.EnumType.class+"\n" 
        +"statisch veld          : "+T.staticField+"\n"
        +"statische methode         : "+T.staticMethod()+"\n"
        +"niet-statisch genest type: "+T.InnerClass.class+"\n"
        +"niet-statisch veld      : "+object.nonStaticField+"\n"
        +"niet-statische methode     : "+object.nonStaticMethod()+"\n"
        +"constructor           : "+(nieuwe T())+"\n"                   // fout
        +"privélid        : "+object.privateField+"\n"          // fout 
        ;
    }
}

De grens SuperKlasse geeft toegang tot de geneste typen, statische velden en methoden en niet-statische velden en methoden.  Alleen de constructor is niet toegankelijk.  Dit komt omdat constructors niet worden geërfd. Elke subklasse definieert zijn eigen constructors en hoeft de constructors van zijn superklasse niet te ondersteunen.  Er is dus geen garantie dat een subklasse van SuperKlasse zal dezelfde constructeur hebben als zijn superklasse.

De context waarten Enum<E breidt Enum<E>> uit ” verschijnt is de verklaring van de Enum klasse in pakket java.lang :

openbare abstracte klasse Enum<E breidt Enum<E>> uit {
  ...
}

Het soort Enum is de gemeenschappelijke basisklasse van alle opsommingstypen.  In Java is een opsommingstype zoals Kleur wordt vertaald naar een klasse Kleur dat strekt zich uit Enum<Kleur> . Het doel van de superklasse Enum is om functionaliteit te bieden die gemenschappelijk is voor alle opsomungstypen.

Hier is een schets van klasse Enum :

openbare abstracte klasse Enum< E breidt Enum<E>> uit implementeert Vergelijkbaar< E >, serialiseerbaar {
  privé uiteindelijke tekenreeksnaam;
  openbare finale Stringnaam() { ... }   privé finale int rangtelwoord;
  publieke finale int ordinal() { ... }

  protected Enum(Stringnaam, int rangtelwoord) { ... }

  public String           toString() { ... }
  public final boolean is gelijk aan (Object anders) { ... }
  openbare finale int hashCode() { ... }
  beschermde uiteindelijke Objectkloon() gooit CloneNotSupportedException { ... }
  openbare finale int CompareTo( E o) { ... }

  openbare finale Class< E > getDeclaringClass() { ... }
  openbare statische <T breidt Enum<T>> T waardeOf(Class<T> enumType, String naam) { ... }
}

Het verrassende kenmerk in de verklaring “ Enum<E breidt Enum<E>> uit ” is het feit dat de nieuw gedefinieerde klasse Enum en de nieuw gedefinieerde typeparameter E verschijnen binnen de grens van dezelfde typeparameter.  Het betekent dat de Enum type moet worden geïnstantieerd voor een van zijn subtypen.  Om te begrijpen waarom dit logisch is, moet je bedenken dat elk enum-type wordt vertaald in een subtype van Enum .

Hier is het gekunstelde enum-type Kleur :

opsomming Kleur {ROOD, BLAUW, GROEN}

De compiler vertaalt het naar de volgende klasse:

openbare eindklasse Kleur strekt zich uit Enum<Kleur> {
  openbare statische definitieve kleur[]-waarden() { return (kleur[])$VALUES.clone(); }
  public static KleurwaardeOf(Stringnaam) { ... }   privé Kleur(String s, int i) { super(s, i); }

  openbare statische finale Kleur ROOD;
  openbare statische finale Kleur BLAUW;
  openbare statische finale Kleur GROEN;

  privé statische uiteindelijke kleur $VALUES[];

  statisch {
    ROOD = nieuwe kleur("ROOD", 0);
    BLAUW = nieuwe kleur("BLAUW", 1);
    GROEN = nieuwe kleur("GROEN", 2);
    $VALUES = (nieuwe kleur[] { ROOD, BLAUW, GROEN });
  }
}

De erfenis heeft tot gevolg dat de Kleur type erft alle methoden die erin zijn geïmplementeerd Enum<Kleur> .  Onder hen bevindt zich de vergelijkTo methode.  De Kleur. vergelijkTo methode zou waarschijnlijk a moeten nemen Kleur als argument.  Om dit mogelijk te maken, klasse Enum is generiek en de Enum.vergelijkTo methode duurt Enum ‘s typeparameter E als argument.  Als resultaat typt u Kleur afgeleid van Enum<Kleur> erft een vergelijkTo methode die een Kleur zoals en argument, precies zoals het hoort.
Als we de verklaringen ontleden “ Enum<E breidt Enum<E>> uit ” we kunnen zien dat dit patroon verschillende aspecten heeft.

Ten eerste is het feit dat het parametertype gebonden is, het type zelf is: “ Enum <E breidt uit Enum <E>> “. Het zorgt ervoor dat alleen subtypen van type zijn Enum zijn toegestaan ​​als typeargumenten. (Theoretisch, typ Enum zou op zichzelf kunnen worden geïnstantieerd, zoals in Enum<ENum> , maar dit is zeker niet de intelling en het is moeilijk een situatie voor te stellen waarten een kleine instantiatie nuttig zou zijn.)

Ten tweede is het feit dat het parametergebonden type het geparametriseerde type is Enum <E> , die de parameter type gebruikt E als het typeargument van de grens. Deze declaratie zorgt ervoor dat de overervingsrelatie tussen een subtype en een instantiatie van Enum is van de vorm X breidt Enum<X> uit “. Een subtype zoals “ X breidt Enum<Y> uit ” kan niet worden gedeclareerd vanwege het argumenttype Y zou niet binnen de perken blijven; alleen subtypes van Enum<X> zijn binnen de perken.

Op de derde plaats is het feit dat Enum is in de eerste plaats generiek.  Het betekent dat sommige van de methoden van klasse Enum neem een ​​argument of retourneer een waarde van een onbekend type (of afhankelijk van een onbekend type). Zoals we al weten, zal dit onbekende type later een subtype zijn X van Enum<X> .  Daarom in het geparametriseerde type Enum<X> Bij deze methoden is het subtype betrokken X , en ze worden overgenomen in het subtype X .  De vergelijkTo methode is een vergelijkbare methode, het wordt gegërfd van de superklasse in elke subklasse en heeft in elk geval een subklasse-specifieke handtekening.

Kortom, de verklaring “ Enum<E breidt Enum<E>> uit ” kan worden gecodeerd als: Enum is een generiek type dat alleen kan worden geïnstantieerd voor zijn subtypen, en die subtypen zullen een aantal nuttige methoden erven, waarvan sommige subtype-specifieke argumenten gebruiken (of anderszins afhankelijk zijn van het subtype).

Ondergrenstypeparameters van klassen

Typeparameters kunnen verschillende grenzen hebben, zoals in klasse Box<T breidt Nummer> uit {...} . Maar een typeparameter kan geen ondergrens hebben, dat wil zeggen een constructie zoals klasse Box<T Geweldig Nummer> {...} is niet toegestaan.  Waarom niet?  Het antwoord is: het heeft geen zin, want als het was, zou je er niets mee kopen.  Laten we eens kijken waarom ondergrenstypeparameters van klassen verwarrend zijn door te erønkeren wat een bovengrens voor een typeparameter beekent.

De bovengrens voor een typeparameter heeft drie effecten:

    1. Beperkte instantiatie. De bovengrens beperkt de set typen die kunnen worden gebruikt voor het instantiëren van het generieke type.  Als we verklaren a klasse Box<T breidt Nummer> uit {...} dan zou de compiler ervoor zorgen dat alleen subtypen van Nummer kan worden gebruikt als typeargument.  Dat wil zeggen, een Doos<Nummer> of een Doos<Lang> is toegestaan, maar a Vak<Object> van Vak<String> zou worden afgewezen

Voorbeeld (van beperkte instantiatie vanwege een bovengrens voor een typeparameter):
klasse Box<T breidt Nummer> uit { 
   privé T-waarde; 
   openbare Box(T t) { waarde = t; } 
   ...
}  klasse Test { 
   public static void main(String[] args) { 
       Box<Long> boxOfLong = nieuwe Box<Long>(0L);   // prima
       Box<String> boxOfString = nieuwe Box<String>(""); // fout: String ligt niet binnen de grenzen
   }
}

  1. Toegang tot niet-statische leden. De bovengrens geeft toegang tot alle openbare niet-statische methoden en velden van de bovengrens.  Bij de uitvoering van onze klasse Box<T breidt Nummer> uit {...} we kunnen alle openbare niet-statische methoden aanroepen die in de klasse zijn gedefinieerd Nummer , zoals intWaarde() bijvoorbeeld.  Zonder de bovengrens zou de compiler een dergelijke aanroep afwijzen. Voorbeeld (van toegang tot niet-statische leden vanwege een bovengrens voor een typeparameter):

klasse Box<T breidt Nummer> uit { 
   privé T-waarde; 
   openbare Box(T t) { waarde = t; } 
   public int increment() {retourwaarde.intValue()+1; } // <= zou een fout zijn zonder de nummergrens
   ...
} 

    1. Typ Wissen. De meest linkse bovengrens wordt gebruikt voor het wissen van het type en vervangt de typeparameter in de bytecode.  In onze klasse Box<T breidt Nummer> uit {...} alle voorkomen van T vervangen door de bovengrens Nummer .  Bijvoorbeeld als klasse Doos heeft een privéveld van type T en een methode ongeldige set (T-inhoud) voor het instellen van dit privéveld, dan zou het veld van het type zijn Nummer na het wissen van het type en de methode zou worden vertaald naar een methode void set (nummerinhoud) .

Voorbeeld (van gebruik van de bovengrens voor een typeparameter bij het wissen van type – voor soort wissen):
klasse Box< T breidt Nummer uit > { 
   privé T waarde; 
   openbare box( T t) { waarde = t; } 
   ...
} 

      Voorbeeld (van gebruik van de bovengrens voor een typeparameter bij het wissen van type –

na

      soort wissen):

 

klasse Box { 
   privé Nummer waarde; 
   openbare box( Nummer t) { waarde = t; } 
   ...
}

    Bovendien verschijnt de meest linkse bovengrens op andere locaties, zoals automatisch ingevoegde afgietsels en brugmethoden.

Als er ondergrenzen zouden worden toegestaan ​​voor typeparameters, welke bijwerkingen zouden of zouden deze dan moeten hebben? Als een constructie zoals klasse Box<T Geweldig Nummer> {...} waren begeanten, wat zou dat betekenen?  Wat zouden de drie bijwerkingen van een hogere parametergrens – bereketen instantiatie, toegang tot niet-statisch deksel, typeverwijdering – betekenen voor een ondergrens?

Beperkte instantiaties. De compiler zou de set typen kunnen beperken die kunnen worden gebruikt voor het instantiëren van het generieke type met een ondergrenstypeparameter.  De compiler kan bijvoorbeeld instantiaties toestaan ​​zoals Doos<Nummer> En Vak<Object> van een Doos<T Geweldig Nummer> en instantiaties zoals Doos<Lang> van Doos<Kort> .  Dit zou een effect zijn dat in lijn ligt met het restrictieve neveneffect dat is beschreven voor de hogere parametergrenzen.

Toegang tot niet-statische leden. Een lagere parametergrens voor het type geeft geen toegang tot bepaalde methoden die verder gaan dan die welke van de klasse zijn geërfd Voorwerp . In het voorbeeld van Doos<T Geweldig Nummer> de supertypes van Nummer hebben niets met elkaar gemeen, behalve dat het referentietypes zijn en dus subtypes ervan Voorwerp .  De compiler kan er niet van uitgaan dat het veld type T is van type Nummer of een subtype daarvan.  In plaats daarvan het veld type T kan van elk supertype zijn Nummer , zoals Serialiseerbaar van Voorwerp .  Het aanroepen van een methode zoals intWaarde() is niet langer typeveilig en de compiler zou het moeten afwijzen. Als gevolg hiervan zou de lagere typeparametergrens geen toegang geven tot niet-statische leden die verder gaan dan de leden die in de klasse zijn gedefinieerd Voorwerp en heeft dit semelde effect als “no bound”.

Typ Wissen .  Als we deze logica volgen, heeft het geen zin om het voorkomen van de parameter type te vervangen door de meest linkse ondergrens.  Het declareren van een methode zoals de constructor Doos( T T) als constructeur Doos( Nummer T) heeft geen zin, gezien dat T wordt vervangen door een supertype van Nummer .  Een Voorwerp kan terecht in een instantiatie aan de constructor worden doorgegeven Vak<Object> en de constructeur zou het afwijzen.  Dit zou volkomen verkeerd zijn.   Het wissen van het type zou dus alle exemplaren van de typevariabele vervangen T per type Voorwerp , en niet volgens de ondergrens ervan.  Nogmaals, de ondergrens zou een effect hebben als “geen grens”.

Wil je erachter komen wat het zou betekenen als beide lager zijn? _En_ bovengrenzen waren begeanten?   Persoonlijk wil ik er niet over nadenken en zou ik het liever hebben onder “niet beheersbaar” platen, als u dat toestaat.

Het komt erop neer: dat alles is een “ Geweldig ‘bound’ zou je kopen is de beperking waar alleen supertypes van mogen genen Nummer kunnen worden gebruikt als typeargumenten.  En zelfs dat wordt vaak verkeerd begrepen.  Dat zou het NIET betekenen klasse Box<T super Nummer> {...} bevat alleen exemplaren van supertypes van Nummer .  Integendeel, zoals het voorbeeld laat zien!

Voorbeeld (van gebruik van de bovengrens voor een typeparameter bij het wissen van type – voor soort wissen):

klasse Box< T supernummer > {
   privé T waarde;
   openbare box( T t) { waarde = t; }
   ...
}

Voorbeeld (van gebruik van de bovengrens voor een typeparameter bij het wissen van type – na soort wissen):

klasse Box {
   privé Voorwerp waarde;
   openbare box( Voorwerp t) { waarde = t; }
   ...
}

A klasse Box<T Geweldig Nummer> {...} zou door typeverwijdering worden vertaald naar a Doos met daarin een Voorwerp veld en de constructor ervan zou worden vertaald Vak(Object t) . Dat is fundamenteel anders dan a klasse Box<T strekt zich uit Nummer> {...} , wat zou worden vertaald naar een Doos met daarin een Nummer veld en de constructor ervan zou worden vertaald Vak(Nummer t) . Bijgevolg is een Doos<Nummer> geïnstantieerd uit een klasse Box<T breidt Nummer> uit {...} zou anders zijn dan a Doos<Nummer> instantiatie van a klasse Box<T super Nummer> {...} , wat waarschijnt tot gevaar zal leiden.  Om deze reden zijn ondergrenzen niet zinvol voor typeparameters van klassen.

 


 

Ondergrenstypeparameters van methoden

In combinatie met methoden en hun argumenttypen kan een parametertype met een ondergrens en toe utzel zijn.

Voorbeeld (van een methode die baat zou hebben bij een typeparameter met een ondergrens):

klasse Paar<X,Y> {
   privé X eerst;
   privé Y seconde;
   ...
   openbare < Een super-X , B super Y > B addToMap(Map<A,B> map) {// error: type parameter kan geen ondergrens hebben
      return map.put(eerste, tweede);
   }
}
klasse Test { 
   public static void main(String[] args) { 
     Pair<String,Long>-paar = nieuw Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Getalnummer = pair.addToMap(kaart);
   }
}

De toevoegenToMap() methode voegt de inhoud van het paar toe aan een kaart.  Elke kaart die supertypes kan bevatten X En Y zou doen.  De kaart neerzetten() method retourneert de waarde die in de kaart voor de gegeven sleutel is gevonden, als er al een sleutelwaarde-invoer voor de sleutel in de kaart bestaat.  De geretourneerde waarde van de kaart neerzetten() methode wordt geretourneerd uit de toevoegenToMap() methode.  Onder deze omstandigheden zou men de methode willen declareren zoals hierboven weergegeven: De kaart is geparametriseerd met supertypes van de typeparameters van het paar en de toevoegenToMap() Het retourtype van de methode is het waardetype van de kaart.

Omdat de compiler geen ondergrenzen voor typeparameters toestaat, hebben we een oplossing nodig.

Eén oplossing die je opkomt is het gebruik van een jokerteken, omdat jokertekens een ondergrens kunnen hebben.  Hier is een oplossing met behulp van een wildcard.

Voorbeeld (van een oplossing voor het vorige voorbeeld met behulp van jokertekens):

klasse Paar<X,Y> {
   privé X eerst;
   privé Y seconde;
   ...
   publiek Voorwerp addToMap(Map< ? Super X , ? super Y >-kaart) { 
      return map.put(eerste, tweede);
   }
}
klasse Test { 
   public static void main(String[] args) { 
     Pair<String,Long>-paar = nieuw Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Nummer nummer = (Nummer) pair.addToMap(kaart);
   }
}

Het werkt, behalve dat er geen manier is om het retourtype naar wens te declareren.  Het zou het supertype zijn van Y de compiler van het kaarttype opvangt, maar er is geen syntaxis om dit te specificeren.  We mogen het retourtype niet declareren als een “ ? super Y “, omdat “ ? super Y ” is een wildcard en geen type en daarom niet toegestaan ​​als retourtype. We hebben geen keuze en moeten deze gebruiken Voorwerp in plaats daarvan als het retourtype van onze methode.  Dit nogal onspecifieke retourtype dwingt op zijn beurt bellers van de toevoegenToMap() methode om de retourwaarde naar beneden te gieten Voorwerp naar zijn verkeilte type.  Dit is niet precies wat we in gedachten hadden.

Een andere oplossing is het gebruik van statische methoden.  Hier is een oplossing met een statische in plaats van een niet-statische methode.

Voorbeeld (van een oplossing voor het vorige voorbeeld met behulp van een statistische methode):

klasse Paar<X,Y> {
   privé X eerst;
   privé Y seconde;
   ...
   publiek statisch < A , B ,X verlengt A ,J verlengt B > B addToMap(Pair<X,Y>-paar, Map< A , B >-kaart) {
      return map.put(paar.eerste,paar.tweede);
   }
}
klasse Test { 
   public static void main(String[] args) { 
     Pair<String,Long>-paar = nieuw Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Nummer nummer = Paar .addToMap( paar ,kaart);
   }
}

De generieke toevoegenToMap() methode heeft vier typeparameters: twee tijdelijke aanduidingen X En Y voor het type van het paar en twee tijdelijke aanduidingen A En B voor het kaarttype. A En B zijn supertypes van X En Y , omdat X En Y   worden verklaard met A En B als hun bovengrens. (Merk op dat de typeparameters van de generieke methode X En Y hebben er niets mee te maken Paar klasse X En Y parameters.  De namen X En Y worden hergebruikt voor de algemene methode om ze gemakkelijk herkenbaar te maken als de typeparameters van het paar. Met behulp van vier typeparameters kunnen we het nauwkeurige retourtype naar wens verduidelijken: het type selecteren als het waardetype van de kaart.

Voorbeeld (van illegaal gebruik van typeparameter in statische context van een generieke klasse):

klasse SommigeKlasse <T> {
  // statische initialisator, statisch veld, statische methode
  statisch {
    SommigeKlasse< T >-test = nieuwe SomeClass< T >(); // fout
  }
  privé statisch T globalInfo;          // fout
  publiek  statisch T getGlobalInfo() {        // fout
    retourneer globalInfo;
  }
  // niet-statische initialisator, niet-statisch veld, niet-statische methode
  { 
    SommigeKlasse< T >-test = nieuwe SomeClass< T >();
  }
  privé        T lokaleInfo;
  publiek         T getLocalInfo() { 
    lokaleInfo retourneren;
  }
  // statische geneste typen
  publiek statisch klas Mislukking verlengt uitzondering {
    besloten finale T informatie;           // fout
    openbare mislukking( T t) {info = t; }      // fout
    publiek T getInfo() {retourinfo; }    // fout
   }
  privé interface Kopieerbaar {
    T kopiëren();                       // fout
  }
  privé opsomming Staat {
    GELDIG, ONGELDIG;
    privé T informatie; // fout
    openbare ongeldige setInfo( T t) {info = t; } // fout
    publiek T getInfo() {retourinfo; }    // fout
  }
  // niet-statische geneste typen
  publiek klas Accessoire {
    publiek T getInfo() { return localInfo; }
  }
}

Het voorbeeld illustreert dat het parametertype niet kan worden gebruikt in de statische context van een generieke klasse.  Het laat ook zien dat geneste interfaces en enum-typen worden beschouwd als statische typeleden van de klasse.  Alleen binnenklassen, dat wil zeggen niet-statische geneste klassen, kunnen het parametertype van de omslustende algemene klasse gebruiken.

Het bereik van het parametertype van een interface is de volledige definitie van de interface, met uitzondering van eventuele velden met geneste typen.  Dit komt omdat velden en geneste typen in een interface impliciet statisch zijn gedefinieerd.

Voorbeeld (van illegaal gebruik van parametertype in een generieke interface):

interface SommigeInterface <T> {
  // veld
  SommigeKlasse< T >-waarde = nieuwe SomeClass< T >();  // fout
  // geneste soort
  klas Accessoire {
    publiek T getInfo() {            // fout
      retourwaarde.getGlobalInfo();
    }
  }
  // methoden
  T getWaarde();
}

Het voorbeeld laat zien dat velden van een interface impliciet statisch zijn, zodat de parameter type nergens kan worden gebruikt bij de declaratie van een veld van een generieke interface.  Op dezelfde manier wordt de geneste klasse beschouwd als een statische geneste klasse, niet als een binnenklasse, en om deze reden is het gebruik van de parameter type waar dan ook in de geneste klasse illegaal.

Oplossing voor geneste statische klassen en interfaces:

Bij statische klassen en interfaces is dit geen grote ramp.  Als tijdelijke oplossing genereren we de statische interfaceklasse zelf.

Voorbeeld (oplossing: genereer het geneste statische type):

klasse Wrapper <T> {
  besloten finale T hetObject;

  openbare verpakking( T t) { hetObject = t; }
  publiek T getWrapper() { return theObject; }   publiek Onveranderlijk <T> maak onveranderlijk() {
        retourneer nieuw Onveranderlijk <T> (hetObject);
  }
  publiek Veranderlijk <T> makeMutable() {
        retourneer nieuw veranderlijk <T> (hetObject);
  }
  privé statisch <A> A makeClone(A theObject) { ... }

  openbare statische laatste klasse Onveranderlijk <A> { // is nu een generieke klasse
        besloten finale A hetObject;

        openbaar Onveranderlijk( A arg) {
            hetObject = makeClone(arg);
        }
        publiek A getWrappedItem() {
            return makeClone(hetObject);
        }
  }
  openbare statische klasse Muteerbaar <A> {
        ... vergelijkbaar ...
  }
}

Er bestaat geen dergelijke oplossing voor geneste enum-typen, omdat deze niet generiek kunnen zijn.

Oplossing voor geneste statische klassen:

Als het geneste statische type een klasse is, zou een alternatieve oplossing erin bestaan ​​​​de statische klasse in een niet-statische binnenklasse te mederenden zijn.

Voorbeeld (oplossing: gebruik een binnenklasse):

klasse Wrapper <T> {
  besloten finale T hetObject;

  openbare verpakking( T t) { hetObject = t; }
  publiek T getWrapper() { return theObject; }   public veranderlijk makeMutable() {
      opbrengst dit. nieuw Mutable(hetObject);
  }
  public Onveranderlijk makeImmutable() {
      opbrengst dit. nieuw onveranderlijk(hetObject);
  }
  privé statisch <A> A makeClone(A theObject) { ... }

  openbare eindklasse Onveranderlijk {  // is niet langer een statische klasse
      besloten finale T hetObject;

      openbaar Onveranderlijk( T arg) {
          hetObject = makeClone(arg);
      }
      publiek T getWrappedItem() {
          return makeClone(hetObject);
      }
  }
  openbare klasse Veranderlijk 
        ... vergelijkbaar ...
  }
}

Deze oplossing brengt een grote overhead met zich mee, omdat alle interne klassen een verborgen verwijzing hebben naar een exemplaar van het buinste type, waar ze in dit geval geen behoefte hebben aan het hebben en er ook geen voordeel uit halen; de referentie verbergen is slechts bagage.  Vaak worden interne klassen gecombineerd met interfaces om de interne klasse een privé-implementatiedetail van de omsluendte klasse te houden.  Hetzelfde kunnen wij hier doen.

Voorbeeld (de vorige oplossing ingewikkeld):

klasse Wrapper <T> {
  besloten finale T hetObject;

  openbare verpakking( T t) { hetObject = t; }
  publiek T getWrapper() { return theObject; }   publiek Veranderlijk <T> makeMutable() {
      return this.new Mutable(theObject);
  }
  publiek Onveranderlijk <T> maak onveranderlijk() {
      return this.new Onveranderlijk (hetObject);
  }
  privé statisch <A> A makeClone(A theObject) { ... }

  publieke interface Onveranderlijk<S> {
      S getWrappedItem();
  }
  publieke interface Veranderlijk<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  privé laatste les Onveranderlijke Implementatie implementeert onveranderlijk<T> { 

      privé finale T theObject;

      public ImmutableImplementation(T arg) {
          hetObject = makeClone(arg);
      }
      openbare T getWrappedItem() {
          return makeClone(hetObject);
      }
  }
  privé klasse MutableImplementatie implementeert Mutable<T> { 
        ... vergelijkbaar ...
  }
}

Zoals u kunt zien, moeten de geneste openbare interfaces algemeen zijn, terwijl de binnenste privéklassen het parametertype van de omsluendte klasse kunnen gebruiken.

 

 

 

 

 

Laat een reactie achter