Grunnleggende om Java Generics

1. Introduksjon

Java Generics ble introdusert i JDK 5.0 med sikte på å redusere feil og legge til et ekstra lag med abstraksjon over typer.

Denne artikkelen er en rask intro til Generics i Java, målet bak dem og hvordan de kan brukes til å forbedre kvaliteten på koden vår.

2. Behovet for generiske stoffer

La oss forestille oss et scenario der vi vil lage en liste i Java for å lagre Heltall; vi kan bli fristet til å skrive:

Listeliste = ny LinkedList (); list.add (nytt heltal (1)); Heltall i = list.iterator (). Neste (); 

Overraskende nok vil kompilatoren klage på den siste linjen. Den vet ikke hvilken datatype som returneres. Kompilatoren vil kreve en eksplisitt rollebesetting:

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

Det er ingen kontrakt som kan garantere at returtypen på listen er en Heltall. Den definerte listen kan inneholde ethvert objekt. Vi vet bare at vi henter en liste ved å inspisere konteksten. Når man ser på typer, kan det bare garantere at det er et Gjenstand, krever dermed en eksplisitt rollebesetning for å sikre at typen er trygg.

Denne rollebesetningen kan være irriterende, vi vet at datatypen i denne listen er en Heltall. Medvirkende roter også koden vår. Det kan forårsake typebaserte kjøretidsfeil hvis en programmerer gjør en feil med den eksplisitte castingen.

Det ville vært mye lettere hvis programmerere kunne uttrykke sin intensjon om å bruke bestemte typer, og kompilatoren kan sikre korrektheten av en slik type. Dette er kjernetanken bak generiske legemidler.

La oss endre den første linjen i forrige kodebit til:

Listeliste = ny LinkedList ();

Ved å legge til diamantoperatøren som inneholder typen, begrenser vi spesialiseringen av denne listen bare til Heltall type dvs. vi spesifiserer typen som skal holdes inne i listen. Kompilatoren kan håndheve typen på kompileringstidspunktet.

I små programmer kan dette virke som et trivielt tillegg, men i større programmer kan dette gi betydelig robusthet og gjør det lettere å lese.

3. Generiske metoder

Generiske metoder er de metodene som er skrevet med en enkelt metodedeklarasjon og kan kalles med argumenter av forskjellige typer. Kompilatoren vil sikre korrektheten av hvilken type som brukes. Dette er noen egenskaper ved generiske metoder:

  • Generiske metoder har en typeparameter (diamantoperatøren som omslutter typen) før returtypen til metodedeklarasjonen
  • Typeparametere kan begrenses (grensene blir forklart senere i artikkelen)
  • Generiske metoder kan ha forskjellige typeparametere atskilt med komma i metodesignaturen
  • Metodekroppen for en generisk metode er akkurat som en vanlig metode

Et eksempel på å definere en generisk metode for å konvertere en matrise til en liste:

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

I forrige eksempel, i metodesignaturen innebærer at metoden vil dreie seg om generisk type T. Dette er nødvendig selv om metoden blir ugyldig.

Som nevnt ovenfor kan metoden håndtere mer enn en generisk type, hvor dette er tilfelle, må alle generiske typer legges til metodesignaturen, for eksempel hvis vi vil endre metoden ovenfor for å håndtere typen T og skriv G, det skal skrives slik:

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

Vi sender en funksjon som konverterer en matrise med elementene av typen T for å liste med elementer av typen G. Et eksempel kan være å konvertere Heltall til dens String representasjon:

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

Det er verdt å merke seg at Oracle-anbefaling er å bruke en stor bokstav for å representere en generisk type og velge en mer beskrivende bokstav for å representere formelle typer, for eksempel i Java-samlinger T brukes til type, K for nøkkel, V for verdi.

3.1. Avgrensede generika

Som nevnt tidligere kan typeparametere begrenses. Avgrenset betyr “begrenset“, Vi kan begrense typer som kan aksepteres med en metode.

For eksempel kan vi spesifisere at en metode godtar en type og alle dens underklasser (øvre grense) eller en type alle dens superklasser (nedre grense).

For å erklære en øvre grense bruker vi nøkkelordet strekker etter typen etterfulgt av den øvre grensen som vi vil bruke. For eksempel:

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

Nøkkelordet strekker brukes her for å bety at typen T utvider den øvre grensen i tilfelle en klasse eller implementerer en øvre grense i tilfelle et grensesnitt.

3.2. Flere grenser

En type kan også ha flere øvre grenser som følger:

Hvis en av typene som utvides med T er en klasse (dvs. Nummer), må den settes først i listen over grensene. Ellers vil det føre til kompileringsfeil.

4. Bruke jokertegn med generiske

Jokertegn er representert med spørsmålstegnet i Java “?”Og de brukes til å referere til en ukjent type. Jokertegn er spesielt nyttige når du bruker generiske legemidler og kan brukes som parametertype, men først er det en viktig merknad å vurdere.

Det er kjent at Gjenstand er supertypen til alle Java-klasser, men en samling av Gjenstand er ikke supertypen til noen samling.

For eksempel, a Liste er ikke supertypen av Liste og tilordne en variabel av typen Liste til en variabel av typen Liste vil forårsake en kompilatorfeil. Dette er for å forhindre mulige konflikter som kan skje hvis vi legger til heterogene typer i samme samling.

Den samme regelen gjelder enhver samling av en type og dens undertyper. Tenk på dette eksemplet:

offentlig statisk tomrom paintAllBuildings (Liste bygninger) {bygninger. for hver (Bygning :: maling); }

hvis vi forestiller oss en undertype av Bygning, for eksempel en Hus, kan vi ikke bruke denne metoden med en liste over Hus, selv om Hus er en undertype av Bygning. Hvis vi trenger å bruke denne metoden med typen Building og alle dens undertyper, kan det avgrensede jokertegnet gjøre magien:

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

Nå vil denne metoden fungere med type Bygning og alle dens undertyper. Dette kalles et øvre avgrenset jokertegn der typen Bygning er den øvre grensen.

Jokertegn kan også spesifiseres med en nedre grense, der den ukjente typen må være en supertype av den spesifiserte typen. Nedre grenser kan spesifiseres ved hjelp av super nøkkelord etterfulgt av den spesifikke typen, for eksempel betyr ukjent type som er en superklasse av T (= T og alle foreldrene).

5. Skriv sletting

Generics ble lagt til Java for å sikre typesikkerhet og for å sikre at generics ikke forårsaker overhead ved kjøretid, bruker kompilatoren en prosess kalt sletting på generiske legemidler ved kompileringstid.

Type sletting fjerner alle typeparametere og erstatter den med sine rammer eller med Gjenstand hvis typeparameteren er ubegrenset. Dermed inneholder bytekoden etter kompilering bare normale klasser, grensesnitt og metoder, slik at ingen nye typer produseres. Riktig støping brukes også på Gjenstand skriv på kompileringstidspunktet.

Dette er et eksempel på type sletting:

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

Med type sletting, den ubegrensede typen T erstattes med Gjenstand som følger:

// for illustrasjon offentlig List withErasure (List list) {return list.stream (). collect (Collectors.toList ()); } // som i praksis resulterer i offentlig Liste medErasur (Listeliste) {return list.stream (). collect (Collectors.toList ()); } 

Hvis typen er avgrenset, blir typen erstattet av bundet ved kompileringstidspunktet:

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

ville endret seg etter kompilering:

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

6. Generiske og primitive datatyper

En begrensning av generikk i Java er at typeparameteren ikke kan være en primitiv type.

Følgende kompileres for eksempel ikke:

Listeliste = ny ArrayList (); list.add (17);

La oss huske det for å forstå hvorfor primitive datatyper ikke fungerer generiske er en kompileringstid-funksjon, noe som betyr at typeparameteren er slettet og alle generiske typer er implementert som type Gjenstand.

La oss som et eksempel se på legge til metode for en liste:

Listeliste = ny ArrayList (); list.add (17);

Signaturen til legge til metoden er:

boolsk tilsetning (E e);

Og vil bli samlet til:

boolsk tilsetning (Objekt e);

Derfor må typeparametere være konverterbare til Gjenstand. Siden primitive typer ikke strekker seg Gjenstand, vi kan ikke bruke dem som typeparametere.

Imidlertid gir Java boksetyper for primitive, sammen med autoboksing og unboxing for å pakke dem ut:

Heltall a = 17; int b = a; 

Så hvis vi vil lage en liste som kan inneholde heltall, kan vi bruke innpakningen:

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

Den sammensatte koden tilsvarer:

Listeliste = ny ArrayList (); list.add (Integer.valueOf (17)); int først = ((Heltall) list.get (0)). intValue (); 

Fremtidige versjoner av Java kan tillate primitive datatyper for generiske. Prosjekt Valhalla har som mål å forbedre måten generiske stoffer håndteres på. Ideen er å implementere generisk spesialisering som beskrevet i JEP 218.

7. Konklusjon

Java Generics er et kraftig tillegg til Java-språket, da det gjør programmererjobben enklere og mindre feilutsatt. Generics håndhever typekorrekthet ved kompileringstid, og viktigst av alt, muliggjør implementering av generiske algoritmer uten å forårsake ekstra omkostninger til applikasjonene våre.

Kildekoden som følger med artikkelen er tilgjengelig på GitHub.