Általános szoftver ismeretek
  A polimorfizmusról általában

Dupla polimorfizmus (double dispatch)

Dinamikus polimorfizmus + függvénytúlterhelés



A kívánt hatást – azt, hogy nem egy, hanem két objektum aktuális típusa határozza meg, mi is fog ténylegesen történni, melyik metódus fog ténylegesen felhívódni – a dinamikus és a statikus polimorfizmus (konkrétan a függvénytúlterhelés) ügyes elegyítésével oldjuk meg.

A dupla polimorfizmusról szóló bevezetőben láttuk, hogy a cél a kliens függvény egyszerűségének fenntartása:

  static void Összecsapás(Szereplő Egyik, Szereplő Másik){
     Egyik.Harcol(Másik);
  }

tehát hogy ennek a függvénynek ne kelljen arról tudni, milyen a konrét, aktuális típusa a paramétereinek, amikor őt felhívják. Más szóval: ne kelljen neki tudnia, hogy Ő mindössze csak annyit mond az egyiknek, hogy harcoljon a másikkal és rájuk bízza, miként teszik ezt, mindkettejük típusának függvényében.

Az egyszerűség kedvéért most csak kétféle szereplőt definiálunk: óriást és törpét. Minden szereplőnek az absztrakt Szereplő osztály leszármazottjának kell lennie. Ebben az ős-osztályban három Harcol metódust kell deklarálnunk:

  abstract class Szereplő{
    abstract void Harcol(Szereplő Ellenfél);
    abstract void Harcol(Óriás    Ellenfél);
    abstract void Harcol(Törpe    Ellenfél);
  }

Világos, hogy az elsőre szükség van, ezt hívja fel az Összecsapás kliens függvény. Hamarosan látni fogjuk, hogy miért kell deklarálni a másik kettőt is.

Az óriás így valósítja meg a metódusokat:

  class Óriás extends Szereplő{
    void Harcol(Szereplő Ellenfél){
      System.out.println("Óriás vagyok, Szereplő az ellenfél");
      Ellenfél.Harcol(this); //????
    }
    
    void Harcol(Óriás Ellenfél){
      System.out.println("Óriás vagyok, Óriás az ellenfél");
    }
    
    void Harcol(Törpe Ellenfél){
      System.out.println("Óriás vagyok, Törpe az ellenfél");
    }
  }

a törpe pedig így:

  class Törpe extends Szereplő{
    void Harcol(Szereplő Ellenfél){
      System.out.println("Törpe vagyok, Szereplő az ellenfél");
      Ellenfél.Harcol(this); //????
    }
  
    void Harcol(Óriás Ellenfél){
      System.out.println("Törpe vagyok, Óriás az ellenfél");
    }
  
    void Harcol(Törpe Ellenfél){
      System.out.println("Törpe vagyok, Törpe az ellenfél");
    }
  }

A megoldás kulcsa az általános Szereplő Ellenfél paraméterű metódusában lévő, első ránézésre nagyon rejtélyesnek tűnő sor:

  Ellenfél.Harcol(this); //????

Első gondolatunk az lehet, hogy itt mintha rekurzív függvényhívásról lenne szó, hiszen a Harcol metódusban felhívjuk az Ellenfél paraméter-objektum Harcol metódusát. Azt hihetnénk, hogy az pedig ugyanezt fogja tenni, azaz visszahívja a kezdeményező metódust és aztán az egész a végtelenségig folytatódik – pontosabban addig, amíg el nem fogy a memória.

Valójában itt már egy konkrét osztály metódusának belsejében vagyunk, ami azért fontos, mert már

fordítás közben

is ismert az osztály típusa. Ha például az Összecsapás függvény első paramétere Óriás típusú, akkor az Óriás osztály Harcol metódusában vagyunk, melynek paramétere Szereplő. A fordítóprogram megpróbálja kitalálni, hogy ennek a sornak a fordításakor:

  Ellenfél.Harcol(this);

az Ellenfél paraméter-objektumnak melyik metódusát hívja fel, hiszen három is van neki:

  abstract class Szereplő{
    abstract void Harcol(Szereplő Ellenfél);
    abstract void Harcol(Óriás    Ellenfél);
    abstract void Harcol(Törpe    Ellenfél);
  }

Ezek közül nyilván az Óriás paraméterűt fogja tehát kiválasztani, hiszen this típusa Óriás. Azaz már fordításkor eldől, hogy melyik virtuális függvény fog felhívódni, amit most egy megjegyzéssel ki is hangsúlyozunk:

  class Óriás extends Szereplő{
    void Harcol(Szereplő Ellenfél){
      System.out.println("Óriás vagyok, Szereplő az ellenfél");
      Ellenfél.Harcol(this); // Szereplő::Harcol(Óriás Ellenfél) fog felhívódni
    }
    ...
  }

  class Törpe extends Szereplő{
    void Harcol(Szereplő Ellenfél){
      System.out.println("Törpe vagyok, Szereplő az ellenfél");
      Ellenfél.Harcol(this); // Szereplő::Harcol(Törpe Ellenfél) fog felhívódni
    }
    ...
  }

Tehát az ellenfél pontos típusát még persze nem tudja fordítás közben a fordítóprogram, de azt már igen, hogy melyik metódusát fogja felhívni:


És most már láthatjuk, miért kellett az absztrakt alaposztályban is deklarálni a különböző paraméterű metódusokat: azért, hogy a fordítóprogram ezen a ponton ki tudja választani a megfelelőt.

Próbáljuk ki a gyakorlatban a módszerünket:

  Szereplő ó = new Óriás();
  Szereplő t = new Törpe();

  Összecsapás(ó,t);
  System.out.println("-----------------------");
  Összecsapás(t,ó);

A program kimenete ez lesz:

  Óriás vagyok, Szereplő az ellenfél
  Törpe vagyok, Óriás az ellenfél
  -----------------------
  Törpe vagyok, Szereplő az ellenfél
  Óriás vagyok, Törpe az ellenfél

Valóban működik tehát a dupla polimorfizmusnak a dinamikus-statikus keveréken alapuló változata. A harcoló felek tisztában vannak vele, milyen fajta ellenféllel állnak szemben. (És egyes esetekben hamarosan háttal...)

Párbajkódex

A dupla polimorfizmusnak a függvénytúlterhelésen alapuló megoldását talán egy hasonlattal lehet legjobban jellemezni. Láttuk, hogy a lényeg a következő sorban van elrejtve:

  class Óriás extends Szereplő{
    void Harcol(Szereplő Ellenfél){
      Ellenfél.Harcol(this);
    }

Az óriás harcra hívja az ellenfelet (akiről még nem tudja pontosan, mifajta). A polimorfiai párbajkódex azt írja elő, hogy amikor inzultusra kerül sor, akkor az ellenfélnek (legyen az bárki) át kell adnunk a névjegyünket, hogy ő majd el tudja küldeni hozzánk a segédeit.

A this referencia az óriás névjegye, rajta van a neve, címe – az ellenfél már pontosan tudni fogja, kivel áll szemben. És kezdődhet a harc!


Rejtvények
1.

Mi történik vajon, ha az absztrakt alaposztályt megváltoztatjuk, elhagyunk belőle két metódust:

  abstract class Szereplő{
    abstract void Harcol(Szereplő Ellenfél);
  }

  1. Nem fordul le a program, mert szintaktikai hiba van benne, mégpedig: ......
  2. Lefordul és változatlanul jól működik.
  3. Lefordul és rosszul működik, mégpedig: ......
A megoldás itt olvasható el.

2.

Mi történik vajon, ha az absztrakt alaposztályt megváltoztatjuk, elhagyjuk belőle a Szereplő paraméterű metódust:

  abstract class Szereplő{
    abstract void Harcol(Óriás Ellenfél);
    abstract void Harcol(Törpe Ellenfél);
  }

  1. Nem fordul le a program, mert szintaktikai hiba van benne, mégpedig: ......
  2. Lefordul és változatlanul jól működik.
  3. Lefordul és rosszul működik, mégpedig: ......
A megoldás itt látható.


A polimorfizmusról általában
  Általános szoftver ismeretek