instanceof
final
brukt på metoder og klasserinterface
Alle variable som er av en eller annen klassetype er, som vi vet, det som kalles referansevariable eller pekere: De kan referere til, eller peke på, et objekt av vedkommende klassetype. For objekter som tilhører samme klassehierarki gjelder følgende viktige regel:
En referansevariabel (peker) av en bestemt klassetype kan også referere til (peke på) objekter av en hvilken som helst subklassetype til vedkommende klasse (direkte eller indirekte subklasse).
Vi tenker oss at vi har følgende klassehierarki:
Bok
Fagbok
Skolebok
Roman
NorskRoman
UtenlandskRoman
Med denne listeoppstillingen så menes det at Fagbok, Skolebok
og Roman
alle tre er subklasser til Bok
.
NorskRoman
og UtenlandskRoman
er i sin tur
subklasser til Roman
, slik at de dessuten blir indirekte
subklasser til Bok
.
For en variabel av type Bok
vil da følgende gjelde:
Bok b;
b
kan nå referere til objekter av type
Bok
Fagbok
Skolebok
Roman
NorskRoman
UtenlandskRoman
eventuelle nye subklassetyper av de nevnte typer
Det virker jo helt logisk at det er slik det også bør være, for alle
de nevnte typer er jo en eller annen type bok! For øvrig er det selvsagt slik
at alle de nevnte typene objekter må opprettes ved å bruke
new
-operatoren og en konstruktør for vedkommende klasse før vi kan
sette b
til å referere til dem. Vi kan
for eksempel skrive
Bok b = new Fagbok( ... ); // med aktuelle konstruktør-parametre
Vi kan også si det på den måten at et objekt av en eller annen subklassetype til en gitt klasse kan behandles som om det var et objekt av vedkommende klassetype. Dette er i samsvar med at det logisk sett er tilfelle at for eksempel alle fagbøker er bøker. Alle norske romaner er også bøker. Men omvendt er det ikke tilfelle at alle bøker er romaner!
Disse omstendighetene har betydning i forbindelse med metoder og metodekall. Følgende to eksempler forsøker å belyse dette.
Vi tenker oss at vi har en metode som er definert i klassen
Bok
:
public void bokmetode( Bok b ) { < bruk b til et eller annet > }
Ved kall på metoden må vi bruke en aktuell parameter som refererer til et eller annet bokobjekt:
bokmetode( a );
Men i følge regelen ovenfor har vi mange muligheter for hvilken type
bokobjekt parameteren a
virkelig refererer til: det
kan være av typen Bok
eller en hvilken som helst subklassetype til Bok
(Fagbok,
Skolebok, Roman, NorskRoman
etc.).
Vi tenker oss så at vi har en metode som er definert i klassen
Roman
:
public void romanmetode( Roman r ) { < bruk r til et eller annet > }
Kall på metoden:
romanmetode( c );
Den aktuelle parameteren c
kan være av type
Roman, NorskRoman
eller UtenlandskRoman
. Men
c
kan ikke uten videre være av type Bok
!
Dersom c
er av type Bok
, kan imidlertid
c
referere til for eksempel et Roman
-objekt,
jamfør det som er sagt foran. Og dersom det er tilfelle, kan c
konverteres til en referanse som kan brukes som aktuell parameter.
For å
teste om en referansevariabel refererer til et objekt av en bestemt type, gjør
vi bruk av nøkkelordet instanceof
som i følgende eksempel:
if ( c instanceof Roman ) // refererer c til et Roman-objekt? { Roman boka = (Roman) c; //typekonvertering, kan gjøres fordi vi nå //vet at c refererer til et Roman-objekt romanmetode( boka ); //typekonverteringen er nødvendig fordi . //romanmetode krever en parameter av type . //Roman eller en subklassetype til Roman . } . . .
I et programeksempel i kapittel 9 definerte vi følgende klassehierarki:
Ansatt /\ / \ / \ / \ Administrator FagligAnsatt
I hver av klassene definerte vi en toString
-metode som
returnerte en tekststreng med informasjon om datafeltenes verdier. Dersom vi
nå har en variabel av type Ansatt
:
Ansatt a;
så kan, etter det som er sagt, a
referere til objekter både av
type Ansatt
, av type Administrator
og av type
FagligAnsatt
(samt til eventuelt nye subklassetyper som vi måtte
finne på å definere). Det betyr også at dersom vi har en array av type
Ansatt
:
int maks = 10; //antallet er uten betydning i denne sammenheng Ansatt[] personale = new Ansatt[ maks ];
så kan vi på hver plass i arrayen sette inn et objekt av en hvilken som
helst subklassetype til Ansatt
, i tillegg til
Ansatt
-objekter. (Egentlig er det referanser vi setter inn.
Ansatt
-arrayen består egentlig av referanser som kan peke på
Ansatt
-objekter, eller på subklasseobjekter.)
Som demonstrasjon
av dette skal vi nå lage et enkelt program der vi oppretter en slik array
av type Ansatt
og fyller den med objekter av de tre klassetypene
vi har definert i Ansatt
-hierarkiet. Deretter skal vi gjennomløpe
arrayen for å skrive ut den informasjon som er lagret. Det gjør vi ved å gjøre
kall på de enkelte objektenes toString
-metode. Det fine er at det da er
unødvendig å foreta noen test på hvilken type objekt referansene
personale[0], personale[1], ...
refererer til, for å få gjort
kall på den riktige toString
-metoden. Dette finner systemet ut selv.
Dette kalles dynamisk metodebinding og betyr at det først er når
programmet blir kjørt at det avgjøres hvilken metodedefinisjon som skal
brukes i et metodekall. Det avgjøres ikke under kompileringen av
programmet. For under kompileringen er det jo umulig å vite hvilken type
objekt i et klassehierarki som en referansevariabel vil komme til å referere til.
Det at en referansevariabel kan referere til forskjellige objekttyper i et
klassehierarki og velge ut den riktige av redefinerte metoder for å utføre en
oppgave, kalles polymorfisme. Det betyr at én og samme
identifikator kan opptre i flere forskjellige former eller forkledninger.
Resultatet er at vi kan bruke samme instruksjon for å få utført forskjellige
handlinger, avhengig av hvilken type objekt som mottar instruksjonen.
(Når vi bruker et objekt obj
for å gjøre kall på en metode
definert i klassen som obj
er en instans av, så sier vi at
obj
mottar en instruksjon.)
I vårt programs utskriftsrutine har vi en for
-løkke for å
gjennomløpe Ansatt
-arrayen:
for ( int i = 0; i < antall; i++ ) ansatte += personale[ i ].toString() + "\n";
Her gjøres det kall på toString
-metoden til hvert
Ansatt
-objekt som er lagt inn i arrayen. For hvert objekt er
det forskjellige instruksjoner som utføres av toString
-metoden,
avhengig av objekttypen: om typen
er Ansatt, Administrator
eller FagligAnsatt
.
Og som vi ser av utskriften fra programmet, er det alltid den riktige
toString
-metoden som blir kalt opp. (Denne funksjonaliteten
ble allerede brukt i Programeksempel 2
i kapittel 9 uten nærmere forklaring.)
Klassene Ansatt
,
FagligAnsatt
og
Administrator
brukes akkurat slik de ble definert i nevnte program i kapittel 9.
De to siste av dem er gjengitt nedenfor.
1 public class FagligAnsatt extends Ansatt 2 { 3 private String fag; 4 private final int FAGLIGMINIMUM = 45; 5 6 public FagligAnsatt( String n, Dato f, Dato t, String fg ) 7 { 8 super( n, f, t ); 9 fag = fg; 10 setLønnstrinn( FAGLIGMINIMUM ); 11 } 12 13 public String toString() 14 { 15 return super.toString() + ", fagområde: " + fag; 16 } 17 18 public void setLønnstrinn( int trinn ) 19 { 20 if ( trinn < FAGLIGMINIMUM ) 21 trinn = FAGLIGMINIMUM; 22 super.setLønnstrinn( trinn ); 23 } 24 } 1 public class Administrator extends Ansatt 2 { 3 private String seksjon, stilling; 4 private final int ADMINMINIMUM = 41; 5 6 public Administrator( String n, Dato f, Dato t, String seksj, 7 String st ) 8 { 9 super( n, f, t ); 10 seksjon = seksj; 11 stilling = st; 12 setLønnstrinn( ADMINMINIMUM ); 13 } 14 15 public String toString() 16 { 17 return super.toString() + ", seksjon: " + seksjon + 18 ", stilling: " + stilling; 19 } 20 21 public void setLønnstrinn( int trinn ) 22 { 23 if ( trinn < ADMINMINIMUM ) 24 trinn = ADMINMINIMUM; 25 super.setLønnstrinn( trinn ); 26 } 27 }
Vi skal i det nye programmet opprette de samme ansattobjektene av forskjellig type
som vi gjorde i det opprinnelige programmet. Men
det vi skal endre på er klassen Ansatte
som ble brukt i programmet
til å inneholde objektene som ble opprettet. Den
modifiserer vi slik at de fire objektene nå legges inn i en array.
Den modifiserte klassen er kalt
Ansatte2
og er gjengitt nedenfor.
1 public class Ansatte2 2 { 3 private int maks = 10; 4 private Ansatt[] personale = new Ansatt[ maks ]; 5 private int antall = 0; //tellevariabel for arrayen 6 7 public Ansatte2() 8 { 9 personale[ antall++ ] = new Administrator( "Siri Ledestjerne", 10 new Dato( 13, 1, 1983 ), new Dato( 1, 1, 2009 ), 11 "sentraladministrasjon", "direktør" ); 12 personale[ antall++ ] = new Administrator( "Per Skrivekarl", 13 new Dato( 17, 5, 1989 ), new Dato( 1, 4, 2010 ), 14 "papirmølla", "sekretær" ); 15 personale[ antall++ ] = new FagligAnsatt( "Tone Regnevik", 16 new Dato( 20, 6, 1985 ), new Dato( 1, 8, 2008 ), 17 "matematikk" ); 18 personale[ antall++ ] = new FagligAnsatt( "Else Fortun", 19 new Dato( 29, 2, 1990 ), new Dato( 1, 8, 2006 ), "data" ); 20 } 21 22 public String toString() 23 { 24 String ansatte = "Ansatte:\n"; 25 for ( int i = 0; i < antall; i++ ) 26 ansatte += personale[ i ].toString() + "\n"; 27 28 return ansatte; 29 } 30 31 public String getLønnsoversikt() 32 { 33 String tabell = "Navn\t\tLønnstrinn\n"; 34 for ( int i = 0; i < antall; i++ ) 35 tabell += personale[ i ].getNavn() + "\t\t" + 36 personale[ i ].getLønnstrinn() + "\n"; 37 38 return tabell; 39 } 40 }
Som nevnt i kapitlet om arrayer, er det i slike tilfelle lurt å ha en
tellevariabel for hvor mange objekter som er lagt inn i arrayen.
Det er dette som er oppgaven til datafeltet antall
i klassen vår.
Dette har
vi blant annet nytte av i toString
-metoden og i metoden
getLønnsoversikt
der arrayen blir
gjennomløpt for å hente ut informasjon fra hvert enkelt objekt.
I programmets main
-metode må det nå opprettes et objekt av type
Ansatte2
istedenfor av type Ansatte
, ellers er det ingen
endringer i forhold til opprinnelig program (se
Ansattest2.java
).
I programmet merker vi oss spesielt følgende: I Ansatte2
-klassens
toString
-metode, gjengitt ovenfor, blir
Ansatt
-arrayen personale
gjennomløpt ved hjelp av
en for
-løkke. Det gjøres kall på objektenes
toString
-metode ved metodekallene
personale[ i ].toString()
Referansene personale[ i ]
refererer til forskjellige typer
objekter: de to første til Administrator
-objekter, de to siste
til FagligAnsatt
-objekter. De to klassene har ulikt innhold i sin
toString
-metode. Men av utskriften fra programmet, som er
vist nedenfor, ser vi tydelig at det for hvert objekt er den riktige
toString
-metoden som er blitt utført. Vi ser for øvrig at alle
ansatte nå er tildelt sin minimumslønn. Dette blir gjort av konstruktøren og
det er ikke lagt inn noen instruksjoner i programmet for å endre lønnstrinnet.
Når vi skal lage et program for noe, må vi først definere klasser som danner en modell for de objektene som programmet skal behandle. Ofte vil det da være slik at disse objektene har en del felles egenskaper, men dessuten noen særtrekk, slik at vi kan dele dem inn i bestemte grupper.
Som et eksempel på det som er antydet ovenfor, tenker vi oss at vi skal definere klasser som representerer utlånsobjektene i et mediatek (det vil si en virksomhet som låner eller leier ut bøker, cd-er, dvd-er, etc.). For at eksemplet ikke skal bli for stort, begrenser vi oss til følgende typer utlånsobjekter:
For alle disse typene kan vi tenke oss at det vil være ønskelig å registrere data om følgende:
Klassene som skal representere objektene må derfor ha datafelter for disse dataene. Av operasjoner som skal kunne utføres på objektene, kan vi tenke oss at det for alle sammen iallfall må være mulig å utføre følgende:
Dessuten kan det være felles egenskaper for cd og dvd, men som bøker ikke har, slik som
En naturlig og effektiv løsning ved definering av klasser som skal representere de forskjellige typene av objekter, vil da være følgende:
Vi definerer egne klasser som samler de felles "egenskapene" (det vil si datafeltene og metodene som alle objekttyper skal ha). Disse klassene har bare som oppgave å inneholde det som er felles, det er ikke meningen at de skal instansieres. Klasser som skal representere virkelige objekter, definerer vi som subklasser til de nevnte klasser, slik at de felles "egenskapene" blir arvet. For utlånsobjektene våre vil det gi følgende klassehierarki:
Utlaansobjekt /\ / \ / \ Bok AV /\ / \ / \ CD DVD
Klassen Utlaansobjekt
skal inneholde det som er felles for
alle typer utlånsobjekter, mens klassen AV
skal inneholde det
som i tillegg er felles for cd-er og dvd-er. Disse klassene representerer
ikke virkelige objekttyper, men har bare som oppgave å inneholde det som
er felles (datafelter og metoder) for sine subklasser. (Metoder som blir arvet
fra disse klassene, må imidlertid som regel redefineres i subklasser.)
Det vil ikke være aktuelt å opprette objekter av type Utlaansobjekt
og AV
. Det kan til og med være ønskelig å kunne hindre at det
gjøres! Og det får vi faktisk til ved å definere Utlaansobjekt
og AV
som abstrakte klasser. For å gjøre dette, bruker vi
nøkkelordet abstract
:
abstract class Utlaansobjekt { ... } abstract class AV extends Utlaansobjekt { ... }
Virkningen av nøkkelordet abstract
er nettopp at vi hindrer
instansiering av klassen.
abstract public void leverInn(); abstract public void lånUt( Dato d ); abstract public String ventetid();
I vårt eksempel med utlånsobjekter, kan det være at de tre nevnte metodene
skal implementeres forskjellig for de forskjellige typene av utlånsobjekter.
I klassen Utlaansobjekt
vet vi derfor ikke hvordan metodene
skal implementeres. Derfor deklarerer vi dem til å være abstrakte. Alle
abstrakte metoder må implementeres i ikke-abstrakte subklasser.
Alle klasser som inneholder minst én abstrakt metode, må deklareres som en abstrakt klasse. En klasse kan imidlertid deklareres som abstrakt selv om den ikke inneholder noen abstrakt metode. (Klassen kan da ikke instansieres.)
En av hensiktene med å gjøre bruk av abstrakte metoder og klasser kan være å sikre at alle subklasser får et sett av metoder som er felles for alle sammen. (Metodene er felles, men virkemåten kan være forskjellig.) Alle ikke-abstrakte subklasser må nemlig, som nevnt, implementere alle abstrakte metodene som de arver.
For vårt eksempel med utlånsobjekter vil dette opplegget kunne se ut som vist nedenfor. (Klassene vil til sammen ikke utgjøre noe fullstendig program.)
Merknad I eksemplet er det brukt et datafelt av type Date
.
Det eneste du trenger å vite om denne typen nå er at den representerer et
tidspunkt. En nærmere beskrivelse av hvordan klassen brukes finnes i notatet
Dato og tid: bruk av Calendar
-klassen.
Etter planen vil det bli forelest om dette i kurset Programutvikling.
0 //Utlaansobjekt.java 1 //Eksempel på abstrakte klasser og metoder 2 3 import java.util.Date; 4 5 //abstrakt klasse som inneholder datafelter og metoder 6 //som alle utlånsobjekter skal ha 7 abstract class Utlaansobjekt 8 { 9 private Date inn; //definert i java.util 10 private int lånetid; 11 private boolean ute = false; 12 13 public Utlaansobjekt( int tid ) 14 { 15 lånetid = tid; 16 } 17 18 //redefinerer som abstrakt toString-metode arvet fra klasse Object 19 abstract public String toString(); //obs: ingen implementasjon. 20 //skal returnere relevante data. 21 //Må implementeres i ikke-abstrakte subklasser. 22 23 //abstrakt metode for innlevering 24 abstract public void returner(); 25 26 //abstrakt metode for utlån fra dato d 27 abstract public void lånUt( Date d ); 28 29 //abstrakt metode for utskrift av info 30 abstract public String ventetid(); 31 32 } //slutt på klasse Utlaansobjekt 33 34 35 //Klassedefinisjon for utlånsobjekt av type bok. 36 //Må implementere alle abstrakte metoder den arver fra sin superklasse. 37 class Bok extends Utlaansobjekt 38 { 39 private String forfatter; 40 private String tittel; 41 42 public Bok( String f, String t, int p ) 43 { 44 super( p ); 45 forfatter = f; 46 tittel = t; 47 } 48 49 public String toString() 50 { 51 return forfatter + ": " + tittel; 52 } 53 54 public void returner() 55 { 56 //implementasjon av arvet abstrakt metode 57 } 58 59 public void lånUt( Date d ) 60 { 61 //implementasjon av arvet abstrakt metode 62 } 63 64 public String ventetid() 65 { 66 String output = ""; 67 //implementasjon av arvet abstrakt metode 68 return output; 69 } 70 } //slutt på klasse Bok 71 72 73 //abstrakt klasse som inneholder det som alle utlånsobjekter av 74 //type cd og dvd skal ha felles i tillegg til det som den arver 75 //fra superklassen Utlaansobjekt 76 abstract class AV extends Utlaansobjekt 77 { 78 private int spilletid; 79 80 public AV( int t, int p ) 81 { 82 super( p ); 83 spilletid = t; 84 } 85 86 public String toString() 87 { 88 return "Spilletid: " + spilletid; 89 } 90 91 public void lånUt( Date d ) 92 { 93 //implementasjon av arvet abstrakt metode 94 } 95 96 public void returner() 97 { 98 //implementasjon av arvet abstrakt metode 99 } 100 101 //Arvet abstrakt metode ventetid er ikke implementert. 102 //AV må derfor spesifiseres som en abstrakt klasse ved å 103 //bruke nøkkelordet abstract. 104 } //slutt på klasse AV 105 106 107 //Klassedefinisjon for utlånsobjekt av type cd. 108 //Må implementere alle abstrakte metoder som den arver. 109 class CD extends AV 110 { 111 private String artist; 112 private String verk; 113 114 public CD( String a, String v, int spt, int lt ) 115 { 116 super( spt, lt ); 117 artist = a; 118 verk = v; 119 } 120 121 public String toString() 122 { 123 return artist + ": " + verk + " " + super.toString(); 124 } 125 126 public String ventetid() 127 { 128 String output = ""; 129 //implementasjon av arvet abstrakt metode 130 return output; 131 } 132 } //slutt på klasse CD 133 134 135 //Klassedefinisjon for utlånsobjekt av type dvd. 136 //Må implementere alle abstrakte metoder som den arver. 137 class DVD extends AV 138 { 139 private String tittel; 140 private String produksjonsland; 141 142 public DVD( String t, String land, int spt, int lt ) 143 { 144 super( spt, lt ); 145 tittel = t; 146 produksjonsland = land; 147 } 148 149 public String toString() 150 { 151 return tittel + " " + produksjonsland + super.toString(); 152 } 153 154 public String ventetid() 155 { 156 String output = ""; 157 //implementasjon av arvet abstrakt metode 158 return output; 159 } 160 } //slutt på klasse DVD
En hvilken som helst klasse kan redefinere metoder som er arvet fra sin(e)
superklasse(r) og deklarere dem som abstrakte. Dette kan være nyttig, for
eksempel når den definisjonen som er gitt tidligere ikke kan brukes i en del
av klassehierarkiet. I vårt eksempel er toString
-metoden
som blir arvet fra Object
-klassen redefinert som en abstrakt metode
i klassen Utlaansobjekt
. Dermed må den implementeres
i alle ikke-abstrakte subklasser. Når det gjøres kall på metoden, sikres det
derfor at det ikke er den versjonen som blir arvet fra Object
som
vil bli utført.
Selv om det ikke er tillatt å instansiere abstrakte klasser, er det tillatt å deklarere (referanse)variable av abstrakt klassetype. Ved bruk må de da referere til subklasseobjekter. Og når de brukes til metodekall, er det, som alltid, typen av objekt det blir referert til som avgjør hvilken versjon av vedkommende metode som vil bli utført. På grunnlag av vårt klassehierarki kunne vi for eksempel hatt en metode som ser slik ut:
public String[] utlånsliste( Utlaansobjekt[] utlånt ) { String[] tabell = new String[ utlånt.length ]; int k = 0; for ( int i = 0; i < utlånt.length; i++ ) { if ( utlånt[ i ] != null ) tabell[ k++ ] = utlånt[ i ].toString(); } return tabell; }
Metoden utlånsliste
har parameteren
Utlaansobjekt[] utlånt
der datatypen Utlaansobjekt
er definert av en abstrakt klasse.
Husk at når vi oppretter en array der datatypen for elementene er definert av
en klasse, så vil arrayelementene automatisk bli initialisert til
null
. For å få lagt inn
objekter i arrayen, må vi opprette de enkelte objektene ved å bruke
new
-operatoren.
Og vi kan bare opprette objekter som er definert av ikke-abstrakte klasser.
Det vil i dette tilfelle si at det som arrayelementene
utlånt[ i ]
kan referere til, er objekter av type
Bok, CD
eller DVD
. Typen vil avgjøre hvilken
toString
-metode som blir utført for de forskjellige
arrayindeksene.
final
brukt på metoder og klasserVi vet at nøkkelordet final
blir brukt til å definere konstanter
i java, som i eksemplet
final int MAXANTALL = 100;
Nøkkelordet final
brukt på en variabel, som i eksemplet,
innebærer at variabelen ikke kan tildeles verdi mer enn én gang. Når
variabelen først har fått en verdi, kan den ikke seinere endres. Derfor blir
variabelen i praksis en konstant. For å markere at det er en konstant, bruker
vi bare store bokstaver i navnet på den.
Nøkkelordet final
kan vi imidlertid også bruke på klasser
og metoder. Når vi markerer en metode med final
, betyr det at
ingen subklasse kan redefinere metoden og endre dens virkemåte. Vi har med
andre ord skrevet dens final
, det vil si endelige versjon. Hele
klasser kan også markeres som final
:
final class IkkeUtvidbar
{
...
}
En final
klasse er det ikke tillatt å definere subklasser av.
Alle dens metoder blir derfor implisitt final
metoder.
Hva er så hensikten med å markere en metode som final
? Den
viktigste har med sikkerhet å gjøre: Alle som bruker metoden kan være sikre på
at dens virkemåte ikke endres, uansett hvilken type (subklasse)objekt som brukes
for å kalle den opp. Dersom en klasse er final
, kan ingen definere
en subklasse som bryter med de garantier som klassen er ment å oppfylle. Dette
kan for eksempel være aktuelt med kode som brukes ved innlesing og validering
av passord.
På den andre side innebærer bruk av final
en streng
restriksjon på bruken av en klasse og begrenser dens fleksibilitet. Det bør
derfor ikke brukes uten at vi har en viktig grunn til det. Istedenfor å markere
en hel klasse som final
, kan vi i mange tilfeller oppnå den
nødvendige sikkerhet ved å markere hver enkelt metode i klassen med
final
. Da kan vi stole på virkemåten til disse metodene, og
samtidig åpne for muligheten til å utvide klassen med mer funksjonalitet uten
å kunne redefinere de metodene som arves. Pass imidlertid på at metoder som
er final
lages slik at de bare er avhengige av datafelter som enten er
final
eller
private
. Ellers vil en subklasse kunne
endre virkemåten til metodene ved å endre på disse datafeltene.
En annen hensikt med bruk av final
, kan være optimalisering,
det vil si minimalisering av kjøretid. Når det gjøres kall på en metode som ikke er
final
, vil javas kjøresystem først bestemme klassetypen til det
objektet som har gjort kall på metoden, deretter knytte kallet til den riktige
implementasjonen av vedkommende metode, og så utføre koden som denne inneholder.
For en metode som er final
, kan det allerede under kompileringen
avgjøres hvilken kode som skal utføres, og metodekallet kan rett og slett
erstattes ved å putte inn vedkommende kode på kallstedet. Tilsvarende kan gjøres
med metoder som er private
og
static
, siden de heller
ikke kan redefineres i subklasser. For final
klasser er det også
enkelte typer sjekking som kan foretas allerede under kompilering, sjekking som
for andre klasser først kan foretas under kjøring.
I javas klassebibliotek er blant annet String
-klassen
markert som final
.
interface
Ved hjelp av et interface
kan vi beskrive hva en
klasse skal gjøre, uten å spesifisere hvordan den skal gjøre det.
Eller, uttrykt på en annen måte: Et
interface
er en samling av
krav til de klasser vi ønsker skal være i samsvar med vedkommende
interface
. En klasse kan implementere et eller flere
interface
. Det betyr at den oppfyller de krav som stilles i vedkommende
interface
. Kravene det dreier seg om, er krav til klassenes funksjonalitet.
Det vil si hvilke metoder klassene skal inneholde og hvordan disse skal virke.
En klasse som implementerer et interface
må derfor implementere de
metodene som interface
't lister opp, og implementere dem på en slik
måte at de virker slik det blir spesifistert i
interface
't.
Klassen
kan gjerne inneholde tilleggsfunksjonalitet som det ikke stilles krav om i vedkommende
interface
.
Som et konkret eksempel på interface
ser vi på
koden til interface Icon
i javas klassebibliotek. Koden ser ut som følger:
1 public interface Icon 2 { 3 /** 4 * Draw the icon at the specified location. Icon implementations 5 * may use the Component argument to get properties useful for 6 * painting, e.g. the foreground or background color. 7 */ 8 void paintIcon(Component c, Graphics g, int x, int y); 9 10 /** 11 * Returns the icon's width. 12 * 13 * @return an int specifying the fixed width of the icon. 14 */ 15 int getIconWidth(); 16 17 /** 18 * Returns the icon's height. 19 * 20 * @return an int specifying the fixed height of the icon. 21 */ 22 int getIconHeight(); 23 }
For å markere at det er et interface
, brukes nøkkelordet
interface
. Som du ser,
inneholder interface Icon
tre metoder. Men metodene er
ikke implementert. De blir avsluttet på samme måte som abstrakte metoder
(se foran). Men det er ikke brukt nøkkelordet
abstract
for dem.
For interface
er det underforstått
at alle metoder er abstrakte. Alle metodene er også automatisk
public
.
Metoder deklarert i et interface
kan ikke
være static
,
for static
-metoder er klasse-spesifikke
og aldri abstrakte, og et interface
kan bare ha abstrakte metoder.
(Klassebibliotekets klasse ImageIcon
implementerer
interface Icon
. Det er en av de to klassene i klassebiblioteket som
kan brukes til å opprette bilder.)
Et interface
kan også inneholde datafelt (eller bare det).
Men datafeltene er automatisk
static
og
final
, altså
konstanter. De må derfor også tilordnes en verdi samtidig med at de deklareres,
og som for konstanter ellers, så bør navnet skrives med bare store bokstaver.
En klasse kan implementere et interface
. Det gjør
den ved at den gir metodedefinisjoner for alle metodene som er deklarert i
interface
'et. Dessuten må vi bruke nøkkelordet
implements
og skrive
interface
-navnet:
class Klassenavn implements Interfacenavn { < definisjon av alle metoder deklarert i Interfacenavn > < eventuelle andre ting > }
Et interface
gir en form for garanti: Alle klasser som
implementerer interface
't inneholder de metoder som
interface
'et lister opp.
Vi ser at et interface
har stor likhet med en abstrakt klasse.
Men det er en viktig forskjell i måten de kan brukes på: En klasse kan bare
være direkte subklasse til én enkelt superklasse, men den kan
implementere så mange interface
som vi ønsker, og om vi ønsker det,
samtidig være subklasse til en annen klasse. Vi kan altså skrive
class Klassenavn extends Superklasse implements Interface1, Interface2 //så mange interface vi ønsker { ... }
Ved å bruke dette, er det mulig i en viss grad å knytte sammen flere forskjellige klassehierarkier.
Det er også tillatt å deklarere variable der datatypen er definert som et
interface
. Som verdi må da variablene tildeles en referanse
til et objekt av en klassetype som implementerer vedkommende
interface
.
Ved metodekall vil det som alltid være typen objekt
det refereres til som avgjør hvilken metodeversjon som vil bli utført.
Det er også mulig å bruke operatoren instanceof
for å sjekke om en variabel er tilordnet et objekt som er av en klassetype som implementerer
et gitt interface
:
if ( variabel instanceof Interfacenavn ) ...
På tilsvarende måte som vi kan bygge hierarkier av klasser ved å bruke nøkkelordet
extends
for å definere subklasser,
kan vi også bruke extends
til å
bygge hierarkier av interface
'er.
I mange tilfelle får vi
behov for å implementere interface
som er definert i klassebiblioteket.
Dette må vi for eksempel gjøre så snart vi vil lage vindusprogrammer som er såkalt
hendelsesbaserte, det vil si som foretar innlesing av verdier fra brukeren,
eller som skal reagere på at vi bruker musa eller tastaturet.
Hendelsesbaserte vindusprogrammer har vi allerede laget noen av, se programeksemplene
tidskonvertering og
Bruk av knapper i kapitlet
Vindusbaserte programmer, samt
Programeksempel 2 i kapittel 9.
I disse programmene har vindusklassen vår implementert
interface ActionListener
.
I dette er det bare én metode
som er listet opp
public void actionPerformed( ActionEvent e );
Denne har vi implementert. Dessuten har vi bak klassenavnet skrevet
implements ActionListener
.
Vi skal seinere lære om en del flere grafiske skjermkomponenter enn dem
vi har brukt hittil. En del av disse genererer andre typer hendelser enn typen
ActionEvent
som blir håndtert av actionPerformed
-metoden.
Det er da andre interface
enn ActionListener
som må
implementeres for at komponentene skal virke på den måten vi ønsker.
Det er ikke uvanlig at det er litt forskjellig kode som skal utføres avhengig av hvilken type et objekt tilhører. En måte å programmere dette på er at vi eksplisitt skriver kode for typetesting på formen
if (x <er av type 1>) handling1(x); else if (x <er av type 2>) handling2(x);
Typetesting får vi som kjent utført ved bruke av operatoren
instanceof
.
I slike situasjoner er det grunn til å tenke på bruk polymorfisme:
Er det slik at handling1
og handling2
representerer
noe felles som ligger i bunnen, men som skal utføres på litt forskjellig måte
avhengig av hvilken type objekt vi har, og som egentlig er spesialtyper av en felles hovedtype?
Dersom det er tilfelle, så definer metoden som en fellesmetode
handling
i en felles superklasse eller i et
interface
for de to klassene som definerer de to metodene, og erstatt koden ovenfor med
det enkle metodekallet
x.handling();
Den innebygde dynamiske metodebindingen vil da sørge for å kalle opp riktig
versjon av metoden (redefinert i subklassene), avhengig av hvilken
subklassetype x
tilhører.
Kode som bruker polymorfisme eller
interface
-implementasjoner
er også mye lettere å vedlikeholde og utvide enn kode som bruker multippel typetesting.
Fra kapitlet Vindusbaserte programmer vet vi at dersom noe skal skje som følge av at vi for eksempel klikker på en knapp med musa, så må det for knappen være registrert et lytteobjekt. Vi har hittil brukt selve vindusobjektet til lytteobjekt. Det er imidlertid mer ryddig å definere lytteobjektet i form av en egen klasse. Siden vi i klassen som definerer lytteobjektet har behov for å referere til skjermkomponenter i vindusklassen (for å sjekke hvor hendelsen skjedde), er det praktisk å definere lytteobjektet i form av det som kalles en indre klasse. Vi skal derfor se nærmere på hva dette er.
En indre klasse er en klasse som er definert inni en annen klasse. Indre klasser brukes først og fremst for å definere lytteobjekter, men de kan også være nyttige i andre sammenhenger.
En indre klasse kan tilordnes aksessnivå
(public
,
protected
,
pakkeaksess, eller private
), slik vi kan gjøre med datafelter og
metoder. Merk deg at det bare er indre klasser som kan være
private
eller protected
. Vanlige klasser er alltid enten
public
eller har pakkeaksess.
OBS! En indre klasse vil alltid ha full tilgang til den omgivende klasses datafelter og metoder, uavhengig av hvilken aksessibilitet disse er tilordnet. (Aksessibilitet bestemmer tilgang utenfra.)
Vi skal omarbeide programmet Klokketest
fra kapittel 8 slik at lytteobjektet defineres i form av en indre klasse.
Klassene Tid2
og Klokke
kan vi bruke
som de ble definert i kapittel 8, ingen
endringer er nødvendig. Vindusklassen Klokkevindu
skal nå fritas
fra rollen som lytteobjekt. Lytteobjekt for knappen og tekstfeltene skal i dette programmet
defineres i form av en indre klasse. Derfor tilføyer vi ikke
implements ActionListener
bak extends JFrame
.
Isteden står dette bak navnet til den
indre klassen:
private class Klokkelytter implements ActionListener
I konstruktøren til den ytre klassen blir det opprettet et objekt av
type Klokkelytter
:
Klokkelytter lytter = new Klokkelytter();
Dette objektet blir registrert som lytteobjekt for tekstfeltene og knappen som det skal lyttes på for hendelser:
sek.addActionListener( lytter ); min.addActionListener( lytter ); ...
Legg ellers merke til at vi i den indre klassen kan referere direkte
til tekstfeltene og knappen i den ytre klassen, selv om disse har
private
aksess. Siden vi har foretatt en del endringer i den
opprinnelige vindusklassen, bør den også tildeles et nytt navn. Vi kaller den
Klokkevindu2
. Fullstendig kode for klassen er
vist nedenfor og finnes i fila
Klokkevindu2.java.
1 import javax.swing.*; 2 import java.awt.*; 3 import java.awt.event.*; 4 5 public class Klokkevindu2 extends JFrame 6 { 7 private Klokke ur; 8 private JTextField sek, min, time, universaltid, standardtid; 9 private JButton klokketikk; 10 11 public Klokkevindu2() 12 { 13 super( "Javaklokke" ); 14 Klokkelytter lytter = new Klokkelytter(); 15 ur = new Klokke(); 16 sek = new JTextField( 2 ); 17 min = new JTextField( 2 ); 18 time = new JTextField( 2 ); 19 universaltid = new JTextField( 6 ); 20 universaltid.setEditable( false ); 21 standardtid = new JTextField( 6 ); 22 standardtid.setEditable( false ); 23 klokketikk = new JButton( "1 sekund fram" ); 24 sek.addActionListener( lytter ); 25 min.addActionListener( lytter ); 26 time.addActionListener( lytter ); 27 klokketikk.addActionListener( lytter ); 28 Container c = getContentPane(); 29 c.setLayout( new FlowLayout() ); 30 c.add( new JLabel( "Sett time:" ) ); 31 c.add( time ); 32 c.add( new JLabel( "Sett minutt:" ) ); 33 c.add( min ); 34 c.add( new JLabel( "Sett sekund:" ) ); 35 c.add( sek ); 36 c.add( new JLabel( "Universaltid " ) ); 37 c.add( universaltid ); 38 c.add( new JLabel( "Standardtid" ) ); 39 c.add( standardtid ); 40 c.add( klokketikk ); 41 visTid(); 42 } 43 44 public void visTid() 45 { 46 universaltid.setText( ur.visUniversaltid() ); 47 standardtid.setText( ur.visStandardtid() ); 48 } 49 50 private class Klokkelytter implements ActionListener 51 { 52 public void actionPerformed(ActionEvent e) 53 { 54 if (e.getSource() == time) 55 { 56 int t = Integer.parseInt(time.getText()); 57 ur.setTime(t); 58 time.setText(""); 59 visTid(); 60 } 61 else if (e.getSource() == min) 62 { 63 int m = Integer.parseInt(min.getText()); 64 ur.setMinutt(m); 65 min.setText(""); 66 visTid(); 67 } 68 else if (e.getSource() == sek) 69 { 70 int s = Integer.parseInt(sek.getText()); 71 ur.setSekund(s); 72 sek.setText(""); 73 visTid(); 74 } 75 else if (e.getSource() == klokketikk) 76 { 77 ur.tikk(); 78 visTid(); 79 } 80 } 81 } 82 }
I programmets main
-metode som finnes i den nye driverklassen
Klokketest2
er det nå et vindusobjekt av type
Klokkevindu2
som må opprettes. For øvrig er det ingen endringer.
Vinduet som vises på skjermen når vi kjører programmet, vil se ut akkurat
som før og det vil virke på samme måte.
static
indre klasserIndre klasser kan deklareres til å være
static
. Eneste forskjellen
fra vanlige indre klasser er at det fra en
static
indre klasse ikke er
mulig å referere til datafelt i den omgivende klasse. Så dersom vi ikke har
behov for det, så kan vi gjerne deklarere den indre klassen til å være
static
. Dersom
objekter av en indre klasse skal opprettes av en
static
metode, så er vi nødt
til å deklarere den indre klassen til å være
static
.
Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014