6.2. Polymorfe variabler

Figur 6-3. Snydespil2MedPolymorfiefter 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);
    }
  }
}

Resultatet bliver:

[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 ?

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

6.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.: override) 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

6.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 Afsnit 5.5), 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.

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

Figur 6-4. 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 linje 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 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 i linje 2.

6.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:

Figur 6-5. 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