Forrige kapittel Neste kapittel

Kapittel 9 — Objektorientert programmering: arv

Innledning

En klassedefinisjon er definisjon av en ny datatype. Klassen beskriver hvilke egenskaper objekter av den nye typen har:

Objektenes data er bestemt av klassens datafelter, mens objektenes operasjoner er bestemt av klassens metoder.

I naturen og samfunnet er det svært vanlig at vi kan klassifisere ting i form av hierarkier.

Eksempler på hierarkier

Alle medlemmer av en undergruppe har alle egenskapene til gruppa den er avledet fra. I tillegg har medlemmene av undergruppa noen spesielle egenskaper som gjør at dens medlemmer til sammen vil utgjøre en delmengde av den gruppa de er avledet fra. For eksempel er alle pattedyr dyr, men det er ikke alle dyr som er pattedyr. Alle romaner er bøker, men det er ikke alle bøker som er romaner, og så videre.

Et programmeringsspråk brukes til å modellere virkeligheten. Da er det nyttig at språket inneholder mekanismer som kan brukes til å gjenspeile slike hierarkier som virkeligheten består av. Javas mekanisme for dette er muligheten til å definere subklasser, eller avledete klasser som de også kalles. Dersom vi for eksempel har definert en klasse Bok som datatype for bøker generelt, vil vi da for fagbøker definere en subklasse Fagbok som datatype for fagbøker.

For å definere en subklasse av en gitt klasse, bruker vi nøkkelordet extends. Vi kjenner dette nøkkelordet allerede fra vindusklasser, der vi har definert en subklasse til klassen JFrame som vi importerte fra klassebiblioteket. Nå skal vi se nærmere på hva dette egentlig innebærer, og vi skal definere subklasser til klasser som vi selv har definert.

Eksempel 1: subklasse og superklasse

Anta at klassen Ansatt allerede er definert og skal representere en ansatt i en eller annen virksomhet, slik at det i denne klassen er lagt inn datafelter for navn, adresse og andre data vi ønsker å registrere for hver enkelt ansatt, slik vi gjorde i kapittel 8. Vi skal definere en klasse FagligAnsatt som representerer en faglig ansatt. De faglig ansatte er, til forskjell fra ansatte ellers, knyttet til et eller annet fag. Dette faget ønsker vi skal registreres for de faglig ansatte, i tillegg til de data som skal registreres for alle ansatte. Istedenfor å definere klassen FagligAnsatt som en selvstendig klasse der vi skriver inn alt vi har skrevet i klassen Ansatt og i tillegg tilføyer et datafelt for fag, kan vi og bør vi da definere FagligAnsatt som en subklasse til Ansatt ved at vi skriver

class FagligAnsatt extends Ansatt
{
  private String fag;
  // alle datafeltene i Ansatt blir arvet og 
  //skal derfor ikke skrives om igjen.
  .
  .
  .
}

Ansatt er nå superklassen til FagligAnsatt. Vi kan definere så mange subklasser vi vil til én og samme klasse, som dermed blir superklasse til alle disse.

Virkemåte

Virkningen av å definere en subklasse er at alt som er definert i superklassen også vil være definert for subklassen, uten at definisjonen for superklassen blir gjentatt. (Det er et unntak for konstruktører, se nedenfor.) Vi sier at det som er definert i superklassen blir arvet til subklassen. I tillegg kan vi i subklassen legge inn tilleggsdefinisjoner (datafelter og metoder) for ting som er spesielle for objekter som faller inn under subklassen, slik vi har eksempel på ovenfor: En faglig ansatt er knyttet til et fag, noe som ikke er tilfelle for alle ansatte. For øvrig vil det i en subklasse ofte være behov for å redefinere metoder som blir arvet fra superklassen, slik at vi får tilpasset dem til subklasseobjektene. På engelsk brukes ordet 'override' for slik redefinering av metoder.

Når skal vi definere subklasser?

Når vi skriver programmer, og definerer klasser, vil følgende spørsmål ofte melde seg: Skal klassen være en subklasse til en annen klasse, eller ikke? Rettesnoren bør da alltid være dette: Vil subklasseobjektene være en naturlig undergruppe av superklasseobjektene? Dersom svaret er ja, definerer vi en subklasse, ellers ikke. (Legg merke til at i eksemplet ovenfor stemmer dette: de faglig ansatte er en naturlig undergruppe av samlingen av alle ansatte.)

En annen rettesnor kan være dette: Definer ikke en subklasse uten at alle de metodene som dermed blir arvet er meningsfulle for subklasseobjektene.

Subklasser og konstruktører — referansen super

En konstruktørs oppgave er å reservere plass i memory for et nytt objekt, samt å foreta diverse initialisering. En konstruktør blir ikke arvet på samme måte som det andre klasseinnholdet i superklassen. Det er imidlertid absolutt nødvendig at en konstruktør utfører sin initialiseringsjobb. Systemet krever at alle datafelter i superklassen initialiseres før det foretas initialisering av subklassens datafelter. Derfor må kall på en av superklassens konstruktører være første instruksjon i alle konstruktører for subklasser. En superklassekonstruktør får vi kalt opp ved bruk av nøkkelordet super, sammen med eventuelle parametre:

  super();  // kall på superklassens defaultkonstruktør
  super( < aktuelle parametre > );  // kall på en annen av
                     // superklassens konstruktører

Merk deg at dersom kall på en av superklassens konstruktører ikke står som første instruksjon i en konstruktør for subklassen, så vil systemet automatisk foreta kall på superklassens default-konstruktør. Dersom en slik ikke finnes (fordi det bare er definert konstruktør(er) med parametre), vil programmet ikke la seg kompilere.

Initialisering av datafelter kan skje på tre måter:

Eksempel 2

class Initialiseringseksempel
{
  private int nr;
  private int antall = 0;
  private String navn;

  public Initialiseringseksempel( String n )
  {
    navn = n;
  }
  .
  .
  .
}

class Test
{
  private Initialiseringseksempel e =
                    new Initialiseringseksempel( "eksempel" );
                    // e.nr får default-verdi 0
                    // e.antall får verdi 0
                    // e.navn får verdi "eksempel"
  .
  .
  .
}

Som nevnt foran, er det i en subklasse ofte behov for å modifisere metoder som blir arvet fra superklassen, slik at de blir tilpasset subklasseobjektene. Metodene må da redefineres. Men ofte er det da nok å tilføye noe kode til den som allerede er skrevet i superklassens definisjon av metoden, uten at den eksisterende koden endres. For å slippe å skrive alt om igjen, kan vi da ved hjelp av nøkkelordet super gjøre kall på superklassens versjon av den metode som redefineres.

Eksempel 3

Vi ønsker å redefinere metoden toString som blir arvet fra superklassen. Subklassens versjon av metoden skal tilføye noe ekstra tekst i tillegg til den tekst som blir returnert av superklassens versjon av metoden. Da kan vi skrive slik (uttrykt i pseudokode):

  public String toString()
  {
    String s = super.toString();  //kall på superklassens toString-metode
    s += < ekstra tekst >;  //tilføyer den ekstra teksten
    return s;
  }

Programeksempel 1

Vi tar utgangspunkt i klassen Ansatt som ble definert og brukt i Programeksempel 4 i kapittel 8. Klassen er gjengitt nedenfor. Klassen gjør bruk av klassen Dato som du kan finne i nevnte eksempel i kapittel 8.

Det skal til klassen Ansatt defineres en subklasse FagligAnsatt, med et datafelt for fag. Dessuten skal toString-metoden tilpasses, slik at den returnerer faget i tillegg til de andre dataene. Faglig ansatte skal sikres en lønnsplassering på minimum lønnstrinn 45.

Det skal også til klassen Ansatt defineres en subklasse Administrator. For administratorer skal det, i tillegg til det som er felles for alle ansatte, registreres seksjon og stilling, slik at det i subklassen Administrator må være datafelter for dette. Også for denne subklassen skal toString-metoden tilpasses, slik at den returnerer seksjon og stilling i tillegg til de andre dataene. Administrativt ansatte skal sikres en lønnsplassering på minimum lønnstrinn 41.

For begge subklasser skal alle datafelter initialiseres ved bruk av konstruktører og nødvendige konstruktørparametre. Det skal skrives et lite program som tester ut klassene ved å opprette to objekter av hver klasse og skrive ut deres data.

Alle klasser som brukes av programmet, unntatt klassen Dato, er gjengitt nedenfor.

 1 public class Ansatt //ingen endringer i forhold til tidligere
 2 {
 3   private String navn;
 4   private Dato født, tiltrådt;
 5   private int lønnstrinn;
 6   private final int MIN = 30, MAX = 80;
 7
 8   public Ansatt( String n, Dato f, Dato t )
 9   {
10     navn = n;
11     født = f;
12     tiltrådt = t;
13     lønnstrinn = MIN;
14   }
15
16   public String getNavn()
17   {
18     return navn;
19   }
20
21   public int getLønnstrinn()
22   {
23     return lønnstrinn;
24   }
25
26   public void setLønnstrinn( int trinn )
27   {
28     if ( trinn > lønnstrinn && trinn <= MAX )
29       lønnstrinn = trinn;
30     else if ( trinn > MAX )
31       lønnstrinn = MAX;
32   }
33
34   public String toString()
35   {
36     return navn + ", født: " +
37           født.toString() + ", tiltrådt: " +
38           tiltrådt.toString();
39   }
40 }


 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 }

I de to subklassene FagligAnsatt og Administrator merker vi oss følgende:

I klassen Ansatte som er gjengitt nedenfor, blir det opprettet to objekter av hver av de definerte subklassetypene. For å teste ut metoden setLønnstrinn, forsøker vi for noen av objektene å sette et lønnstrinn som er utenfor de tillatte grensene.

 1 //Testklasse som oppretter to objekter av hver av de 
 2 //definerte subklassetypene.
 3 public class Ansatte
 4 {
 5   private Administrator direktør, sekretær;
 6   private FagligAnsatt matematikklærer, programmeringslærer;
 7
 8   public Ansatte()
 9   {
10     direktør = new Administrator( "Siri Ledestjerne",
11             new Dato( 13, 1, 1983 ), new Dato( 1, 1, 2009 ),
12             "sentraladministrasjon", "direktør" );
13     direktør.setLønnstrinn( 85 );
14     sekretær = new Administrator( "Per Skrivekarl",
15             new Dato( 17, 5, 1989 ), new Dato( 1, 4, 2010 ),
16             "papirmølla", "sekretær" );
17     sekretær.setLønnstrinn( 40 );
18     matematikklærer = new FagligAnsatt( "Tone Regnevik",
19             new Dato( 20, 6, 1985 ), new Dato( 1, 8, 2008 ),
20             "matematikk" );
21     programmeringslærer = new FagligAnsatt( "Else Fortun",
22             new Dato( 29, 2, 1990 ), new Dato( 1, 8, 2006 ), "data" );
23     programmeringslærer.setLønnstrinn( 57 );
24   }
25
26   public String toString()
27   {
28     String ansatte = "ANSATTE\n";
29     ansatte += direktør.toString() + "\n";
30     ansatte += sekretær.toString() + "\n";
31     ansatte += matematikklærer.toString() + "\n";
32     ansatte += programmeringslærer.toString();
33     return ansatte;
34   }
35
36   public String getLønnsoversikt()
37   {
38     String tabell = "Navn\t\tLønnstrinn\n";
39     tabell += direktør.getNavn() + "\t\t" +
40             direktør.getLønnstrinn() + "\n";
41     tabell += sekretær.getNavn() + "\t\t" +
42             sekretær.getLønnstrinn() + "\n";
43     tabell += matematikklærer.getNavn() + "\t\t" +
44             matematikklærer.getLønnstrinn() + "\n";
45     tabell += programmeringslærer.getNavn() + "\t\t" +
46             programmeringslærer.getLønnstrinn();
47     return tabell;
48   }
49 }



 1 import javax.swing.JTextArea;
 2 import javax.swing.JOptionPane;
 3
 4 public class Ansattest
 5 {
 6   public static void main( String[] args )
 7   {
 8     Ansatte s = new Ansatte();
 9     String utskrift = s.toString();
10     utskrift += "\n\nLØNNSOVERSIKT\n" + s.getLønnsoversikt();
11     JTextArea tavle = new JTextArea( utskrift );
12     JOptionPane.showMessageDialog( null, tavle, "Ansatte",
13                   JOptionPane.PLAIN_MESSAGE );
14   }
15 }

Når vi kjører programmet, vil vi få fram følgende vindu:

Vi merker oss her følgende:

Merknader om tilgjengelighet og beskyttelse av data

De fire nivåene for aksessibilitet av datafelter, metoder, etc. ble beskrevet på slutten av kapittel 8. Som vi har vært inne på tidligere, er det for datafelter vanlig å begrense den direkte tilgangen til dem til den klassen som datafeltene ligger i, ved å gi dem private aksess. Med tanke på at det skal være mulig å nå dem fra subklasser, kan det noen ganger vurderes å gi dem protected aksess, slik at det også i subklasser er mulig å referere direkte til dem. Hensikten kunne være å øke effektiviteten, siden det er litt mer effektivt å gjøre direkte aksess på datafelter enn å gjøre det via set- og get-metoder. Men som det går fram av beskrivelsen i kapittel 8, vil konsekvensen bli at vi da kan referere direkte til dem også fra alle andre klasser som tilhører samme pakken. Aksessnivået protected er med andre ord ganske "åpent", det gir dårlig beskyttelse. For å gi en liten illustrasjon av dette, går vi tilbake til de tre klassene Ansatt, FagligAnsatt og Administrator i programeksempel 1 ovenfor. Som alternativ tenker vi oss at vi i klassen Ansatt brukte protected aksess på datafeltene, slik at klassen så ut som følger:

public class Ansatt
{
  protected String navn;
  protected Dato født, tiltrådt;
  protected int lønnstrinn;
  public static final int MIN = 30, MAX = 80;

  < resten av klassen som før >
}

Klassen Administrator kunne vi da ha skrevet på følgende måte:

public class Administrator extends Ansatt
{
  private String seksjon, stilling;
  public static final int ADMINMINIMUM = 41;

  public Administrator( String n, Dato f, Dato t, String seksj, String st )
  {
    super( n, f, t );
    seksjon = seksj;
    stilling = st;
    lønnstrinn = ADMINMINIMUM; // kan referere direkte til lønnstrinn
  }

  public String toString()
  {
    return super.toString() + ", seksjon: " + seksjon +
             ", stilling: " + stilling;
  }

  public void setLønnstrinn( int trinn )
  {
    if ( trinn < ADMINMINIMUM )
      trinn = ADMINMINIMUM;
    lønnstrinn = trinn;  // kan referere direkte til lønnstrinn
  }
}

På tilsvarende måte kunne vi endret klassen FagligAnsatt. Dersom vi nå i klassen Ansatte hadde opprettet objektene på samme måte som før, med instruksjonene

     direktør = new Administrator( "Siri Ledestjerne",
          new Dato( 13, 1, 1983 ), new Dato( 1, 1, 2009 ),
          "sentraladministrasjon", "direktør" );
     direktør.setLønnstrinn( 85 );
     sekretær = new Administrator( "Per Skrivekarl",
          new Dato( 17, 5, 1989 ), new Dato( 1, 4, 2010 ),
          "papirmølla", "sekretær" );
     sekretær.setLønnstrinn( 40 );
     matematikklærer = new FagligAnsatt( "Tone Regnevik",
          new Dato( 20, 6, 1985 ), new Dato( 1, 8, 2008 ),
          "matematikk" );
     programmeringslærer = new FagligAnsatt( "Else Fortun",
          new Dato( 29, 2, 1990 ), new Dato( 1, 8, 2006 ), "data" );
     programmeringslærer.setLønnstrinn( 57 );

ville følgende ha skjedd: Direktøren ville nå ha blitt tildelt lønnstrinn 85, i strid med virksomhetens begrensninger, for i Administrator-klassens metode setLønnstrinn er det ingen test på maksimalverdi. (Det var det heller ikke i vår opprinnelige Administrator-klasse.)

En annen sak er at dersom klassen Ansatte ligger i samme pakke som klassen Ansatt, så har vi i klassen Ansatte mulighet for å opprette objekter for eksempel på denne måte:

    direktør = new Administrator( "Siri Ledestjerne",
          new Dato( 13, 1, 1983 ), new Dato( 1, 1, 2009 ),
          "sentraladministrasjon", "direktør" );
     direktør.lønnstrinn = 90; // direkte tilgang til datafeltet,
                               // ingen kontroll med tilordnet verdi
     sekretær = new Administrator( "Per Skrivekarl",
          new Dato( 17, 5, 1989 ), new Dato( 1, 4, 2010 ),
          "papirmølla", "sekretær" );
     sekretær.lønnstrinn = 30; // direkte tilgang til datafeltet,
                               // ingen kontroll med tilordnet verdi

Når vi bruker protected aksess, gir vi altså mulighet til direkte aksess til datafeltene ikke bare fra subklasser, men fra alle andre klasser i samme pakke, forutsatt at klassene har tilgang til et objekt av vedkommende klassetype. Eksemplet ovenfor viste hvordan vi da kunne tilordne verdier utenfor det spesifiserte gyldighetsintervallet. Enda mer alvorlig er det at det da kunne være mulig å tilordne verdier som ville være helt meningsløse, slik som for eksempel en negativ verdi for lønnstrinn. Vi kunne risikere å ende opp med data helt uten mening, data som medførte at hele systemet brøt sammen.

Et annet problem som kan oppstå i tilfelle vi bruker protected aksess og dermed åpner for muligheten til å gjøre direkte aksess på datafeltene fra subklasser, er at vi inviterer til å programmere subklassene slik at de avhenger av hvordan superklassen er implementert. Dette kan ha som konsekvens at dersom vi gjør en liten endring i implementasjonen av superklassen, så må også alle subklasser endres. Det trenger ikke være større endring enn at vi gjør en liten navneendring i et datafelt. Vi får kode som er lite robust, siden en liten endring ett sted kan få store konsekvenser mange andre steder. Dessuten kan det resultere i funksjonelle feil som kan være vanskelige å oppdage.

Generelt bør vi programmere slik at det til en klasse ikke kan defineres subklasser som avhenger av hvordan klassen har implementert sine data. Subklasser bør bare være avhengig av det vi kan kalle superklassens tjenester, det vil si av dens public metoder, ikke av hvordan superklassen har implementert sine data. Følger vi denne regelen, kan vi endre implementasjonen av superklassen uten at det får noen konsekvenser for subklassene, forutsatt at superklassen fortsatt tilbyr de samme tjenestene, det vil si har metoder med samme signatur og virkemåte som tidligere.

Object-klassen

Alle javaklasser er direkte eller indirekte subklasse til klassebibliotekets klasse Object. Det betyr at dersom vi skriver

class Klassenavn
{
  < hva som helst av klasseinnhold >
}

så er dette likeverdig med at vi hadde skrevet

class Klassenavn extends Object
{
  < hva som helst av klasseinnhold >
}

Det betyr også at dersom vi definerer en subklasse til en annen klasse enn Object, så blir subklassen vår en indirekte subklasse til Object. Alle klasser vil altså arve de metodene som er definert i Object. Klassen ligger i pakken java.lang og blir derfor automatisk importert til alle javaprogrammer. Dokumentasjon av klassen Object kan du finne på adressen http://download.oracle.com/javase/8/docs/api/java/lang/Object.html. Den viktigste av metodene som arves er kanskje toString-metoden. Alle klasser vi skriver har altså en toString-metode. Dersom vi ikke eksplisitt skriver noen slik metode i klassen vår, er det da den som arves fra Object-klassen som vil bli utført i tilfelle kall. (Husk at dersom vi skjøter sammen et objekt med en streng ved å bruke pluss-operatoren, så vil det automatisk bli kall på objektets toString-metode.) Det sier seg selv at Object-klassens toString-metode må returnere ganske generelle opplysninger, siden det i Object-klassen er umulig å vite noe om hva som måtte befinne seg i dens utallige subklasser. Som eksempel kan vi ta for oss klassen Klokke som ble brukt i programeksempel 3 i kapittel 8. Dersom vi i dette programmets vindusklasse legger inn utskriftsinstruksjonen

    System.out.println( ur.toString() );

(der ur er et Klokke-objekt), så vil vi få en utskrift som

Klokke@ec16a4

Det eneste vi drar kjensel på her er klassenavnet. Det som følger etter er krøllalfa-tegnet etterfulgt av objektets såkalte hashkode uttrykt som et heksadesimalt tall uten fortegn. Dette er noe som vi svært sjelden trenger å ha kjennskap til. (For øvrig vil denne hashkoden endre seg fra kjøring til kjøring, siden den er avhengig av objektets memoryadresse.) Det sier seg derfor selv at for de aller fleste klasser som vi skriver, så vil det være fornuftig å redefinere toString-metoden slik at den returnerer forståelige og informative opplysninger om det objektet som brukes til å gjøre kall på metoden.

En annen metode fra Object-klassen som det er nyttig å ha kjennskap til, er equals-metoden. Dersom vi har to objekter a og b av en eller annen klassetype, kan vi alltid sammenlikne dem ved å skrive

  if ( a.equals( b ) )  // rekkefølgen for a og b spiller ingen rolle
    ...

Implementasjonen av equals-metoden i Object-klassen virker på den måten at den sjekker om a og b refererer til samme fysiske objekt, altså om pekerverdiene er lik hverandre. I mange tilfeller er det ikke dette vi er interessert i, men om to forskjellige objekter har samme innhold. Derfor vil det i mange tilfeller være aktuelt å redefinere equals-metoden slik at den utfører sammenlikning på den måten vi ønsker det. Slik redefinering er blant annet gjort i klassebibliotektets String-klasse, slik at metoden returnerer true i tilfelle to String-objekter har samme innhold, og returnerer false ellers.

Programeksempel 2

I følgende eksempel er det programmert klasser som representerer de geometrinske figurene i følgende hierarki:

For å begrense størrelsen på eksemplet, er det for hver figurtype bare registrert navn og relevante dimensjoner. Det er programmert metoder for beregning av areal og eventuelt volum, samt en toString-metode som returnerer en beskrivelse av figuren og dens data. I tillegg er det lagt inn nødvendige get-metoder. Siden alle figurklassene er lagt inn i samme fil, kan de ikke defineres som public-klasser. En javafil kan som kjent bare inneholde én klasse som er public. Figurklassene finnes i fila Figurtyper.java.

Klassen Figur på toppen av hierarkiet har bare som oppgave å inneholde det som er felles for alle figurtypene. Dens subklasse TodimensjonalFigur har som oppgave å inneholde det som i tillegg er felles for alle todimensjonale figurer, mens subklassen TredimensjonalFigur har tilsvarende oppgave for tredimensjonale figurer. Disse tre klassene representerer altså ikke konkrete figurtyper. Det normale ville derfor være å definere dem som såkalt abstrakte klasser, men slike blir først omtalt i neste kapittel.

Todimensjonale figurer har nettopp to dimensjoner. I klassen er disse kalt bredde og høyde. Det er vel dette som er mest vanlig i dagligspråket når vi omtaler for eksempel trekanter og firkanter. I klassen for tredimensjonale figurer er de to tilsvarende dimensjonene kalt lengde og bredde, mens dimensjonen høyde nettopp angir høyden. Dette er også i samsvar med dagligspråket, der vi for eksempel snakker om lengden, bredden og høyden til et hus. Når vi programmerer, må vi passe på at konstruktørene for subklassene overfører de riktige verdiene til sine superklasser når vi bruker referansen super. Der spiller det ingen rolle hvilket navn parametrene og datafeltene har. Det avgjørende er parameterrekkefølgen i konstruktøren. De aktuelle parametrene til super blir overført som parametre til superklassens konstruktør i den rekkefølge de har som parametre til super, uavhengig av hva de heter. For en sirkel må vi for øvrig huske på at både bredden og høyden er det dobbelte av radien. For en kule er både lengde, bredde og høyde det dobbelte av radien.

  1 //Figurtyper.java
  2 //Eksempel på klassehierarki
  3 import java.text.DecimalFormat;
  4
  5 class Figur
  6 {
  7   private String navn;
  8   DecimalFormat formatterer = new DecimalFormat("0.00");
  9                                 //skal brukes i subklassene
 10
 11   public Figur(String n)
 12   {
 13     navn = n;
 14   }
 15
 16   public String getNavn()
 17   {
 18     return navn;
 19   }
 20 }
 21
 22 class TodimensjonalFigur extends Figur
 23 {
 24   private double bredde, høyde;
 25
 26   public TodimensjonalFigur(String n, double b, double h)
 27   {
 28     super(n);
 29     bredde = b;
 30     høyde = h;
 31   }
 32
 33   public double getBredde()
 34   {
 35     return bredde;
 36   }
 37
 38   public double getHøyde()
 39   {
 40     return høyde;
 41   }
 42
 43   public String toString()
 44   {
 45     String beskrivelse = getNavn();
 46     beskrivelse += "\nBredde: " + bredde;
 47     beskrivelse += "\nHøyde: " + høyde;
 48     return beskrivelse;
 49   }
 50 }
 51
 52 class TredimensjonalFigur extends Figur
 53 {
 54   private double lengde, bredde, høyde;
 55
 56   public TredimensjonalFigur(String n, double l, double b, double h)
 57   {
 58     super(n);
 59     lengde = l;
 60     bredde = b;
 61     høyde = h;
 62   }
 63
 64   public double getLengde()
 65   {
 66     return lengde;
 67   }
 68
 69   public double getBredde()
 70   {
 71     return bredde;
 72   }
 73
 74   public double getHøyde()
 75   {
 76     return høyde;
 77   }
 78
 79   public String toString()
 80   {
 81     String beskrivelse = getNavn();
 82     beskrivelse += "\nLengde: " + lengde;
 83     beskrivelse += "\nBredde: " + bredde;
 84     beskrivelse += "\nHøyde: " + høyde;
 85     return beskrivelse;
 86   }
 87 }
 88
 89 class Rektangel extends TodimensjonalFigur
 90 {
 91   public Rektangel(double bredde, double høyde)
 92   {
 93     super("Rektangel", bredde, høyde);
 94   }
 95
 96   public double getAreal()
 97   {
 98     return getBredde() * getHøyde();
 99   }
100
101   public String toString()
102   {
103     String beskrivelse = super.toString();
104     double areal = getAreal();
105     beskrivelse += "\nAreal: " + formatterer.format(areal);
106     return beskrivelse;
107   }
108 }
109
110 class Kvadrat extends TodimensjonalFigur
111 {
112   public Kvadrat(double sidekant)
113   {
114     super("Kvadrat", sidekant, sidekant);
115   }
116
117   public double getAreal()
118   {
119     return getBredde() * getHøyde();
120   }
121
122   public String toString()
123   {
124     String beskrivelse = getNavn();
125     beskrivelse += "\nSidekant: " + getBredde();
126     double areal = getAreal();
127     beskrivelse += "\nAreal: " + formatterer.format(areal);
128     return beskrivelse;
129   }
130 }
131
132 class Sirkel extends TodimensjonalFigur
133 {
134   public Sirkel(double radius)
135   {
136     super("Sirkel", 2*radius, 2*radius);
137   }
138
139   public double getAreal()
140   {
141     double radius = getBredde()/2;
142     return Math.PI * radius * radius;
143   }
144
145   public String toString()
146   {
147     String beskrivelse = getNavn();
148     double radius = getBredde()/2;
149     beskrivelse += "\nRadius: " + formatterer.format(radius);
150     double areal = getAreal();
151     beskrivelse += "\nAreal: " + formatterer.format(areal);
152     return beskrivelse;
153   }
154 }
155
156 class Terning extends TredimensjonalFigur
157 {
158   public Terning(double sidekant)
159   {
160     super("Terning", sidekant, sidekant, sidekant);
161   }
162
163   public double getVolum()
164   {
165     double side = getBredde();
166     return side * side * side;
167   }
168
169   public double getOverflate()
170   {
171     double sidekant = getBredde();
172     return 6 * sidekant * sidekant;
173   }
174
175   public String toString()
176   {
177     String beskrivelse = getNavn();
178     beskrivelse += "\nSidekant: " + getBredde();
179     double areal = getOverflate();
180     beskrivelse += "\nOverflate: " + formatterer.format(areal);
181     double volum = getVolum();
182     beskrivelse += "\nVolum: " + formatterer.format(volum);
183     return beskrivelse;
184   }
185 }
186
187 class Kule extends TredimensjonalFigur
188 {
189   public Kule(double radius)
190   {
191     super("Kule", 2*radius, 2*radius, 2*radius);
192   }
193
194   public double getOverflate()
195   {
196     double radius = getBredde()/2;
197     return 4 * Math.PI * radius * radius;
198   }
199
200   public double getVolum()
201   {
202     double radius = getBredde()/2;
203     return 4 * Math.PI * radius * radius * radius / 3;
204   }
205
206   public String toString()
207   {
208     String beskrivelse = getNavn();
209     double radius = getBredde()/2;
210     beskrivelse += "\nRadius: " + formatterer.format(radius);
211     double areal = getOverflate();
212     beskrivelse += "\nOverflate: " + formatterer.format(areal);
213     double volum = getVolum();
214     beskrivelse += "\nVolum: " + formatterer.format(volum);
215     return beskrivelse;
216   }
217 }

For å kunne prøve ut figurklassene, er det definert en vindusklasse Figurvindu som er gjengitt nedenfor. Den inneholder en array som kan inneholde Figur-objekter og som dermed kan inneholde alle de forskjellige figurtypene som er definert. (Nærmere omtale og forklaring av virkemåte for arrayer som inneholder objekter tilhørende samme klassehierarki finner du i neste kapittel.) Vinduet inneholder funksjonalitet som gjør det mulig for brukeren å opprette objekter av de forskjellige figurtypene, legge dem inn i arrayen, samt få skrevet ut en oversikt over hvilke figurer som er lagt inn. Arrayens kapasitet er begrenset til 10 figurer, men dette er det lett å endre på.

 1 import javax.swing.*;
 2 import java.awt.*;
 3 import java.awt.event.*;
 4
 5 public class Figurvindu extends JFrame implements ActionListener
 6 {
 7   public static final int MAXANTALL = 10;
 8   public static final int SIRKEL = 1, KVADRAT = 2, REKTANGEL = 3,
 9           KULE = 4, TERNING = 5;
10   private Figur[] figurliste = new Figur[MAXANTALL];
11   private int antFigurer = 0;
12   private JTextField typefelt, breddefelt, høydefelt;
13   private JButton ny, oversikt;
14   private JTextArea utskriftsområde;
15
16   public Figurvindu()
17   {
18     super("Registrering av geometriske figurer");
19     typefelt = new JTextField(2);
20     breddefelt = new JTextField(5);
21     høydefelt = new JTextField(5);
22     ny = new JButton("Registrer figur");
23     ny.addActionListener(this);
24     oversikt = new JButton("Skriv figuroversikt");
25     oversikt.addActionListener(this);
26     utskriftsområde = new JTextArea(25, 40);
27     utskriftsområde.setEditable(false);
28     Container c = getContentPane();
29     c.setLayout(new FlowLayout());
30     c.add(new JLabel("Velg først figurtype, deretter dimensjon(er), " +
31             "og klikk Registrer figur."));
32     String figurtyper = SIRKEL + ": sirkel, " + KVADRAT +
33             ": kvadrat, " + REKTANGEL + ": rektangel, " +
34             KULE + ": kule, " + TERNING + ": terning";
35     c.add(new JLabel("Figurtype (" + figurtyper + ")"));
36     c.add(typefelt);
37     c.add(new JLabel("Bredde/sidekant/radius:"));
38     c.add(breddefelt);
39     c.add(new JLabel("Høyde"));
40     c.add(høydefelt);
41     c.add(ny);
42     c.add(oversikt);
43     c.add(new JScrollPane(utskriftsområde));
44   }
45
46   public void registrerFigur()
47   {
48     if (antFigurer < MAXANTALL)
49     {
50       Figur nyFigur;
51       int figurtype = Integer.parseInt(typefelt.getText());
52       double dimensjon1 = Double.parseDouble(breddefelt.getText());
53       if (dimensjon1 < 0.1)
54         dimensjon1 = 0.0;
55
56       switch(figurtype)
57       {
58         case REKTANGEL:  double dimensjon2 =
59                          Double.parseDouble(høydefelt.getText());
60                          if (dimensjon2 < 0.1)
61                            dimensjon2 = 0.0;
62                          nyFigur = new Rektangel(dimensjon1,
63                                  dimensjon2);
64                          break;
65         case SIRKEL:     nyFigur = new Sirkel(dimensjon1);
66                          break;
67         case KVADRAT:    nyFigur = new Kvadrat(dimensjon1);
68                          break;
69         case KULE:       nyFigur = new Kule(dimensjon1);
70                          break;
71         case TERNING:    nyFigur = new Terning(dimensjon1);
72                          break;
73         default:         nyFigur = null;
74       }
75       figurliste[antFigurer++] = nyFigur;
76     }
77     else
78       JOptionPane.showMessageDialog(this,
79               "Kan ikke registrere flere figurer!",
80               "Registeret er fullt!", JOptionPane.WARNING_MESSAGE);
81   }
82
83   public void skrivOversikt()
84   {
85     utskriftsområde.setText("REGISTRERTE FIGURER\n");
86     for (int i = 0; i < antFigurer; i++)
87       utskriftsområde.append(figurliste[i].toString() + "\n\n");
88   }
89
90   public void actionPerformed(ActionEvent e)
91   {
92     if (e.getSource() == ny)
93       registrerFigur();
94     else if (e.getSource() == oversikt)
95       skrivOversikt();
96   }
97 }

Klassen Figurregistrering er driverklasse for programmet. Nedenfor er det gjengitt et bilde fra en kjøring av programmet.

 1 import javax.swing.JFrame;
 2
 3 public class Figurregistrering
 4 {
 5   public static void main(String[] args)
 6   {
 7     Figurvindu vindu = new Figurvindu();
 8     vindu.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
 9     vindu.setSize(500, 560);
10     vindu.setVisible(true);
11   }
12 }

Forrige kapittel Neste kapittel

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