Forrige kapittel

Kapittel 10 — Objektorientert programmering: polymorfisme

Forholdet mellom superklasse-objekter og subklasse-objekter

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).

Eksempel

Vi tenker oss at vi har følgende klassehierarki:

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

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.

Eksempel 1

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.).

Eksempel 2

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
      .
    }
    .
    .
    .

Dynamisk metodebinding — polymorfisme

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.)

Programeksempel

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.

Abstrakte klasser og metoder

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.

Eksempel

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.

Metoder kan også deklareres til å være abstrakte. Vi gir da ingen implementasjon av metodene, men avslutter signaturen med semikolon.

Eksempel

  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 implementeres i ikke-abstrakte subklasser.

Alle klasser som inneholder minst én abstrakt metode, 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 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 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.

Nøkkelordet final brukt på metoder og klasser

Vi 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.

Bruk av 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.

Bruk av polymorfisme kontra bruk av typeinformasjon

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.

Hendelses-håndtering (Event-håndtering)

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.

Indre klasser

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.)

Eksempel

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 klasser

Indre 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.

Forrige kapittel

Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014