Grundlæggende om Java Generics

1. Introduktion

Java Generics blev introduceret i JDK 5.0 med det formål at reducere bugs og tilføje et ekstra lag af abstraktion over typer.

Denne artikel er en hurtig introduktion til Generics i Java, målet bag dem, og hvordan de kan bruges til at forbedre kvaliteten af ​​vores kode.

2. Behovet for generiske stoffer

Lad os forestille os et scenario, hvor vi vil oprette en liste i Java, der skal gemmes Heltal; vi kan blive fristet til at skrive:

Liste liste = ny LinkedList (); list.add (nyt heltal (1)); Heltal i = list.iterator (). Næste (); 

Overraskende nok vil compileren klage over den sidste linje. Det ved ikke, hvilken datatype der returneres. Compileren kræver en eksplicit rollebesætning:

Heltal i = (Heltal) list.iterator.next ();

Der er ingen kontrakt, der kan garantere, at listens returtype er en Heltal. Den definerede liste kunne indeholde ethvert objekt. Vi ved kun, at vi henter en liste ved at inspicere sammenhængen. Når man ser på typer, kan det kun garantere, at det er et Objekt, kræver således en eksplicit rollebesætning for at sikre, at typen er sikker.

Denne rollebesætning kan være irriterende, vi ved, at datatypen i denne liste er en Heltal. Medvirkende er også rodet i vores kode. Det kan forårsage type-relaterede runtime-fejl, hvis en programmør laver en fejl med den eksplicitte casting.

Det ville være meget lettere, hvis programmører kunne udtrykke deres hensigt om at bruge bestemte typer, og compileren kan sikre rigtigheden af ​​en sådan type. Dette er kerneideen bag generiske stoffer.

Lad os ændre den første linje i det forrige kodestykke til:

Liste liste = ny LinkedList ();

Ved at tilføje diamantoperatøren, der indeholder typen, begrænser vi specialiseringen af ​​denne liste kun til Heltal type, dvs. vi specificerer den type, der skal holdes inde på listen. Compileren kan håndhæve typen på kompileringstidspunktet.

I små programmer kan det virke som en triviel tilføjelse, men i større programmer kan dette tilføje betydelig robusthed og gør det lettere at læse programmet.

3. Generiske metoder

Generiske metoder er de metoder, der er skrevet med en enkelt metodedeklaration og kan kaldes med argumenter af forskellige typer. Compileren vil sikre rigtigheden af ​​den type, der anvendes. Dette er nogle egenskaber ved generiske metoder:

  • Generiske metoder har en typeparameter (diamantoperatøren, der omslutter typen), før metadeklarationens returneringstype
  • Typeparametre kan afgrænses (grænser forklares senere i artiklen)
  • Generiske metoder kan have forskellige typeparametre adskilt af kommaer i metodesignaturen
  • Metodekroppen til en generisk metode er ligesom en normal metode

Et eksempel på at definere en generisk metode til at konvertere en matrix til en liste:

offentlig liste fraArrayToList (T [] a) {return Arrays.stream (a) .collect (Collectors.toList ()); }

I det foregående eksempel blev i metodesignaturen indebærer, at metoden skal beskæftige sig med generisk type T. Dette er nødvendigt, selvom metoden returnerer ugyldig.

Som nævnt ovenfor kan metoden håndtere mere end en generisk type, hvor dette er tilfældet, skal alle generiske typer tilføjes til metodesignaturen, for eksempel hvis vi vil ændre ovenstående metode for at håndtere typen T og skriv G, det skal skrives således:

offentlig statisk liste fraArrayToList (T [] a, Function mapperFunction) {return Arrays.stream (a) .map (mapperFunction) .collect (Collectors.toList ()); }

Vi sender en funktion, der konverterer en matrix med elementerne af typen T for at liste med elementer af typen G. Et eksempel ville være at konvertere Heltal til dens Snor repræsentation:

@Test offentlig ugyldighed givenArrayOfIntegers_thanListOfStringReturnedOK () {Integer [] intArray = {1, 2, 3, 4, 5}; Liste stringList = Generics.fromArrayToList (intArray, Object :: toString); assertThat (stringList, hasItems ("1", "2", "3", "4", "5")); }

Det er værd at bemærke, at Oracle's anbefaling er at bruge et stort bogstav til at repræsentere en generisk type og vælge et mere beskrivende bogstav til at repræsentere formelle typer, for eksempel i Java-samlinger T bruges til type, K for nøgle, V for værdi.

3.1. Begrænsede generika

Som nævnt før kan typeparametre være afgrænset. Begrænset betyder “begrænset“, Vi kan begrænse typer, der kan accepteres ved en metode.

For eksempel kan vi specificere, at en metode accepterer en type og alle dens underklasser (øvre grænse) eller en type alle dens superklasser (nedre grænse).

For at erklære en øvre afgrænset type bruger vi nøgleordet strækker sig efter typen efterfulgt af den øvre grænse, som vi vil bruge. For eksempel:

offentlig liste fraArrayToList (T [] a) {...} 

Nøgleordet strækker sig bruges her til at betyde, at typen T udvider den øvre grænse i tilfælde af en klasse eller implementerer en øvre grænse i tilfælde af en grænseflade.

3.2. Flere grænser

En type kan også have flere øvre grænser som følger:

Hvis en af ​​de typer, der udvides med T er en klasse (dvs. Nummer), skal det placeres først på listen over grænser. Ellers vil det forårsage en kompileringstidsfejl.

4. Brug af jokertegn med generiske gener

Jokertegn er repræsenteret af spørgsmålstegnet i Java “?”Og de bruges til at henvise til en ukendt type. Jokertegn er især nyttige, når du bruger generiske gener, og kan bruges som en parametertype, men først er der en vigtig note at overveje.

Det er kendt, at Objekt er supertypen på alle Java-klasser, dog en samling af Objekt er ikke supertypen på nogen samling.

F.eks Liste er ikke supertypen af Liste og tildele en variabel af typen Liste til en variabel af typen Liste vil forårsage en compiler-fejl. Dette er for at forhindre mulige konflikter, der kan opstå, hvis vi føjer heterogene typer til den samme samling.

Den samme regel gælder for enhver samling af en type og dens undertyper. Overvej dette eksempel:

offentlig statisk hulrum malingAllBuildings (Liste bygninger) {bygninger.forEach (bygning :: maling); }

hvis vi forestiller os en undertype af Bygningfor eksempel en Hus, vi kan ikke bruge denne metode med en liste over Hus, selv om Hus er en undertype af Bygning. Hvis vi har brug for denne metode med typen Building og alle dens undertyper, kan det afgrænsede jokertegn gøre magien:

offentlig statisk hulrum malingAllBuildings (Liste bygninger) {...} 

Nu fungerer denne metode med typen Bygning og alle dens undertyper. Dette kaldes et øvre afgrænset wildcard hvor type Bygning er den øvre grænse.

Jokertegn kan også specificeres med en nedre grænse, hvor den ukendte type skal være en supertype af den angivne type. Nedre grænser kan specificeres ved hjælp af super nøgleord efterfulgt af den specifikke type, f.eks. betyder ukendt type, der er en superklasse af T (= T og alle dets forældre).

5. Skriv sletning

Generics blev føjet til Java for at sikre typesikkerhed og for at sikre, at generics ikke forårsager overhead ved kørsel, anvender compileren en proces kaldet sletning om generiske stoffer på kompileringstidspunktet.

Type sletning fjerner alle typeparametre og erstatter det med deres grænser eller med Objekt hvis typeparameteren er ubegrænset. Således indeholder bytekoden efter kompilering kun normale klasser, grænseflader og metoder, hvilket sikrer, at der ikke produceres nye typer. Korrekt støbning anvendes også på Objekt skriv på kompileringstidspunktet.

Dette er et eksempel på type sletning:

public List genericMethod (List list) {return list.stream (). collect (Collectors.toList ()); } 

Med type sletning, den ubegrænsede type T erstattes med Objekt som følger:

// til illustration offentlig liste medErasure (liste liste) {return list.stream (). collect (Collectors.toList ()); } // som i praksis resulterer i offentlig Liste medErasure (Liste liste) {return list.stream (). collect (Collectors.toList ()); } 

Hvis typen er afgrænset, erstattes typen med den bundne på kompileringstidspunktet:

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

ville ændre sig efter kompilering:

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

6. Generiske og primitive datatyper

En begrænsning af generiske egenskaber i Java er, at typeparameteren ikke kan være en primitiv type.

For eksempel kompileres ikke følgende:

Liste liste = ny ArrayList (); list.add (17);

Lad os huske det for at forstå, hvorfor primitive datatyper ikke virker generiske stoffer er en kompileringstid, hvilket betyder, at typeparameteren slettes, og alle generiske typer implementeres som type Objekt.

Lad os som et eksempel se på tilføje metode til en liste:

Liste liste = ny ArrayList (); list.add (17);

Underskriften af tilføje metoden er:

boolsk tilsætning (E e);

Og vil blive samlet til:

boolsk tilføjelse (Objekt e);

Derfor skal typeparametre være konvertible til Objekt. Da primitive typer ikke strækker sig Objekt, vi kan ikke bruge dem som typeparametre.

Java leverer dog boksede typer til primitiver sammen med autoboxing og unboxing for at pakke dem ud:

Heltal a = 17; int b = a; 

Så hvis vi vil oprette en liste, der kan indeholde heltal, kan vi bruge indpakningen:

Liste liste = ny ArrayList (); list.add (17); int først = liste.get (0); 

Den kompilerede kode svarer til:

Liste liste = ny ArrayList (); list.add (Integer.valueOf (17)); int først = ((Heltal) list.get (0)). intValue (); 

Fremtidige versioner af Java muligvis tillader primitive datatyper til generiske. Project Valhalla sigter mod at forbedre den måde, hvorpå generiske stoffer håndteres. Ideen er at implementere generisk specialisering som beskrevet i JEP 218.

7. Konklusion

Java Generics er en kraftig tilføjelse til Java-sproget, da det gør programmørens job lettere og mindre fejlbehæftet. Generics håndhæver typekorrekthed på kompileringstidspunktet og vigtigst af alt muliggør implementering af generiske algoritmer uden at forårsage ekstra omkostninger til vores applikationer.

Kildekoden, der ledsager artiklen, er tilgængelig på GitHub.