5 Nedarvning

Indhold:



Kapitlet forudsættes af 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 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.print("[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 underklassen 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 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 og underklasse. Her er et udpluk:


Superklasse kaldes også: Baseklasse, basisklasse, forældreklasse, stamklasse.

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


I vores eksempel er superklassen Terning og underklassen FalskTerning1.


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


public class Snydespil1
{
  public static void main(String[] args)
  {
    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!");
    }
  }
}

[kast() på FalskTerning1] t1: 1
t2: 3
[kast() på FalskTerning1] t1=1  t2=5
[kast() på FalskTerning1] t1=1  t2=3
[kast() på FalskTerning1] t1=4  t2=3
[kast() på FalskTerning1] t1=6  t2=6
To ens!
[kast() på FalskTerning1] t1=2  t2=6


Vi kan altså bruge FalskTerning1-objekter på præcis samme måde som Terning-objekter.

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.print("[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[] args)
  {
    FalskTerning2 t1 = new FalskTerning2();
    t1.sætSnydeværdi(4);

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

[kast() på FalskTerning2] [kast() på FalskTerning2] t1=4
[kast() på FalskTerning2] t1=4
[kast() på FalskTerning2] t1=6
[kast() på FalskTerning2] t1=6
[kast() på FalskTerning2] t1=4


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
{
  // tilsidesæt kast med en "bedre" udgave
  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.2 Polymorfe variabler


Snydespil2MedPolymorfi
efter punkt A

Se på følgende eksempel:


public class Snydespil2medPolymorfi
{
  public static void main(String[] args)
  {
    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);
    }
  }
}

[kast() på FalskTerning2] [kast() på FalskTerning2] t=4
[kast() på FalskTerning2] t=6
[kast() på FalskTerning2] 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) .


Jamen, 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. Men 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. Variablen bruger den bare ikke.


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 t1 refererer til et FalskTerning2-objekt, kan man kun bruge t-variablen til at kalde metoder eller anvende 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 udføre Ternings kast() ).


Variablens type bestemmer, hvilke metoder man kan kalde på objektet, og hvilke objektvariabler man kan læse og ændre
Objektets type bestemmer, hvilken metode-definition der bliver udført

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


For at tilsidesætte en metode skal man i underklassen lave en eksakt kopi af metode-hovedet fra superklassen

5.2.3 Et eksempel på polymorfi: Brug af Raflebaeger

public class SnydeMedBaeger
{
  public static void main(String[] args)
  {
    Raflebaeger bæger = new Raflebaeger(0);

    FalskTerning2 ft = new FalskTerning2();
    ft.sætSnydeværdi(6);

    bæger.tilføj(ft);   // tilføj() tager et objekt af typen Terning,
                        // og dermed også af typen FalskTerning2.

    Terning t = new Terning();
    bæger.tilføj(t);

    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"-vektoren:

  public void ryst()
  {
    int i;
    for (i=0;i<terninger.size();i++) 
    {
      Terning t;
      t=(Terning) terninger.elementAt(i);
      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 at den kan bruges sammen med 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?


Efter punkt A
(programmet vil ikke oversætte)


Svaret er: Nej!


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

    t = new Terning();
    ft = t;                // sprogfejl
                           // punkt A
    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.


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

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


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

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 kan vi bruge explicit 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.


Når vi læser objekter i en vektor, er det faktisk det, der sker.

I Raflebaeger's ryst()-metode skrev vi:


  Terning t;
  t = (Terning) terninger.elementAt(i);
  t.kast();


I en vektor kan gemmes alle typer objekter. For at kunne lægge noget fra en vektor ned i en Terning-variabel er det derfor nødvendigt at lave en reference-typekonvertering til Terning. Dette går fint, så længe man har stoppet Terning- eller FalskTerning2-objekter i vektoren, men man kan jo putte hvad som helst i en vektor...


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;
  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;

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

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

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 passeret()-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
  {
    System.out.println(sp.navn+" lander på helle, og får overført "+gevinst);
    sp.transaktion(gevinst);        // opdater spillers konto
    System.out.println(sp.navn+"s konto lyder nu på "+sp.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

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
  {
    System.out.println(sp.navn+" passerer start og modtager "+gevinst);
    sp.transaktion(gevinst);                       // kredit/debit af konto
    System.out.println(sp.navn+"s konto lyder nu på "+sp.konto);
  }

  public void landet(Spiller sp)                   // tilsidesæt metode i Felt
  {
    System.out.println(sp.navn+" lander på start og modtager "+gevinst);
    sp.transaktion(gevinst);
    System.out.println(sp.navn+"s konto lyder nu på "+sp.konto);
  }
}



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-en-relation til klassen Spiller), en pris og en leje for at lande på grunden.


// Rederier

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)
  {
    System.out.println(sp.navn+" er landet på "+navn);
    if (sp==ejer)
    {                                       // spiller ejer selv grunden
      System.out.println("Dette er "+sp.navn+"s egen grund");
    }
    else if (ejer==null)
    {                                       // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        System.out.println(sp.navn+" køber "+navn+" for "+pris);
        ejer=sp;
        sp.transaktion( -pris );
      }
      else System.out.println(sp.navn+" har ikke penge nok til at købe "+navn);
    }
    else
    {                                       // feltet ejes af anden spiller
      System.out.println("Husleje: "+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 bebygges

public class Gade extends Felt
{
  Spiller ejer;
  double pris;
  double grundleje;
  int antalHuse;
  double huspris;

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

  public void landet(Spiller sp)
  {
    System.out.println(sp.navn+" er landet på "+navn);

    if (sp==ejer)
    {                                        // eget felt
      System.out.println("Dette er "+sp.navn+"s egen grund");
      if (antalHuse<5 && sp.konto>huspris)
      {                                     // byg et hus
        System.out.println(ejer.navn+" bygger et 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)
      {
        System.out.println(sp.navn+" køber "+navn+" for "+pris);
        ejer=sp;
        sp.transaktion( -pris );
      }
      else System.out.println(sp.navn+" har ikke penge nok til at købe "+navn);
    }
    else
    {                                        // felt ejes af anden spiller
      double leje = grundleje + antalHuse * huspris;
      System.out.println("Husleje: "+leje);
      sp.betal(ejer, leje);                 // spiller betaler til ejeren
    }
  }
}


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


// Matadorspil for to spillere
import java.util.*;

public class SpilMatador
{
  public static void main(String[] args)
  {
    Spiller sp1=new Spiller("Søren",50000);   // opret spiller 1
    Spiller sp2=new Spiller("Gitte",50000);   // opret spiller 2

    Vector felter=new Vector();               // indeholder alle felter
    felter.addElement(new Start(5000));
    felter.addElement(new Gade("Gade 1",10000, 400,1000));
    felter.addElement(new Gade("Gade 2",10000, 400,1000));
    felter.addElement(new Gade("Gade 3",12000, 500,1200));
    felter.addElement(new Rederi("Maersk",17000,4200));
    felter.addElement(new Gade("Gade 5",15000, 700,1500));
    felter.addElement(new Helle(15000));
    felter.addElement(new Gade("Gade 7",20000,1100,2000));
    felter.addElement(new Gade("Gade 8",20000,1100,2000));
    felter.addElement(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 ved at hente objekterne i vektoren, reference-typekonvertere dem til Felt og kalde objekternes passeret()-metode og landet()-metoden på det sidste objekt.

Denne tur()-metode placerer vi i klassen Spiller sammen med oplysningerne om spilleren.


// Definition af en spiller

import java.util.*;

public class Spiller
{
  String navn;
  double konto;
  int feltnr;

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

  public void transaktion(double kr)
  {
    konto = 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(Vector felter)
  {
    int slag=(int)(Math.random()*6)+1;                // terningkast
    System.out.println("***** "+navn+" på felt "+feltnr+" slår "+slag+" *****");

    // nu rykkes der
    for (int i=1;i<=slag;i=i+1)
    {
      // 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 = (Felt) felter.elementAt(feltnr);
      if (i<slag) felt.passeret(this);  // kald passer() på felter vi passerer
      else felt.landet(this);           // kald land() på sidste felt
    }
    try {Thread.sleep(3000);} catch (Exception e) {}  // vent 3 sek.
  }
}


Fidusen er, at denne tur()-metode kan skrives uafhængigt af, hvilke felt-typer der findes: tur()-metoden kalder automatisk de rigtige landet()- og passeret()-metoder, selvom den kun kender Felt-klassen.


Bemærk i øvrigt, hvordan vi med this overfører en reference til spilleren selv når vi kalder passeret() og landet() på Felt-objekterne.


Linien

    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 kapitlet om 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 kapitel 2).


Her ses uddata af en kørsel af programmet:

***** Søren på felt 0 slår 3 *****
Søren passerer Gade 1
Søren passerer Gade 2
Søren er landet på Gade 3
Søren køber Gade 3 for 12000.0
***** Gitte på felt 0 slår 5 *****
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte passerer Maersk
Gitte er landet på Gade 5
Gitte køber Gade 5 for 15000.0
***** Søren på felt 3 slår 2 *****
Søren passerer Maersk
Søren er landet på Gade 5
Husleje: 700.0
Søren betaler Gitte: 700.0 kr.
***** Gitte på felt 5 slår 4 *****
Gitte passerer Helle
Gitte passerer Gade 7
Gitte passerer Gade 8
Gitte er landet på Gade 9
Gitte køber Gade 9 for 30000.0
***** Søren på felt 5 slår 1 *****
Søren er landet på helle, og får overført 15000.0
Sørens konto lyder nu på 52300.0
***** Gitte på felt 9 slår 5 *****
Gitte har passeret start og modtager 5000.0
Gittes konto lyder nu på 10700.0
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte er landet på Maersk
Gitte har ikke penge nok til at købe Maersk
***** Søren på felt 6 slår 1 *****
Søren er landet på Gade 7
Søren køber Gade 7 for 20000.0
***** Gitte på felt 4 slår 1 *****
Gitte bygger et hus på Gade 5 for 1500.0
Gitte er landet på Gade 5
Dette er Gittes egen grund
***** Søren på felt 7 slår 1 *****
Søren er landet på Gade 8
Søren køber Gade 8 for 20000.0
***** Gitte på felt 5 slår 4 *****
Gitte passerer Helle
Gitte passerer Gade 7
Gitte passerer Gade 8
Gitte bygger et hus på Gade 9 for 2200.0
Gitte er landet på Gade 9
Dette er Gittes egen grund
***** Søren på felt 8 slår 2 *****
Søren passerer Gade 9
Søren er landet på start og modtager 5000.0
Sørens konto lyder nu på 17300.0
***** Gitte på felt 9 slår 1 *****
Gitte er landet på start og modtager 5000.0
Gittes konto lyder nu på 12000.0
***** Søren på felt 0 slår 3 *****
Søren passerer Gade 1
Søren passerer Gade 2
Søren bygger et hus på Gade 3 for 1200.0
Søren er landet på Gade 3
Dette er Sørens egen grund
***** Gitte på felt 0 slår 5 *****
Gitte passerer Gade 1
Gitte passerer Gade 2
Gitte passerer Gade 3
Gitte passerer Maersk
Gitte bygger et hus på Gade 5 for 1500.0
Gitte er landet på Gade 5
Dette er Gittes egen grund
***** Søren på felt 3 slår 5 *****
Søren passerer Maersk
Søren passerer Gade 5
Søren passerer Helle
Søren passerer Gade 7
Søren bygger et hus på Gade 8 for 2000.0
Søren er landet på Gade 8
Dette er Sørens egen grund
***** Gitte på felt 5 slår 1 *****
Gitte er landet på helle, og får overført 15000.0
Gittes konto lyder nu på 25500.0
***** Søren på felt 8 slår 3 *****
Søren passerer Gade 9
Søren har passeret start og modtager 5000.0
Sørens konto lyder nu på 19100.0
Søren er landet på Gade 1
Søren køber Gade 1 for 10000.0


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


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.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 tal3, 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 vektorer indeholder de samme elementer4.

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 referencetypekonvertering:

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


Her var jeg heldig, at o faktisk refererede til en (underklasse) af Terning. Jeg får først at vide, om min typekonvertering 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 Vector benytter sig af dette: Vektorer arbejder med lister af typen Object, det er derfor, man kan gemme alle slags objekter i dem.


  vector v = new Vector();
  Point p = new Point();
  v.addElement(p);


Metoden addElement() tager et objekt 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):

  vector v = new Vector();
  Point p = new Point();
  Object o;                  // overflødig mellemvariabel
  o = p;                     // implicit reference-typekonvertering
  v.addElement(o);


Når man kalder elementAt() 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) v.elementAt(0);


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

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



5.5 Konstruktører i underklasser

Vi minder om, at:



En underklasse skal selv definere, hvordan dets objekter skal kunne oprettes, så den skal selv definere sine konstruktører. Underklassen kan også 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.


Boks3medDensitet tillader oprettelse på to måder


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(); overflødig, den kaldes implicit
    densitet = 10.0;
  }

  public Boks3medDensitet(double lgd, double b,  
          double h, double densitet)
  {
    // kald superklassens konstruktør
    super(lgd,b,h);
    this.densitet = densitet;
  }

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


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

Disse 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
{

}


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

public class B extends A
{
  public B() // 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".


Der er derimod ingen problemer med:

public class A
{
}


public class B extends A
{
}


Java laver det om til:

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


public class B extends A
{
  public B() // 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)
  {
    System.out.println(sp.navn+" er landet på "+navn);
    if (sp==ejer)
    {                                       // spiller ejer feltet
      System.out.println("Dette er "+sp.navn+"s egen grund");
    }
    else if (ejer==null)
    {                                       // ingen ejer grunden, så køb den
      if (sp.konto > pris)
      {
        System.out.println(sp.navn+" køber "+navn+" for "+pris);
        ejer=sp;
        sp.transaktion( -pris );
      }
      else System.out.println(sp.navn+" har ikke penge nok til at købe "+navn);
    }
    else
    {                                       // felt ejes af anden spiller
      double leje = beregnLeje();
      System.out.println("Husleje: "+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 Grund. Vi skal blot definere konstruktøren, som skal kalde den tilsvarende konstruktør i Grund:

// Rederier

public class Rederi2 extends Grund2 
{
  public Rederi2(String navn, double pris, double leje)
  {
    super(navn, pris, leje);    // kald 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, hvis spilleren, der er landet på gaden, ikke er ejeren. Hvis det er ejeren, prøver vi at bygge et hus (udskilt i metoden bygHus()).


// En gade der kan 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);
    this.huspris=huspris;
    antalHuse = 0;
  }

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

  public void landet(Spiller sp)
  {
    if (sp==ejer)
    {                                                 // eget felt; byg hus
      System.out.println(sp.navn+" er landet på "+navn);
      System.out.println("Dette er "+sp.navn+"s egen grund");
      if (antalHuse<5 && sp.konto>huspris) bygHus();  // byg hus hvis vi kan
    }
    else super.landet(sp);                            // brug gamle landet()
  }

  public void bygHus()
  {
    System.out.println(ejer.navn+" bygger et hus på "+navn+" for "+huspris);
    ejer.transaktion( -huspris );
    antalHuse = antalHuse + 1;
  }
}


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-en-relation. Resten af pilene symboliserer er-en-relationer, f.eks. Gade2 er en Grund2, Grund2 er et Felt.



5.7 Test dig selv (fjernet)

Dette afsnit findes i den trykte bog


5.8 Resumé (fjernet)

Dette afsnit findes i den trykte bog


5.9 Opgaver

  1. Lav en LudoTerning, der arver fra Terning. Tilsidesæt toString() med en, der giver "*" på en 3er og "globus" på en 4er. Afprøv klassen.

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

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

  4. 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?

  5. 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.10 Avanceret (fjernet)

Dette afsnit findes i den trykte bog


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

2Det er ikke det samme som overwrite, skønt mange programmører tror det og kalder det at "overskrive" en metode.

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

4Man kan altid kalde equals(), men det kan godt være, at den ikke giver det forventede, hvis den ikke er defineret i klassen. Vector 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.


Jacob Nordfalk - Objektorienteret programmering i Java - http://javabog.dk