javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

5 Nedarvning

Indhold:

Kapitlet forudsættes i resten af bogen.

Forudsætter kapitel 4, Definition af klasser.

I dette kapitel vil vi se på, hvordan man kan genbruge programkode, ved at tage en eksisterende klasse og udbygge den med flere metoder og variabler (nedarvning).

5.1 At udbygge eksisterende klasser

Hvad gør man, hvis man ønsker en klasse, der ligner en eksisterende klasse, men alligevel ikke helt er den samme?

Svaret er: Man kan definere underklasser, der arver (genbruger en del af koden) fra en anden klasse og kun definerer den ekstra kode, der skal til for at definere underklassen i forhold til stamklassen (kaldet superklassen).

Arv (eng.: inheritance) er et meget vigtigt element i objektorienterede sprog. Med nedarvning kan man have en hel samling af klasser, der ligner hinanden på visse punkter, men som er forskellige på andre punkter.

5.1.1 Eksempel: En falsk terning

Hvis man vil snyde i terningspil, findes der et kendt trick: Bruge sine egne terninger, hvor man har boret 1'er-sidens hul ud, kommet bly i hullet og malet det pænt over, så det ikke kan ses. Sådan en terning vil have meget lille sandsynlighed for at få en 1-er og en ret stor sandsynlighed for at få en 6'er.

Herunder har vi lavet en nedarvning fra Terning til en ny klasse, FalskTerning, ved at starte erklæringen med:

public class FalskTerning extends Terning

Vi har automatisk overtaget (arvet) alle metoder og variabler fra Terning-klassen, fordi vi skriver "extends Terning". Dvs. at et FalskTerning1-objekt også har en værdi-variabel og en toString()-metode.

Vi ændrer nu klassens opførsel, ved at definere en anden udgave af kast()-metoden:

/** En Terning-klasse for falske terninger. */

public class FalskTerning1 extends Terning
{
  /** tilsidesæt kast med en "bedre" udgave */
  public void kast()
  {
    // udskriv så vi kan se at metoden bliver kaldt
    // System.out.println("[kast() på FalskTerning1] ");

    værdi = (int) (6*Math.random() + 1);

    // er det 1 eller 2? Så lav det om til 6!
    if ( værdi <= 2 ) værdi = 6;
  }
}

I klassediagrammet er nedarvningen vist med en hul pil fra FalskTerning1 til Terning.

Dette kaldes også en er-en-relation; FalskTerning1 er en Terning, da den jo har alle de egenskaber (variabler og metoder), en terning har.

Kort sagt:

En klasse kan arve variabler og metoder fra en anden klasse
Klassen, der nedarves fra, kaldes superklassen
Klassen, der arver fra superklassen, kaldes underklassen
Underklassen kan tilsidesætte (omdefinere) metoder arvet fra superklassen ved at definere dem igen

Andre steder i litteraturen er der brugt talrige betegnelser for superklasse, underklasse og tilsidesættelse. Her er et udpluk:

Superklasse kaldes også: Basisklasse, forældreklasse, stamklasse.

Underklasse kaldes også: Afledt klasse, nedarvet klasse, subklasse.

Tilsidesætte (eng.: override) kaldes også: Overstyre, overskrive.

I vores eksempel er superklassen Terning. Underklassen FalskTerning1 har tilsidesat metoden kast().

I det følgende program kastes med to terninger, en rigtig og en falsk:

public class Snydespil1
{
  public static void main(String[] arg)
  {
    Terning t1 = new Terning();
    FalskTerning1 t2 = new FalskTerning1();

    System.out.println("t1: "+t1); // kunne også kalde t1.toString()
    System.out.println("t2: "+t2);

    for (int i=0; i<5; i++)
    {
      t1.kast();
      t2.kast();
      System.out.println("t1=" + t1 + "  t2=" + t2);
      if (t1.værdi == t2.værdi) System.out.println("To ens!");
    }
  }
}

t1: 1
t2: 3
t1=1  t2=5
t1=1  t2=6
t1=4  t2=3
t1=6  t2=6
To ens!
t1=2  t2=6

Vi kan altså bruge FalskTerning1-objekter på præcis samme måde som Terning-objekter. Bemærk, hvordan t2 giver 6 meget oftere end t1.

5.1.2 At udbygge med flere metoder og variabler

Lad os nu se på et eksempel, hvor vi definerer nogle variabler og metoder i nedarvingen.

public class FalskTerning2 extends Terning
{
  public int snydeværdi;

  public void sætSnydeværdi(int nySnydeværdi)
  {
    snydeværdi = nySnydeværdi;
  }

  public void kast()
  {
    //System.out.println("[kast() på FalskTerning2] ");

    værdi = (int) (6*Math.random() + 1);

    // 1 eller 2? Så lav det om til snydeværdi!
    if ( værdi <= 2 ) værdi = snydeværdi;
  }
}

FalskTerning2 har fået en ekstra variabel, snydeværdi, og en ekstra metode, sætSnydeværdi()1, der sætter snydeværdi til noget andet.

public class Snydespil2
{
  public static void main(String[] arg)
  {
    FalskTerning2 t1 = new FalskTerning2();
    t1.sætSnydeværdi(4);

    for (int i=0; i<3; i++)
    {
      t1.kast();
      System.out.println("t1=" + t1);
    }
  }
}

t1=4
t1=4
t1=6

5.1.3 Nøgleordet super

Nogen gange ønsker man i en nedarvet klasse, at få adgang til superklassens metoder, selvom de måske er blevet tilsidesat med en ny definition i nedarvingen. F.eks. kunne det være rart, hvis vi kunne genbruge den oprindelige kast()-metode i FalskTerning.

Med super refererer man til de metoder, der er kendt for superklassen. Dermed kan vi skrive en smartere udgave af FalskTerning:

public class FalskTerning3 extends Terning
{
  public void kast ()
  {
    super.kast(); // kald den oprindelige kast-metode

    // blev det 1 eller 2? Så lav det om til en 6'er!
    if ( værdi <= 2 ) værdi = 6;
  }
}

super.kast() kalder kast()-metoden i superklassen. Derefter tager vi højde for, at det er en falsk terning.

5.1.4 Opgaver

  1. Lav en LudoTerning, der arver fra Terning. Tilsidesæt toString() med en, der giver "*" på en 3er og "globus" på en 4er (vink: kopiér Terning's toString()-metode over i LudoTerning og ret i den). Afprøv klassen.

  2. Byg videre på opgave 4.10 og opret klassen Transportmiddel. Et transportmiddel har en farve, et navn, et antal tilbagelagte kilometer og en nypris. Definér metoderne
    public void bevæg(int antalKilometer) // opdaterer antal kilometre
    public double pris() // giver den vurderede salgspris
    public String toString() // giver en beskrivelse af transportmidlet

    Opret nedarvingerne Cykel, Skib og Bil med hver sin pris()-metode.

  3. Forestil dig en virksomhed, hvor der er forskellige slags personer: Ansatte og kunder. De ansatte er delt op i medarbejdere og ledere. Skitsér et passende klassediagram.

  4. Lav klasserne til et skak-spil: Definér superklassen Brik med egenskaben farve (sort eller hvid), position x og position y (hver mellem 1 og 8). Definér også metoden
    public boolean kanFlytteTil(int xNy, int yNy) // om brikken kan flytte dertil
    der (for Brik) returnerer sand, hvis positionen eksisterer (xNy og yNy er mellem 1 og 8).
    Definér nedarvingerne Bonde og Taarn med tilsidesat kanFlytteTil().

  5. Lav et system til at arbejde med forskellige geometriske figurer.
    Opret klassen Figur med metoderne beregnAreal() og beregnOmkreds().
    Lav nedarvingerne Punkt, Linje (med variablen længde), Cirkel (med variablen radius), Rektangel (med variablerne højde og bredde).

5.2 Polymorfe variabler


Snydespil2MedPolymorfi
efter punkt A

Se på følgende eksempel:

public class Snydespil2medPolymorfi
{
  public static void main(String[] arg)
  {
    FalskTerning2 ft = new FalskTerning2();
    ft.sætSnydeværdi(4);

    Terning t;
    t = ft;
                                       // punkt A
    for (int i=0; i<3; i++)
    {
      t.kast();
      System.out.println("t=" + t);
    }
  }
}

t=4
t=6
t=6

Hov: Terning-variablen t refererer nu pludselig til et FalskTerning2-objekt ?!

    t = ft;

Der er altså ikke overensstemmelse mellem typen på venstre side (Terning) og typen på højre side (FalskTerning2). Hvad så med typesikkerheden?

5.2.1 Dispensation fra traditionel typesikkerhed

Typesikkerhed gør, at man ikke f.eks. kan tildele et Point-objekt til en Terning-variabel uden at få en sprogfejl under oversættelsen.

Hvis man kunne det, ville programmerne kunne indeholde mange fejl, der var svære at finde. Hvis man f.eks. et eller andet sted i et kæmpeprogram havde sat en Terning-variabel til at referere til et Point-objekt, og det var tilladt, hvad skulle der så ske, når man så (måske langt senere i en anden del af programmet) forsøgte at kalde dette objekts kast()-metode? Et Point-objekt har jo ingen kast()-metode. Det kunne blive meget svært at finde ud af, hvor den forkerte tildeling fandt sted. Sagt med andre ord: Normalt skal vi være lykkelige for, at Java har denne regel om typesikkerhed.

Der er imidlertid en meget fornuftig dispensation fra denne regel:

En variabel kan referere til objekter af en underklasse af variablens type

t-variablen har ikke ændret type (det kan variabler ikke), men den peger nu på et objekt af typen FalskTerning2. Dette objekt har jo alle metoder og data, som et Terning-objekt har, så vi kan ikke få kaldt ikke-eksisterende metoder, hvis vi bare "lader som om", den peger på et Terning-objekt. At FalskTerning2-objektet også har en objektvariabel, snydeværdi, og en ekstra metode, kan vi være ligeglade med, de kan bare ikke ses fra variablen.

Dispensationen giver altså mening, fordi en nedarving (f.eks. et FalskTerning2-objekt) set udefra kan lade, som om det også er af superklassens type (et Terning-objekt). Udefra har det jo mindst de samme objektvariabler og metoder, da det har arvet dem.

Selvom variablen t af typen Terning refererer til et FalskTerning2-objekt, kan man kun bruge den til at anvende metoder/variabler i objektet, som stammer fra Terning-klassen:

  t.snydeværdi = 4;  // sprogfejl: snydeværdi er ikke defineret i Terning
  t.sætSnydeværdi(4);// sprogfejl: sætSnydeværdi() er ikke defineret i Terning

5.2.2 Polymorfi

En anden meget væsentlig detalje omkring denne dispensation er, at det er objektets type, ikke variablens, der bestemmer, hvilken metodekrop der bliver udført, når vi kalder en metode:

  t.kast();  // kalder FalskTerning2's kast, 
            // fordi t peger på et FalskTerning2-objekt.

Herover kalder vi altså den kast()-metode, der findes i FalskTerning2-klassen. Den kigger således ikke på variablen t's type (så ville den jo bare udføre kast()-metoden i Terning).

Variablens type bestemmer, hvilke metoder der kan kaldes
Objektets type bestemmer, hvilken metodekrop der bliver udført

Af samme grund kaldes det at definere en metode, som allerede findes, fordi den er arvet, for tilsidesættelse af metoden. Man tilsidesætter metodens opførsel med en anden opførsel (en anden metodekrop).

5.2.3 Eksempel på polymorfi: Brug af Raflebaeger

Da FalskTerning2-objekter også er af type Terning, kan de bruges i et Raflebaeger:

public class SnydeMedBaeger
{
  public static void main(String[] arg)
  {
    Raflebaeger bæger = new Raflebaeger(0); // opret et bæger med nul terninger

    Terning t = new Terning();
    bæger.tilføj(t);    // føj en almindelig terning til bægeret

    FalskTerning2 ft = new FalskTerning2();
    ft.sætSnydeværdi(6);
    bæger.tilføj(ft);   // tilføj() får et objekt af typen Terning,
                        // og dermed også af typen FalskTerning2.

    ft = new FalskTerning2();
    ft.snydeværdi=6;
    t=ft;               // t bruges som mellemvariabel for sjov.
    bæger.tilføj(t);

    for (int i=1; i<10; i++)
    {
      bæger.ryst();
    }
  }
}

I SnydeMedBaeger kaldes Raflebaeger's ryst()-metode. Hvis du nu kigger i definitionen af dennes ryst()-metode (se afsnit 4.5.1), kan du se, at den kalder kast()-metoden på de enkelte objekter i "terninger"-listen:

  public void ryst()
  {
    for (Terning t : terninger) 
    {
      t.kast();
    }
  }

Da to af objekterne, vi har lagt ind i bægeret, er af typen FalskTerning2, vil Raflebaeger's ryst()-metode, når den kommer til et objekt af denne type, kalde FalskTerning2's kast() helt automatisk. Resultatet er altså, at vi får større sandsynlighed for at få seksere.

Faktisk har vi ændret den måde, et Raflebaeger-objekt opfører sig på, helt uden at ændre i Raflebaeger-klassen! Raflebaeger ved ikke noget om FalskTerning2, men kan alligevel bruge den.

En programmør kan altså lave en Raflebaeger-klasse, som kan alt muligt smart: Kaste terninger, se hvor mange ens der er, tælle summen af øjnene, se om der er en stigende følge (eng.: straight) osv.

Når en anden programmør vil lave en ny slags terning (f.eks. en snydeterning), behøver han ikke sætte sig ind i, hvordan Raflebaeger-klassen virker og lave tilpasninger af den, for den kan automatisk bruge hans egen nye slags terning!

5.2.4 Hvilken vej er en variabel polymorf ?

Når følgende er muligt:

    Terning t;
    FalskTerning2 ft;

    ft = new FalskTerning2();
    t = ft;

Hvad så med det omvendte? Kan man tildele en FalskTerning2-variabel en reference til et objekt af typen Terning?

Svaret er: Nej!

D


Efter punkt A
(programmet vil ikke oversætte)

et er jo typen af ft (FalskTerning2), der bestemmer, hvilke metoder og variabler vi kan bruge med ft. Dvs. vi ville kunne skrive:

    Terning t;
    FalskTerning2 ft;

    t = new Terning();
    ft = t;                  // sprogfejl
    ft.snydeværdi = 2;

Hvis den sidste sætning kunne udføres, ville det være uheldigt: Terning-objektet som ft refererer til, har jo ingen snydeværdi!

Det er altså et brud på typesikkerhedsreglen og Java tillader det derfor ikke. Man må ikke kunne kalde noget, der ikke findes på objektet.

Bemærk, at her, som i andre sammenhænge, kigger Java kun på en linje ad gangen. F.eks. giver nedenstående stadig en sprogfejl, selvom det i princippet kunne lade sig gøre:

    Terning t;
    FalskTerning2 ft;

    t = new FalskTerning2();
    ft = t;                  // sprogfejl
    ft.snydeværdi = 2;

Her refererer ft i sidste linje til et rigtigt FalskTerning2-objekt, og den sidste linje ville derfor give mening, men programmet kan ikke oversættes, fordi typesikkerhedsreglen med dispensation ikke er opfyldt.

Et andet eksempel

Forestiller vi os den generelle klasse Dyr, med nedarvinger Hest og Hund, kan man skrive

    Dyr d = new Hest();

da en Hest er-et Dyr. Men vi kan ikke skrive

    Hest h = new Dyr();

da Dyr er en generel klasse, der kunne være et hvilket som helst slags dyr (herunder f.eks. også en hund). Den må kan vi ikke lægge den ind i en Hest-variabel.

5.2.5 Reference-typekonvertering

Dispensationen i typesikkerhedsreglen svarer til den implicitte værditypekonvertering: Ved konvertering fra int til double behøver programmøren ikke angive eksplicit, at denne værdi skal forsøges konverteret. Når en typekonvertering med garanti giver det ønskede, laver Java den implicit.

I foregående eksempel så vi noget, der burde gå godt, men hvor Javas typeregel forhindrer oversættelse. Her er vi nødt til at bruge eksplicit reference-typekonvertering:


Efter punkt A

  Terning t;
  FalskTerning2 ft;

  t = new FalskTerning2();
  ft = (FalskTerning2) t; // OK, men muligvis
                          // køretidsfejl

                          // punkt A
  ft.snydeværdi = 2;

Det ligner en almindelig eksplicit værditypekonvertering (eng.: cast) og Javas betegnelse for det er også det samme.

Hvis reference-typekonverteringen går galt (det opdages først under programudførelsen), kommer der en køretidsfejl (undtagelsen ClassCastException opstår) og programmet stopper.

Der er dog nogle tilfælde, hvor Java, selv når man har lavet en reference-typekonvertering, kan opdage en uheldig konvertering. Hvis de to klasser, der forsøges at konverteres imellem, ikke arver fra hinanden, får man en sprogfejl på oversættertidspunktet:

  Terning t = new Terning();
  Point p;
  p = (Point) t;         // Sprogfejl: Point og Terning er urelaterede

5.3 Eksempel: Et matador-spil

Med arv kan man skabe et hierarki af klasser, der ligner hinanden (fordi de har alle fællestrækkene fra superklassen) og samtidig kan opføre sig forskelligt (polymorfi).

Her er vist klassediagrammet fra et matadorspil. Det er en skitse; et rigtigt matadorspil ville indeholde flere detaljer.

Øverst har vi klassen Felt, som indeholder fællestrækkene for alle matadorspillets felter. F.eks. skal alle felter kunne håndtere, at spilleren lander på eller passerer feltet. Vi forestiller os, at metoderne landet() og passeret() bliver kaldt af en anden del af programmet, når en spillers brik henholdsvis lander på eller passerer feltet. I Felt-klassen er metoderne defineret til ikke at gøre noget. Alle felter har også et navn, f.eks. "Hvidovrevej".

/** Superklassen for alle matadorspillets felter */
public class Felt
{
  String navn;

  /** kaldes når en spiller passerer dette felt */
  public void passeret(Spiller sp) 
  {
    sp.besked("Du passerer "+navn);
  }

  /** kaldes når en spiller lander på dette felt */
  public void landet(Spiller sp)
  {
  }
}

Læg mærke til, at der er forskel mellem 'sp.navn' (spillerens navn) og 'navn' (Felt-objektets navn).

Under Felt har vi klasserne Helle, Start, Rederi og Gade, der indeholder data og programkode, der er specifik for de forskellige slags felter i matadorspillet. De arver alle fra Felt og er derfor tegnet med en er-en-relation til Felt.

Klassen Helle er simpel; den skal lægge 15000 kr. til spillerens kassebeholdning, hvis spilleren lander på feltet. Dette gøres ved at tilsidesætte den nedarvede landet()-metode med en, der overfører penge til spilleren.

/** Helle - hvis man lander her får man en gevinst */
public class Helle extends Felt
{
  double gevinst;

  public Helle (int gevinst)
  {
    navn="Helle";                   // navn er arvet fra Felt
    this.gevinst=gevinst;
  }

  public void landet(Spiller sp)    // tilsidesæt metode i Felt
  {
    sp.besked("Du lander på helle og får overført "+gevinst);
    sp.transaktion(gevinst);        // opdater spillers konto
  }
}

I konstruktøren sætter vi feltets navn. Gevinsten ved at lande her er en parameter til konstruktøren. Metodekaldet sp.transaktion(gevinst) beder spiller-objektet om at føje gevinsten til kontoen.

Klassen Start skal overføre 5000 kr. til spilleren, der passerer eller lander på feltet. Dette gøres ved at tilsidesætte både landet() og passeret().

/** Startfeltet - når en spiller passerer dette felt, får han 5000 kr */
public class Start extends Felt
{
  double gevinst;

  public Start(double gevinst)
  {
    navn="Start";
    this.gevinst=gevinst;
  }

  public void passeret(Spiller sp)                 // tilsidesæt metode i Felt
  {
    sp.besked("Du passerer start og modtager "+gevinst);
    sp.transaktion(gevinst);                      // kredit/debit af konto
  }

  public void landet(Spiller sp)                   // tilsidesæt metode i Felt
  {
    sp.besked("Du lander på start og modtager "+gevinst);
    sp.transaktion(gevinst);
  }
}

Nu kommer vi til felter, der kan ejes af en spiller, nemlig rederier og gader. De har en ejer-variabel, der refererer til en Spiller (og er derfor tegnet med en har-relation til klassen Spiller), en pris og en leje for at lande på grunden.

/** Et rederi, der kan købes af en spiller */
public class Rederi extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;

  public Rederi(String navn, double pris, double leje)
  {
    this.navn = navn;
    this.pris = pris;
    this.grundleje = leje;
  }

  public void landet(Spiller sp)
  {
    sp.besked("Du er landet på "+navn);
    if (sp==ejer)
    {                                       // spiller ejer selv grunden
      sp.besked("Det er din egen grund");
    }
    else if (ejer==null)
    {                                       // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        if (sp.spørgsmål("købe "+navn+" for "+pris))
        {
          sp.transaktion( -pris );
          ejer=sp;
        }
      }
      else sp.besked("Du har ikke penge nok til at købe "+navn);
    }
    else
    {                                       // feltet ejes af anden spiller
      sp.besked("Leje: "+grundleje);
      sp.betal(ejer, grundleje);            // spiller betaler til ejeren
    }
  }
}

Når en spiller lander på et rederi, skal der overføres penge fra spilleren til ejeren af grunden. Dette gøres ved at tilsidesætte den nedarvede landet()-metode med en, der overfører beløbet mellem parterne. Først tjekkes, om spilleren er den samme som ejeren (sp==ejer). Hvis dette ikke er tilfældet, tjekkes, om der ingen ejer er (ejer==null) og hvis der ikke er, kan spilleren købe grunden (ejer sættes lig spilleren). Ellers beordres spilleren til at betale et beløb til ejeren: sp.betal(ejer, grundleje).

Klassen Gade repræsenterer en byggegrund og objekter af type Gade har derfor, ud over ejer, pris og grundleje, en variabel, der husker, hvor mange huse der er bygget på dem.

Når en spiller lander på grunden, skal der ske nogenlunde det samme som for et Rederi bortset fra, at hvis det er ejeren, der lander på grunden, kan han bygge et hus.

/** En gade, der kan købes af en spiller og bebygges */
public class Gade extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;
  int antalHuse = 0;
  double huspris;

  public Gade(String navn, double pris, double leje, double huspris)
  {
    this.navn=navn;
    this.pris=pris;
    this.grundleje=leje;
    this.huspris=huspris;
  }

  public void landet(Spiller sp)
  {
    sp.besked("Du er landet på "+navn);

    if (sp==ejer)
    {                                        // eget felt
      sp.besked("Det er din egen grund");
      if (antalHuse<5 && sp.konto>huspris &&  // bemærk: kun hvis betingelserne
        sp.spørgsmål("købe hus for "+huspris))// er opfyldt stilles spørgsmålet
      {                                     // byg et hus
        sp.besked("Du bygger hus på "+navn+" for "+huspris);
        ejer.transaktion( -huspris );
        antalHuse = antalHuse + 1;
      }
    }
    else if (ejer==null)
    {                                        // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        if (sp.spørgsmål("købe "+navn+" for "+pris))
        {
          sp.transaktion( -pris );
          ejer=sp;
        }
      }
      else sp.besked("Du har ikke penge nok til at købe "+navn);
    }
    else
    {                                        // felt ejes af anden spiller
      double leje = grundleje + antalHuse * huspris;
      sp.besked("Leje: "+leje);
      sp.betal(ejer, leje);                 // spiller betaler til ejeren
    }
  }
}

Et spil kunne opbygges ved at lægge forskellige felter ind i en liste, for at få et bræt:

import java.util.*;

/** Et matadorspil for to spillere */
public class SpilMatador
{
  public static void main(String[] arg)
  {
    Spiller sp1=new Spiller("Søren",50000);   // opret spiller 1
    Spiller sp2=new Spiller("Gitte",50000);   // opret spiller 2

    ArrayList<Felt> felter=new ArrayList<Felt>(); // indeholder alle felter
    felter.add(new Start(5000));
    felter.add(new Gade("Gade 1",10000, 400,1000));
    felter.add(new Gade("Gade 2",10000, 400,1000));
    felter.add(new Gade("Gade 3",12000, 500,1200));
    felter.add(new Rederi("Maersk",17000,4200));
    felter.add(new Gade("Gade 5",15000, 700,1500));
    felter.add(new Helle(15000));
    felter.add(new Gade("Gade 7",20000,1100,2000));
    felter.add(new Gade("Gade 8",20000,1100,2000));
    felter.add(new Gade("Gade 9",30000,1500,2200));

    // løb igennem 20 runder
    for (int runde = 0; runde<20; runde=runde+1)
    {
      sp1.tur(felter);
      sp2.tur(felter);
    }
  }
}

Man kan så lave en simpel tur()-metode, der rykker en spiller rundt på felterne. Den placerer vi i klassen Spiller sammen med oplysningerne om spilleren:

import java.util.*;

/** Definition af en spiller */
public class Spiller
{
  String navn;
  double konto;
  int feltnr;

  public Spiller(String navn, double konto)
  {
    this.navn=navn;
    this.konto=konto;
    feltnr = 0;
  }

  /** En besked til spilleren */
  public void besked(String besked)
  {
    System.out.println(navn+": "+besked);
  }

  /** Et ja/nej-spørgsmål. Returnerer true hvis ja, false hvis nej */
  public boolean spørgsmål(String spørgsmål)
  {
    String spm = navn+": Vil du "+spørgsmål+"?";
    String svar = javax.swing.JOptionPane.showInputDialog(spm,"ja");
    if (!"ja".equalsIgnoreCase(svar)) return false;
    System.out.println(navn+": Vil du "+spørgsmål+"? ja");
    return true;
  }

  public void transaktion(double kr)
  {
    konto = konto + kr;
    System.out.println(navn+"s konto lyder nu på "+konto+" kr.");
  }

  public void betal(Spiller modtager, double kr)
  {
    System.out.println(navn+" betaler "+modtager.navn+": "+kr+" kr.");
    modtager.transaktion(kr);
    transaktion(-kr);
  }

  public void tur(ArrayList<Felt> felter)
  {
    int slag = (int)(Math.random()*6)+1;              // et terningkast
    System.out.println("***** "+navn+" på felt "+feltnr+" slår "+slag+" *****");
    
    for (int i=1; i<=slag; i=i+1)  // nu rykkes der
    {
      // gå til næste felt: tæl op, hvis vi når over antal felter så tæl fra 0
      feltnr = (feltnr + 1) % felter.size();
      Felt felt;
      felt = felter.get(feltnr);
      if (i<slag) felt.passeret(this); // kald passeret() på felter vi passerer
      else felt.landet(this);          // kald landet() på sidste felt
    }
    try { Thread.sleep(3000); } catch (Exception e) {}// tur færdig, vent 3 sek.
  }
}

Metoden tur() behandler alle felterne som objekter af typen Felt og kalder passeret() på hver af dem og landet()-metoden på den sidste. Bemærk hvordan vi med this overfører en reference til spilleren selv, når vi kalder passeret() og landet() på Felt-objekterne. Disse objekter kan så kalde metoder tilbage på spilleren.

Linjen nederst i tur():

    try { Thread.sleep(3000); } catch (Exception e) {}

får programmet til at holde en pause i tre sekunder, inden det går videre (try og catch vil blive forklaret i kapitel 14, Undtagelser).

Bemærk også, hvordan vi sørger for, at variablen feltnr forbliver at have en værdi mellem 0 og antallet af felter med operatoren %, der giver resten af en division (se afsnit 2.11.4).

Her ses uddata fra en kørsel af programmet:

***** Søren på felt 0 slår 5 *****
Søren: Du passerer Gade 1
Søren: Du passerer Gade 2
Søren: Du passerer Gade 3
Søren: Du passerer Maersk
Søren: Du er landet på Gade 5
Søren: Vil du købe Gade 5 for 15000.0? ja
Sørens konto lyder nu på 35000.0 kr.
***** Gitte på felt 0 slår 2 *****
Gitte: Du passerer Gade 1
Gitte: Du er landet på Gade 2
Gitte: Vil du købe Gade 2 for 10000.0? ja
Gittes konto lyder nu på 40000.0 kr.
***** Søren på felt 5 slår 5 *****
Søren: Du passerer Helle
Søren: Du passerer Gade 7
Søren: Du passerer Gade 8
Søren: Du passerer Gade 9
Søren: Du lander på start og modtager 5000.0
Sørens konto lyder nu på 40000.0 kr.
***** Gitte på felt 2 slår 2 *****
Gitte: Du passerer Gade 3
Gitte: Du er landet på Maersk
Gitte: Vil du købe Maersk for 17000.0? ja
Gittes konto lyder nu på 23000.0 kr.
***** Søren på felt 0 slår 6 *****
Søren: Du passerer Gade 1
Søren: Du passerer Gade 2
Søren: Du passerer Gade 3
Søren: Du passerer Maersk
Søren: Du passerer Gade 5
Søren: Du lander på helle og får overført 15000.0
Sørens konto lyder nu på 55000.0 kr.
***** Gitte på felt 4 slår 3 *****
Gitte: Du passerer Gade 5
Gitte: Du passerer Helle
Gitte: Du er landet på Gade 7
Gitte: Vil du købe Gade 7 for 20000.0? ja
Gittes konto lyder nu på 3000.0 kr.
***** Søren på felt 6 slår 5 *****
Søren: Du passerer Gade 7
Søren: Du passerer Gade 8
Søren: Du passerer Gade 9
Søren: Du passerer start og modtager 5000.0
Sørens konto lyder nu på 60000.0 kr.
Søren: Du er landet på Gade 1
Søren: Vil du købe Gade 1 for 10000.0? ja
Sørens konto lyder nu på 50000.0 kr.
***** Gitte på felt 7 slår 5 *****
Gitte: Du passerer Gade 8
Gitte: Du passerer Gade 9
Gitte: Du passerer start og modtager 5000.0
Gittes konto lyder nu på 8000.0 kr.
Gitte: Du passerer Gade 1
Gitte: Du er landet på Gade 2
Gitte: Det er din egen grund
Gitte: Vil du købe hus for 1000.0? ja
Gitte: Du bygger hus på Gade 2 for 1000.0
Gittes konto lyder nu på 7000.0 kr.
***** Søren på felt 1 slår 1 *****
Søren: Du er landet på Gade 2
Søren: Leje: 1400.0
Søren betaler Gitte: 1400.0 kr.
Gittes konto lyder nu på 8400.0 kr.
Sørens konto lyder nu på 48600.0 kr.
***** Gitte på felt 2 slår 1 *****
Gitte: Du er landet på Gade 3
Gitte: Du har ikke penge nok til at købe Gade 3
***** Søren på felt 2 slår 2 *****
Søren: Du passerer Gade 3
Søren: Du er landet på Maersk
Søren: Leje: 4200.0
Søren betaler Gitte: 4200.0 kr.
Gittes konto lyder nu på 12600.0 kr.
Sørens konto lyder nu på 44400.0 kr.

... (og så videre)

5.3.1 Polymorfi

Polymorfi vil sige, at objekter af forskellig type bruges på en ensartet måde uden hensyn til deres præcise type.

Matadorspillet udnytter polymorfi til at behandle alle feltobjekter ens (ved at kalde landet() og passeret() fra Spiller's tur()-metode), selvom de er af forskellig type.

Fidusen er, at programkoden i tur()-metoden kan skrives uafhængigt af, hvilke felt-typer der findes: De rigtige landet()- og passeret()-metoder i nedarvingerne vil automatisk blive kaldt, selvom tur() kun kender til Felt-klassen.

Polymorfi er et kraftfuldt redskab til at lave meget fleksible programmer, der senere kan udvides, uden at der skal ændres ret meget i den eksisterende kode.

For eksempel kan vi til enhver tid udbygge matadorspillet med flere felttyper uden at skrive programmet om. Den programkode, der arbejder på felterne, Spiller-klassens tur()-metode, kender faktisk slet ikke til andre klasser end Felt!

En forudsætning for at udnytte polymorfi-mekanismen er, at objekterne "sørger for sig selv", dvs. at data og programkode er i de objekter, som de handler om.

5.3.2 Opgaver

  1. Tilføj en Bryggeri-klasse til matadorspillet og prøv, om det virker.
    Du kan evt. kopiere Gade.java i stedet for at skrive koden forfra. Husk at indsætte et bryggeri i felter-listen i main().
    På bryggerier afhænger lejen af, hvor stort et slag spilleren slog, da han landede på feltet, men lad foreløbig lejen være tilfældig.

  2. Spiller-klassen i eksemplet svarer altid 'ja' til når den får kaldt spørgsmål() med et spørgsmål. Lav en nedarving til Spiller, InteraktivSpiller, der har omdefineret spørgsmål() til rent faktisk at spørge brugeren (se afsnit 2.12.2, Indlæsning fra tastaturet før JDK 1.5).

5.4 Stamklassen Object

Klassen Object (i pakken java.lang) er 'alle klassers moder', dvs. superklasse for alle andre klasser. Arver en klasse ikke fra noget andet, vil den automatisk arve fra Object.

Alle klasser arver fra Object

Således arver f.eks. Terning fra Object. FalskTerning1 arver indirekte fra Object gennem Terning. Alle standardklasserne arver også fra Object, muligvis gennem andre klasser.

Det er derfor, at bl.a. toString()-metoden findes på alle objekter; den er defineret i Object og arves til alle klasser i Java. Her ses, hvad der sker, hvis man udskriver et (f.eks. Boks-) objekt, der ikke har sin egen toString():

...
  Boks b = new Boks();
  System.out.println("b = "+b);   // b.toString() kaldes implicit
...

  b = Boks@4852d1b0

Den bruger toString() fra Object og man kan altså se, at implementationen af toString() i Object returnerer klassens navn, et '@' og et tal2, f.eks. Boks@4852d1b0.

En anden metode i Object er equals(). Den har vi brugt til at undersøge, om strenge er ens, men den findes altså på ethvert objekt og kan f.eks. også bruges til at undersøge, om to lister indeholder de samme elementer. Bemærk, at man selv skal definere equals()-metoden på sine egne klasser, før den fungerer som forventet3.

5.4.1 Referencer til objekter

Når alle objekter arver fra Object, kan man i en variabel af denne type gemme en reference til ethvert slags objekt, jf. reglerne om typekonvertering:

  Object o;
  o = new Point();
  o = "hej";
  o = new FalskTerning2();

Omvendt kan man ikke rigtig bruge variablen til noget, før man har lavet en eksplicit typekonvertering af referencen o:

  Terning t;
  t = (Terning) o;
  t.kast();

Her var vi heldige, at o faktisk refererede til en (underklasse af) Terning. Vi får først at vide, om typekonverteringen er gået godt på kørselstidspunktet.

Tilsvarende kan man bruge Object som parameter/returtype og få et fleksibelt, men ikke særlig sikkert program. Klassen ArrayList benytter sig af dette: Den er en liste af typen Object, det er derfor, man kan gemme alle slags objekter i den.

  ArrayList l = new ArrayList();
  Point p = new Point();
  l.add(p);

Metoden add() får noget af typen Object (dvs. et hvilket som helst objekt) som parameter.

Nedenfor er vist det samme, men her bruges en mellemvariabel, der illustrerer, at der sker en implicit reference-typekonvertering (p skal jo konverteres fra en Point-reference til en Object-reference):

  ArrayList l = new ArrayList(); // liste med alle slags objekter (type Object)
  Point p = new Point();
  Object o;                      // overflødig mellemvariabel
  o = p;                         // implicit reference-typekonvertering
  l.add(o);

Når man kalder get() for at få fat i objektet igen, er det nødvendigt med en eksplicit reference-typekonvertering, fordi konverteringen sker den anden vej, fra superklasse til underklasse:

  p = (Point) l.get(0);

Igen vises det samme blot med mellemvariabel, så man kan se, hvilken typekonvertering der finder sted:

  o = l.get(0);        // ingen konvertering
  p= (point) o;        // eksplicit reference-typekonvertering

5.5 Konstruktører i underklasser

Vi minder om, at:

Underklassen skal selv definere, hvordan dets objekter skal kunne oprettes, så den skal selv definere sine konstruktører. Den kan have færre eller flere konstruktører end superklassen.

Når man definerer en konstruktør på en underklasse, skal man kun initialisere den nye del af objektet. Har man f.eks. tilføjet nye variabler, skal konstruktøren initialisere dem.

Den arvede del af objektet initialiseres ved, at man fra underklassens konstruktør kalder en konstruktør fra superklassen. Dette gøres med sætningen: "super(...);" med eventuelle parametre. Man bruger altså her super som en metode. Det skal gøres som den første sætning i konstruktøren. Hvis man ikke selv kalder super() som det første, sker der det, at super bliver kaldt automatisk uden parametre.

Konstruktører skal defineres på ny i en underklasse
En konstruktør kalder først en af superklassens konstruktører
Superklassens konstruktør kan kaldes med: super(parametre)
Hvis man ikke selv kalder en af superklassens konstruktører, indsætter Java automatisk et kald af superklassens konstruktør uden parametre


Boks3medDensitet tillader oprettelse på andre måder end superklassen Boks3

Herunder definerer vi Boks3medDensitet, der arver fra Boks3. Den nye egenskab er massefylden og metoden vægt(). Den skal kunne oprettes med: new Boks3medDensitet(), som opretter boksen med nogle standardværdier eller med: new Boks3medDensitet(lgd,b,h,d), hvor d er densiteten (massefylden).

public class Boks3medDensitet extends Boks3
{
  private double densitet;

  public Boks3medDensitet()
  {
    // super(); kaldes hvis intet andet angives
    densitet = 10.0;
  }

  public Boks3medDensitet(double lgd, double b,  
          double h, double densitet)
  {
    // vælg en anden konstruktør i superklassen end den uden parametre
    super(lgd,b,h); 
    this.densitet = densitet;
  }

  public double vægt()
  {
    return volumen() * densitet;    // superklassen udregner volumen for os
  }
}

5.5.1 Konsekvenser

Ovenstående regler kombineret med reglerne for standardkonstruktøren har nogle pudsige konsekvenser. Lad os se på et eksempel med al overflødig kode skåret væk:

public class A
{
  public A(int i) { }
}
public class B extends A
{
}

Klassen B vil ikke oversætte, fordi den af Java vil blive lavet om til:

public class B extends A
{
  public B() // standardkonstruktør indsættes automatisk af Java
  {
    super();
  }
}

Standardkonstruktøren i B vil altså prøve at kalde konstruktøren i A uden parametre, men den findes jo ikke, fordi A har en anden konstruktør. Oversætteren kommer med fejlmeddelelsen "constructor A() not found". Så er vi nødt til at angive konstruktøren i superklassen:

public class B extends A
{
  public B()    // vi er nødt til at definere underklassens konstruktør...
  {
    super(42); // ... så vi kan angive parametre til superklassens konstruktør
  }
}

Hvis vi ønsker en konstruktør med de samme parametre i B som i A, vil det se således ud:

public class B extends A
{
  public B(int i)    // vi er nødt til at definere underklassens konstruktør...
  {
    super(i); // ... så vi kan angive parametre til superklassens konstruktør
  }
}

Der er til gengæld ingen problemer med:

public class A2
{
}

og

public class B2 extends A2
{
}

Java laver det om til:

public class A2
{
  public A2() { } // indsættes automatisk af Java
}

og

public class B2 extends A2
{
  public B2()     // indsættes automatisk af Java
  {
    super();
  }
}

5.6 Ekstra eksempler

5.6.1 Matadorspillet version 2

Dette eksempel viser, hvordan man kan spare programkode (og dermed programmeringstid) med nedarvning. Samtidig viser det brugen af konstruktører i underklasser.

Se igen på programkoden til Rederi og Gade. Der er meget programkode, som er ens for de to klasser. Faktisk implementerer de kode, der er fælles for alle grunde, der kan ejes af en spiller og derfor vil det være hensigtsmæssigt, at følgende kode var i en Grund-klasse:

Det har vi gjort herunder. Vi har været forudseende og flyttet beregningen af lejen ud fra landet() og ind i en separat metode beregnLeje(), fordi netop denne er meget forskellig for Rederi og Gade.

/** Mellemklasse mellem 'Felt' og underliggende klasser som Gade og Rederi */

public class Grund2 extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;

  public Grund2(String navn, double pris, double leje)
  {
    this.navn=navn;
    this.pris=pris;
    this.grundleje=leje;
  }

  public double beregnLeje()
  {
    return grundleje;
  }

  public void landet(Spiller sp)
  {
    sp.besked("Du er landet på "+navn);
    if (sp==ejer)
    {                                       // spiller ejer feltet
      sp.besked("Det er din egen grund");
    }
    else if (ejer==null)
    {                                       // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        if (sp.spørgsmål("købe "+navn+" for "+pris))
        {
          sp.transaktion( -pris );
          ejer=sp;
        }
      }
      else sp.besked("Du har ikke penge nok til at købe "+navn);
    }
    else
    {                                       // felt ejes af anden spiller
      double leje = beregnLeje();           // udregn lejen
      sp.besked("Leje: "+leje);
      sp.betal(ejer, leje);                 // spiller betaler til ejeren
    }
  }
}

Nu er Rederi ret nem. Den skal nemlig (i denne simple udgave) opføre sig præcis som Grund2. Vi skal blot definere konstruktøren, som skal kalde den tilsvarende konstruktør i Grund2:

/** Et rederi, der kan købes af en spiller */

public class Rederi2 extends Grund2 
{
  public Rederi2(String navn, double pris, double leje)
  {
    super(navn, pris, leje); // overfør værdierne til superklassens konstruktør
  }
}

Nu kommer vi til Gade. Her er beregnLeje() tilsidesat til også at tage højde for antallet af huse. Med super kan vi faktisk spare en hel del arbejde. Gaderne kan genbruge meget af landet()-metoden, men der er dog en ekstra mulighed for at bygge hus. Derfor kalder vi superklassens landet()-metode. Derefter, hvis spilleren, der er landet på gaden, er ejeren, prøver vi at bygge et hus.

/** En gade, der købes af en spiller og bebygges */

public class Gade2 extends Grund2
{
  int antalHuse;                             // antal huse og pris
  double huspris;

  public Gade2(String navn, double pris, double leje, double huspris)
  {
    super(navn, pris, leje);                 // kald Grund2's konstruktør
    this.huspris=huspris;
    antalHuse = 0;
  }

  public double beregnLeje()                 // tilsidesæt Grund2's beregnLeje()
  {
    return grundleje + antalHuse * huspris;
  }

  public void landet(Spiller sp)
  {
    super.landet(sp);                        // brug gamle landet()
    if (sp==ejer)
    {                                        // eget felt; byg hus?
      if (antalHuse<5 && sp.konto>huspris && sp.spørgsmål("købe hus for "+pris))
      {                                     // byg et hus
        ejer.transaktion( -huspris );
        antalHuse = antalHuse + 1;
        sp.besked("Du bygger hus på "+navn+" for "+huspris);
      }
    }
  }
}

Læg mærke til, at vi har sparet næsten halvdelen af koden væk i de to nye klasser.

Herunder ses klassediagrammet for de nye klasser. Da Grund2 har en spiller (ejeren), er der en pil fra Grund2 til Spiller, en har-relation. Resten af pilene symboliserer er-en-relationer, f.eks. Gade2 er en Grund2, Grund2 er et Felt.

I kapitel 22, Objektorienteret analyse og design kan du læse mere om, hvordan man designer sine klasser og hvordan man vælger hvilke er-en- og har-en-relationer, der skal være mellem klasserne.

5.6.2 Opgaver

  1. Ret SpilMatador til at bruge Rederi2 og Gade2 i stedet for Rederi og Gade. Kør programmet og følg med i, hvad der sker i Gade2's konstruktør og landet()-metode.

  2. Føj en Bryggeri-klasse til matadorspillet version 2. Husk at kalde super() i konstruktøren (hvorfor er det nødvendigt?). Du kan evt. kopiere Gade2.java i stedet for at skrive koden forfra. Hvor meget kode kan du spare?

  3. Ret på Spiller, så slag er en objektvariabel i stedet for en lokal variabel. Nu kan man udefra aflæse, hvad spilleren slog sidst. Brug værdien af slag i landet()-metoden i Bryggeri til at lade lejen afhænge af, hvad spilleren slog.

5.7 Test dig selv

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.8 Resumé

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9 Avanceret

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9.1 Initialisering uden for konstruktørerne

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9.2 Kald af en konstruktør fra en anden konstruktør

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9.3 Metoder erklæret final

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9.4 Metoder erklæret protected

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

5.9.5 Variabel-overskygning

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

1Egentlig er sætSnydeværdi() overflødig, da snydeværdi er public, men vi skal bruge den til at illustrere en pointe i næste afsnit.

2En såkaldt hashkode, der med stor sandsynlighed er unik. Denne opførsel er ikke specielt nyttig, så man definerer ofte sin egen toString()

3Man kan altid kalde equals(), men det kan godt være, at den ikke giver det forventede, hvis den ikke er defineret i klassen. ArrayList og String har defineret den, mens f.eks. StringBuffer ikke har. Da får man opførslen fra Object, der kun undersøger, om objekterne er de samme (samme sted i hukommelsen), den tager ikke højde for, om to objekter indeholder ens data.

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (82% af værket).

Ønsker du at se de sidste 18% af dette værk (199974 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.