De basisprincipes van Java Generics

1. Inleiding

Java Generics zijn geïntroduceerd in JDK 5.0 met als doel bugs te verminderen en een extra abstractielaag over typen toe te voegen.

Dit artikel is een korte introductie tot Generics in Java, het doel erachter en hoe ze kunnen worden gebruikt om de kwaliteit van onze code te verbeteren.

2. De behoefte aan generieke geneesmiddelen

Laten we ons een scenario voorstellen waarin we een lijst in Java willen maken om op te slaan Geheel getal; we kunnen in de verleiding komen om te schrijven:

Lijstlijst = nieuwe LinkedList (); list.add (nieuw geheel getal (1)); Geheel getal i = lijst.iterator (). Next (); 

Verrassend genoeg zal de compiler klagen over de laatste regel. Het weet niet welk gegevenstype wordt geretourneerd. De compiler vereist een expliciete casting:

Geheel getal i = (Geheel getal) list.iterator.next ();

Er is geen contract dat kan garanderen dat het retourtype van de lijst een Geheel getal. De gedefinieerde lijst kan elk object bevatten. We weten alleen dat we een lijst ophalen door de context te inspecteren. Als we naar typen kijken, kan het alleen garanderen dat het een Voorwerp, vereist dus een expliciete cast om ervoor te zorgen dat het type veilig is.

Deze cast kan vervelend zijn, we weten dat het datatype in deze lijst een Geheel getal. De cast maakt ook onze code onoverzichtelijk. Het kan typegerelateerde runtimefouten veroorzaken als een programmeur een fout maakt bij het expliciet casten.

Het zou veel gemakkelijker zijn als programmeurs hun intentie zouden kunnen uitdrukken om specifieke typen te gebruiken en de compiler kan de juistheid van een dergelijk type garanderen. Dit is het kernidee achter generieke geneesmiddelen.

Laten we de eerste regel van het vorige codefragment wijzigen in:

Lijstlijst = nieuwe LinkedList ();

Door de diamantoperator met het type toe te voegen, beperken we de specialisatie van deze lijst tot Geheel getal type, d.w.z. we specificeren het type dat in de lijst zal worden vastgehouden. De compiler kan het type afdwingen tijdens het compileren.

In kleine programma's lijkt dit misschien een triviale toevoeging, maar in grotere programma's kan dit aanzienlijk robuuster zijn en het programma gemakkelijker leesbaar maken.

3. Generieke methoden

Generieke methoden zijn die methoden die zijn geschreven met een enkele methode-declaratie en die kunnen worden aangeroepen met argumenten van verschillende typen. De compiler zorgt voor de juistheid van het type dat wordt gebruikt. Dit zijn enkele eigenschappen van generieke methoden:

  • Generieke methoden hebben een typeparameter (de diamantoperator die het type omsluit) vóór het retourneringstype van de methode-declaratie
  • Type parameters kunnen worden begrensd (grenzen worden later in het artikel uitgelegd)
  • Generieke methoden kunnen verschillende typeparameters hebben, gescheiden door komma's in de methodehandtekening
  • Method body voor een generieke methode is net als een normale methode

Een voorbeeld van het definiëren van een generieke methode om een ​​array naar een lijst te converteren:

openbare lijst fromArrayToList (T [] a) {return Arrays.stream (a) .collect (Collectors.toList ()); }

In het vorige voorbeeld is de in de methode houdt handtekening in dat de methode te maken heeft met generiek type T. Dit is zelfs nodig als de methode ongeldig wordt geretourneerd.

Zoals hierboven vermeld, kan de methode omgaan met meer dan één generiek type, waar dit het geval is, moeten alle generieke typen worden toegevoegd aan de handtekening van de methode, bijvoorbeeld als we de bovenstaande methode willen wijzigen om met type om te gaan. T en typ G, zou het als volgt moeten worden geschreven:

openbare statische lijst fromArrayToList (T [] a, Function mapperFunction) {return Arrays.stream (a) .map (mapperFunction) .collect (Collectors.toList ()); }

We passeren een functie die een array converteert met de elementen van het type T om een ​​lijst te maken met elementen van het type G. Een voorbeeld zou zijn om te converteren Geheel getal naar zijn Draad vertegenwoordiging:

@Test openbare leegte gegevenArrayOfIntegers_thanListOfStringReturnedOK () {Geheel getal [] intArray = {1, 2, 3, 4, 5}; Lijst stringList = Generics.fromArrayToList (intArray, Object :: toString); assertThat (stringList, hasItems ("1", "2", "3", "4", "5")); }

Het is vermeldenswaard dat Oracle aanbeveelt om een ​​hoofdletter te gebruiken om een ​​generiek type weer te geven en om een ​​meer beschrijvende letter te kiezen om formele typen weer te geven, bijvoorbeeld in Java Collections T wordt gebruikt voor type, K voor sleutel, V. voor waarde.

3.1. Bounded Generics

Zoals eerder vermeld, kunnen typeparameters worden begrensd. Bounded betekent "beperkt“, Kunnen we typen beperken die door een methode kunnen worden geaccepteerd.

We kunnen bijvoorbeeld specificeren dat een methode een type en al zijn subklassen (bovengrens) of een type al zijn superklassen (ondergrens) accepteert.

Om een ​​type met bovengrens aan te geven, gebruiken we het trefwoord strekt zich uit na het type gevolgd door de bovengrens die we willen gebruiken. Bijvoorbeeld:

openbare lijst fromArrayToList (T [] a) {...} 

Het sleutelwoord strekt zich uit wordt hier gebruikt om aan te geven dat het type T breidt de bovengrens uit in het geval van een klasse of implementeert een bovengrens in het geval van een interface.

3.2. Meerdere grenzen

Een type kan ook als volgt meerdere bovengrenzen hebben:

Als een van de typen die worden uitgebreid met T is een klasse (d.w.z. Aantal), moet het als eerste in de lijst met grenzen worden geplaatst. Anders veroorzaakt het een compileerfout.

4. Jokertekens gebruiken bij Generics

Jokertekens worden in Java weergegeven door het vraagteken "?”En ze worden gebruikt om naar een onbekend type te verwijzen. Jokertekens zijn vooral handig bij het gebruik van generieke termen en kunnen als parametertype worden gebruikt, maar eerst is er een belangrijk opmerking om te overwegen.

Het is bekend dat Voorwerp is het supertype van alle Java-klassen, maar een verzameling van Voorwerp is niet het supertype van een verzameling.

Bijvoorbeeld een Lijst is niet het supertype van Lijst en het toewijzen van een variabele van het type Lijst naar een variabele van het type Lijst veroorzaakt een compilatiefout. Dit is om mogelijke conflicten te voorkomen die kunnen optreden als we heterogene typen aan dezelfde verzameling toevoegen.

Dezelfde regel is van toepassing op elke verzameling van een type en zijn subtypen. Beschouw dit voorbeeld eens:

openbare statische leegte paintAllBuildings (Lijst gebouwen) {buildings.forEach (Building :: paint); }

als we ons een subtype voorstellen van Gebouw, bijvoorbeeld een Huis, kunnen we deze methode niet gebruiken met een lijst met Huis, Hoewel Huis is een subtype van Gebouw. Als we deze methode moeten gebruiken met het type Building en al zijn subtypen, dan kan het begrensde jokerteken de magie doen:

openbare statische leegte paintAllBuildings (lijst gebouwen) {...} 

Nu werkt deze methode met type Gebouw en al zijn subtypen. Dit wordt een jokerteken met bovengrens genoemd waarbij type Gebouw is de bovengrens.

Jokertekens kunnen ook worden gespecificeerd met een ondergrens, waarbij het onbekende type een supertype van het gespecificeerde type moet zijn. Lagere grenzen kunnen worden gespecificeerd met de super trefwoord gevolgd door het specifieke type, bijvoorbeeld betekent onbekend type dat een superklasse is van T (= T en al zijn ouders).

5. Typ Wissen

Generics zijn toegevoegd aan Java om typeveiligheid te garanderen en om ervoor te zorgen dat generics geen overhead veroorzaken tijdens runtime, past de compiler een proces toe met de naam type wissen op generieke geneesmiddelen tijdens het compileren.

Type wissen verwijdert alle typeparameters en vervangt deze door hun grenzen of door Voorwerp als de type parameter onbegrensd is. De bytecode na compilatie bevat dus alleen normale klassen, interfaces en methoden, zodat er geen nieuwe typen worden geproduceerd. Het juiste gieten wordt ook toegepast op de Voorwerp typ tijdens het compileren.

Dit is een voorbeeld van het wissen van lettertypen:

openbare lijst genericMethod (lijst lijst) {return list.stream (). collect (Collectors.toList ()); } 

Met het wissen van het type, het onbegrensde type T wordt vervangen door Voorwerp als volgt:

// ter illustratie openbare lijst withErasure (lijstlijst) {return list.stream (). collect (Collectors.toList ()); } // wat in de praktijk resulteert in openbare List withErasure (List list) {return list.stream (). collect (Collectors.toList ()); } 

Als het type begrensd is, wordt het type tijdens het compileren vervangen door de gebonden:

public void genericMethod (T t) {...} 

zou veranderen na compilatie:

public void genericMethod (Gebouw t) {...}

6. Generieke en primaire gegevenstypen

Een beperking van generieke geneesmiddelen in Java is dat de parameter type geen primitief type kan zijn.

Het volgende compileert bijvoorbeeld niet:

Lijstlijst = nieuwe ArrayList (); lijst.add (17);

Laten we dat onthouden om te begrijpen waarom primitieve gegevenstypen niet werken generieke geneesmiddelen zijn een functie tijdens het compileren, wat betekent dat de parameter type wordt gewist en alle generieke typen worden geïmplementeerd als type Voorwerp.

Laten we als voorbeeld eens kijken naar het toevoegen methode van een lijst:

Lijstlijst = nieuwe ArrayList (); lijst.add (17);

De handtekening van het toevoegen methode is:

boolean add (E e);

En zal worden samengesteld om:

boolean add (Object e);

Daarom moeten typeparameters kunnen worden geconverteerd naar Voorwerp. Omdat primitieve typen niet uitbreiden Voorwerp, kunnen we ze niet gebruiken als typeparameters.

Java biedt echter boxed-typen voor primitieven, samen met autoboxing en unboxing om ze uit te pakken:

Geheel getal a = 17; int b = a; 

Dus als we een lijst willen maken die gehele getallen kan bevatten, kunnen we de wrapper gebruiken:

Lijstlijst = nieuwe ArrayList (); lijst.add (17); int eerste = lijst.get (0); 

De gecompileerde code is het equivalent van:

Lijstlijst = nieuwe ArrayList (); list.add (Integer.valueOf (17)); int first = ((Geheel getal) list.get (0)). intValue (); 

Toekomstige versies van Java kunnen primitieve gegevenstypen voor generieke geneesmiddelen toestaan. Project Valhalla is gericht op het verbeteren van de manier waarop met generieke geneesmiddelen wordt omgegaan. Het idee is om de specialisatie van generieke geneesmiddelen te implementeren, zoals beschreven in JEP 218.

7. Conclusie

Java Generics is een krachtige toevoeging aan de Java-taal omdat het het werk van de programmeur eenvoudiger en minder foutgevoelig maakt. Generics dwingen typecorrectheid af tijdens het compileren en, belangrijker nog, maken implementatie van generieke algoritmen mogelijk zonder extra overhead voor onze applicaties.

De broncode die bij het artikel hoort, is beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found