Å forstå typekompatibilitet er grunnleggende for å skrive gode Java-programmer, men samspillet mellom avvik mellom Java-språkelementer kan virke veldig akademisk for uinnvidde. Denne artikkelen er for programvareutviklere som er klare til å takle utfordringen! Del 1 avslører de kovariante og kontravariante forholdene mellom enklere elementer som matrixtyper og generiske typer, så vel som det spesielle Java-språkelementet, jokertegnet. Del 2 utforsker typeavhengighet og varians i vanlige API-eksempler og i lambdauttrykk.
last ned Last ned kilden Få kildekoden for denne artikkelen "Type avhengighet i Java, del 1." Laget for JavaWorld av Dr. Andreas Solymosi.Konsepter og terminologi
Før vi går inn i forholdet mellom kovarians og kontravarans mellom ulike Java-språkelementer, la oss være sikre på at vi har et felles konseptuelt rammeverk.
Kompatibilitet
I objektorientert programmering, kompatibilitet refererer til en rettet sammenheng mellom typene, som vist i figur 1.
Andreas Solymosi Vi sier at to typer er kompatibel i Java hvis det er mulig å overføre data mellom variablene av typene. Dataoverføring er mulig hvis kompilatoren godtar det, og gjøres gjennom tildeling eller parameteroverføring. Som et eksempel, kort
er kompatibel med int
fordi oppdraget intVariable = shortVariable;
er mulig. Men boolsk
er ikke kompatibel med int
fordi oppdraget intVariable = booleanVariable;
det er ikke mulig; kompilatoren godtar ikke det.
Fordi kompatibilitet er et direkte forhold, noen ganger T1
er kompatibel med T2
men T2
er ikke kompatibel med T1
, eller ikke på samme måte. Vi får se dette nærmere når vi kommer til å diskutere eksplisitt eller implisitt kompatibilitet.
Det som betyr noe er at kompatibilitet mellom referansetyper er mulig kun innenfor et typehierarki. Alle klassetyper er kompatible med Gjenstand
, for eksempel fordi alle klassene arver implisitt fra Gjenstand
. Heltall
er ikke kompatibel med Flyte
imidlertid fordi Flyte
er ikke en superklasse av Heltall
. Heltall
er kompatibel med Nummer
, fordi Nummer
er en (abstrakt) superklasse av Heltall
. Fordi de er plassert i samme type hierarki, godtar kompilatoren oppgaven numberReference = heltallReferanse;
.
Vi snakker om implisitt eller eksplisitt kompatibilitet, avhengig av om kompatibilitet må merkes eksplisitt eller ikke. For eksempel er kort implisitt kompatibel med int
(som vist ovenfor), men ikke omvendt: oppgaven shortVariable = intVariable;
det er ikke mulig. Imidlertid er kort eksplisitt kompatibel med int
, fordi oppgaven shortVariable = (kort) intVariable;
er mulig. Her må vi merke kompatibilitet med avstøpning, også kjent som typekonvertering.
Tilsvarende blant referansetyper: integerReference = numberReference;
er ikke akseptabelt, bare integerReference = (Heltall) numberReference;
ville bli akseptert. Derfor, Heltall
er implisitt kompatibel med Nummer
men Nummer
er bare eksplisitt kompatibel med Heltall
.
Avhengighet
En type kan avhenge av andre typer. For eksempel arraytypen int []
avhenger av den primitive typen int
. Tilsvarende den generiske typen ArrayList
er avhengig av typen Kunde
. Metoder kan også være typeavhengige, avhengig av parametertypene. For eksempel metoden ugyldig økning (heltall i)
; avhenger av typen Heltall
. Noen metoder (som noen generiske typer) avhenger av mer enn én type - for eksempel metoder som har mer enn én parameter.
Kovarians og kontravarians
Kovarians og kontravarans bestemmer kompatibilitet basert på typer. I begge tilfeller er varians en rettet relasjon. Kovarians kan oversettes som "forskjellige i samme retning," eller med-annerledes, mens kontravarians betyr "forskjellig i motsatt retning," eller mot-annerledes. Kovariant og kontravariant er ikke det samme, men det er en sammenheng mellom dem. Navnene antyder retningen på korrelasjonen.
Så, kovarians betyr at kompatibiliteten til to typer innebærer kompatibiliteten til typene som er avhengige av dem. Gitt typekompatibilitet, antar man at avhengige typer er kovariante, som vist i figur 2.
Andreas Solymosi Kompatibiliteten til T1
til T2
innebærer kompatibiliteten til PÅ1
) til PÅ2
). Den avhengige typen PÅ)
er kalt kovariant; eller mer presist, PÅ1
) er kovariant til PÅ2
).
For et annet eksempel: fordi oppgaven numberArray = heltallArray;
er mulig (i det minste i Java), arraytypene Heltall []
og Nummer[]
er kovariante. Så vi kan si det Heltall []
er implisitt kovariant til Nummer[]
. Og mens det motsatte ikke er sant - oppgaven integerArray = numberArray;
er ikke mulig - oppgaven med type støping (integerArray = (Heltall []) numberArray;
) er mulig; derfor sier vi, Nummer[]
er eksplisitt kovariant til Heltall []
.
Å oppsummere: Heltall
er implisitt kompatibel med Nummer
, derfor Heltall []
er implisitt kovariant til Nummer[]
, og Nummer[]
er eksplisitt kovariant til Heltall []
. Figur 3 illustrerer.
Generelt sett kan vi si at matrityper er samvariante i Java. Vi vil se på eksempler på kovarians blant generiske typer senere i artikkelen.
Kontravaranse
I likhet med kovarians er kontravarians en regissert forhold. Mens kovarians betyr med-annerledes, kontravarians betyr mot-annerledes. Som jeg tidligere nevnte, navnene uttrykker retningen på korrelasjonen. Det er også viktig å merke seg at varians ikke er et attributt av typer generelt, men bare av avhengig typer (som matriser og generiske typer, og også av metoder, som jeg vil diskutere i del 2).
En avhengig type som PÅ)
er kalt motstridende hvis kompatibiliteten til T1
til T2
innebærer kompatibiliteten til PÅ2
) til PÅ1
). Figur 4 illustrerer.
Et språkelement (type eller metode) PÅ)
avhengig av T
er kovariant hvis kompatibiliteten til T1
til T2
innebærer kompatibiliteten til PÅ1
) til PÅ2
). Hvis kompatibiliteten til T1
til T2
innebærer kompatibiliteten til PÅ2
) til PÅ1
), deretter typen PÅ)
er motstridende. Hvis kompatibiliteten til T1
mellom T2
innebærer ingen kompatibilitet mellom PÅ1
) og PÅ2
), deretter PÅ)
er invariant.
Array-typer i Java er ikke det implisitt motstridende, men det kan de være eksplisitt motstridende , akkurat som generiske typer. Jeg vil gi noen eksempler senere i artikkelen.
Typeavhengige elementer: Metoder og typer
I Java er metoder, arraytyper og generiske (parametriserte) typer de typeavhengige elementene. Metodene er avhengig av parametertypene. En array-type, T []
, er avhengig av typene av elementene, T
. En generisk type G
er avhengig av type parameter, T
. Figur 5 illustrerer.
For det meste fokuserer denne artikkelen på typekompatibilitet, selv om jeg vil berøre kompatibilitet mellom metoder mot slutten av del 2.
Implisitt og eksplisitt typekompatibilitet
Tidligere så du typen T1
å være implisitt (eller eksplisitt) kompatibel med T2
. Dette gjelder bare hvis tildelingen av en variabel av typen T1
til en variabel av typen T2
er tillatt uten (eller med) merking. Type casting er den hyppigste måten å merke eksplisitt kompatibilitet:
variableOfTypeT2 = variableOfTypeT1; // implisitt kompatibel variabelOfTypeT2 = (T2) variabelOfTypeT1; // eksplisitt kompatibel
For eksempel, int
er implisitt kompatibel med lang
og eksplisitt kompatibel med kort
:
int intVariable = 5; lang langVariabel = intVariabel; // implisitt kompatibel kort shortVariable = (kort) intVariable; // eksplisitt kompatibel
Implisitt og eksplisitt kompatibilitet eksisterer ikke bare i oppgaver, men også i overføring av parametere fra et metodeanrop til en metodedefinisjon og tilbake. Sammen med inngangsparametere betyr dette også å sende et funksjonsresultat, som du ville gjort som en utgangsparameter.
Noter det boolsk
er ikke kompatibel med noen annen type, og en primitiv og en referansetype kan heller ikke være kompatibel.
Metodeparametere
Vi sier, en metode leser inngangsparametere og skriver utgangsparametere. Parametere for primitive typer er alltid inngangsparametere. En returverdi for en funksjon er alltid en utgangsparameter. Parametere for referansetyper kan være begge deler: Hvis metoden endrer referansen (eller en primitiv parameter), forblir endringen innenfor metoden (noe som betyr at den ikke er synlig utenfor metoden etter samtalen - dette er kjent som ring etter verdi). Hvis metoden endrer det henviste objektet, forblir endringen etter at den er returnert fra metoden - dette er kjent som ring ved referanse.
En (referanse) undertype er implisitt kompatibel med supertypen, og en supertype er eksplisitt kompatibel med subtypen. Dette betyr at referansetyper bare er kompatible innenfor deres hierarkiforgrening - opp implisitt og nedover eksplisitt:
referenceOfSuperType = referenceOfSubType; // implisitt kompatibel referenceOfSubType = (SubType) referenceOfSuperType; // eksplisitt kompatibel
Java-kompilatoren tillater vanligvis implisitt kompatibilitet for en oppgave kun hvis det ikke er fare for å miste informasjon på kjøretid mellom de forskjellige typene. (Vær imidlertid oppmerksom på at denne regelen ikke er gyldig for å miste presisjon, for eksempel i en oppgave fra int
å flyte.) For eksempel int
er implisitt kompatibel med lang
fordi en lang
variabel holder hver int
verdi. I kontrast, a kort
variabel inneholder ikke noe int
verdier; dermed er bare eksplisitt kompatibilitet tillatt mellom disse elementene.
Merk at den implisitte kompatibiliteten i figur 6 forutsetter at forholdet er transitive: kort
er kompatibel med lang
.
I likhet med det du ser i figur 6, er det alltid mulig å tildele en referanse til en undertype int
en referanse til en supertype. Husk at den samme oppgaven i den andre retningen kan kaste a ClassCastException
imidlertid, slik at Java-kompilatoren bare tillater det med type casting.
Kovarians og kontravarians for matrityper
I Java er noen matrityper kovariante og / eller kontravariant. Når det gjelder samvarians, betyr dette at hvis T
er kompatibel med U
, deretter T []
er også kompatibel med U []
. I tilfelle kontravaranse betyr det at U []
er kompatibel med T []
. Matriser av primitive typer er uforanderlige i Java:
longArray = intArray; // skriv feil shortArray = (kort []) intArray; // typefeil
Arrangementer av referansetyper er implisitt kovariant og eksplisitt motstridende, derimot:
SuperType [] superArray; SubType [] subArray; ... superArray = subArray; // implisitt kovariant subArray = (SubType []) superArray; // eksplisitt motstridende
Andreas Solymosi Figur 7. Implisitt kovarians for matriser
Hva dette egentlig betyr, er at en oppgave av array-komponenter kan kaste ArrayStoreException
ved kjøretid. Hvis en matrisereferanse av Supertype
refererer til et arrayobjekt av Undertype
, og en av komponentene tildeles deretter til a Supertype
objekt, deretter:
superArray [1] = ny SuperType (); // kaster ArrayStoreException
Dette kalles noen ganger kovariansproblem. Det sanne problemet er ikke så mye unntaket (som kan unngås ved programmeringsdisiplin), men at den virtuelle maskinen må sjekke hvert oppdrag i et array-element ved kjøretid. Dette setter Java i en effektiv ulempe mot språk uten kovarians (der en kompatibel oppgave for matrisereferanser er forbudt) eller språk som Scala, der kovarians kan slås av.
Et eksempel på kovarians
I et enkelt eksempel er matrisereferansen av type Gjenstand[]
men arrayobjektet og elementene er av forskjellige klasser:
Objekt [] objectArray; // array referanse objectArray = ny streng [3]; // array-objekt; kompatibel tildelingsobjektArray [0] = nytt heltal (5); // kaster ArrayStoreException
På grunn av kovarians kan ikke kompilatoren kontrollere korrektheten til den siste tildelingen til matriseelementene - JVM gjør dette, og til betydelig kostnad. Imidlertid kan kompilatoren optimalisere utgiftene, hvis det ikke er bruk av typekompatibilitet mellom matrityper.
Andreas SolymosiHusk at i Java er det forbudt for en referansevariabel av en eller annen type som refererer til et objekt av supertypen: pilene i figur 8 må ikke rettes oppover.
Avvik og jokertegn i generiske typer
Generiske (parametriserte) typer er implisitt invariant i Java, noe som betyr at forskjellige instantiasjoner av generisk type ikke er kompatible mellom hverandre. Selv støping av typen vil ikke resultere i kompatibilitet:
Generisk superGenerisk; Generisk subGenerisk; subGeneric = (Generisk) superGeneric; // typefeil superGeneric = (Generisk) subGeneric; // typefeil
Typefeilene oppstår selv om subGeneric.getClass () == superGeneric.getClass ()
. Problemet er at metoden getClass ()
bestemmer rå typen - dette er grunnen til at en type parameter ikke hører til signaturen til en metode. Dermed de to metodedeklarasjonene
ugyldig metode (generisk p); ugyldig metode (generisk p);
må ikke forekomme sammen i en grensesnitt (eller abstrakt klasse) definisjon.