Programowanie średniozaawansowane #2: polimorfizm podstawy i słowo this

Zagadnienie, które dziś poznasz to polimorfizm. Po pierwszym wymówieniu tego słowa pewnie zastanawiasz się kto wpadł na taki pomysł, aby używać w programowaniu słowa, którego znaczenia nikt nie zna. Podejrzewam też, że nigdy wcześniej nie udało Ci się natknąć na to określenie. Opowiem Ci, że ja byłem chyba wyjątkiem w tej kwestii, bo będąc młodym chłopakiem, znałem polimorfizm jako zaklęcie w grze Baldur’s Gate*. Rezultatem jego rzucenia na własnego bohatera było przemienienie postaci w jakąś formę zwierzęcia (w zależności od doświadczenia, było ono większe i groźniejsze). Pomimo wykorzystania tu rzadko spotykanego słowo, w pełni ono oddawało cel zaklęcia. Było nim przecież przekształcenie jednej formy na inną. Tym jest właśnie polimorfizm. Słownik języka polskiego tłumaczy je, jako: ‘występowanie w obrębie tego samego gatunku różnych postaci osobników’. Ta definicja także świetnie odzwierciedla jak działa polimorfizm w Javie.

public class Parent {
  private String name;
  
  public Parent(String name) {
    this.name = name;
  }

  public void showName() {
    System.out.println("Rodzic nazywa się " + name);
  }
}

Jest to zwykła klasa z  jednym polem, konstruktorem parametrowym i jedną metodą, która wyświetla wartość pola. Jedyna istotna kwestia, o którą możesz się zapytać jest słowo kluczowe thisJest to wskaźnik na zawartość klasy, w której aktualnie jesteś. Oznacza to, że, gdy wystukasz w metodzie lub konstruktorze this to po kropce Twoje IDE (np. Eclipse) powinno podpowiedzieć Ci wszystkie pola i metody, jakie napisano w Twoje klasie. Oczywiście możesz się do nich odwołać bez tego słowa, po prostu wpisując odpowiednią nazwę. Problem pojawia się, gdy masz konflikt nazw. Np. w Javie w konstruktorach przyjęto, że nazwa parametru powinna być taka sama, jak nazwa pola. Tworzy to konflikt nazw (skąd JVM ma wiedzieć, o który ‘name’ Ci chodzi?). Wtedy możesz użyć this żeby wskazać kompilatorowi, że odwołujesz się do pola. To jest w zasadzie jedna z nielicznych sytuacji, gdy słowo this będziesz stosował, także nie zaprzątaj sobie na razie nim głowy.

public class ChildOne extends Parent{
  private String name;

  public ChildOne(String parentName, String name) {
    super(parentName);
    this.name = name;
  }

  public void showName() {
    System.out.println("Dziecko nazywa się " + name);
  }
}

Kolejna klasa, którą widzisz powyżej, zostaje rozszerzona o rodzica. Co więcej w konstruktorze za pomocą słowa super wywołujesz konstruktor klasy Parent. Poza tym nic specjalnego w tej klasie się nie dzieje.

public class ChildTwo extends Parent{
  private String name;

  public ChildTwo(String parentName, String name) {
    super(parentName);
    this.name = name;
  }

  public void showParentName() {
    super.showName();
  }
  
  public void showName() {
    System.out.println("Dziecko nazywa się " + name);
  }
}

Ta klasa jest bliźniaczo podobna do pierwszego ‘dziecka’, ale posiada dodatkową metodę o nazwie showParentName(), w której wywołujesz znów za pomocą słowa super metodę showName() z klasy rodzica.

Wywołam teraz powyższe klasy za pomocą metody main().

public class Polimorphism {

  public static void main(String args[]) {
    Parent father = new Parent("Adam");
    ChildOne doughter = new ChildOne("Adam", "Ewa");
    ChildTwo son = new ChildTwo("Adam", "Marek");
    
    doughter.showName();
    son.showName();
    son.showParentName();
    
    Parent doughterFromFather = new ChildOne("Adam", "Ewa");
    doughterFromFather.showName();
    
    Parent sonFromFather = new ChildTwo("Adam", "Marek");
    sonFromFather.showName();
    
    if (father instanceof Parent) {
      father.showName();
    } 
    
    ChildOne notAllowed = (ChildOne) new Parent("Adam"); 
    notAllowed.showName();
  }
}

W pierwszych trzech linijkach metody main tworzone są obiekty rodzica: Adama, oraz dzieci: Ewa i Marek.  Następnie sprawdzane są imiona dzieci, oraz imię rodzica. Do tego miejsca wyniki nie powinny być dla Ciebie zaskoczeniem. W kolejnych liniach widać jak tworzone są obiekty dzieci za pomocą zadeklarowanego obiektu rodzica. W jaki sposób to możliwe?  Dzieje się tak dlatego, że każdy z tych obiektów zawiera w sobie (poprzez dziedziczenie) wszystkie publiczne lub chronione właściwości tej klasy. W takim przypadku nie ma problemu, żeby rodzic “tworzył” dziecko. Przyznasz, że to logiczne? Pojawia się jednak pytanie, którą metodę showName() używa taki obiekt? Tą z rodzica, czy tą z dziecka (zauważ, że sygnatura jest ta sama)? Aby ułatwić Ci odpowiedź, spróbuj wpisać w Twoim środowisku programistycznym zmienną sonFromFather oraz son i nacisnąć kropkę (IDE podpowie Ci z jakich metod możesz skorzystać). Okaże się wtedy, że “syn od ojca” nie ma możliwości wywołania “swoich” metod, może skorzystać tylko z metod, które posiada ojciec (spójrz na obrazek poniżej).

Ostatnie dwa ciekawe fragmenty to warunek if, który sprawdza jakiej instancji jest obiekt. Takie pomocnicze zapytanie obiektu “kim jestes?” jest bardzo przydatne, gdy kod używający polimorfizmu jest naprawdę skomplikowany i nie wiesz, jaki właściwie obiekt teraz używasz. Tutaj warto podkreślić największą wadę polimorfizmu: łatwo się w nim pogubić. Dlatego korzystaj z niego z rozwagą i staraj się, gdzie tylko możesz sprawdzać obiekty, które korzystają z dziedziczenia za pomocą metody instanceOf.

Istnieje też możliwość wymuszenia korzystania z instancji (czyli z formy) konkretnej klasy użytej w dziedziczeniu (pamiętaj, że może być więcej niż jedna. Tzn. dziecko może mieć rodzica, a ten dziadka, itp.) Tzw. rzutowanie klas (ang. casting) odbywa się w przedostatniej linijce. Wymusiłem tam za pomocą nawiasów okrągłych, że pomimo, iż tworzę obiekt rodzica, to chcę aby zachowywał się jak dziecko. Ponownie jak wpiszesz nazwę zmiennej i naciśniesz kropkę zobaczysz jakie metody są dostępne do użycia.

Okazuje się, że teraz możesz używać metod, które są dostępne u syna. Pytanie tylko, czy to ma sens, aby rodzic rzutowany na syna powinien używać metody syna (generalnie przecież rodzic nie posiada cech swojego dziecka, to dziecko posiada cechy rodzica). Spróbuj teraz odpalić powyższy kod. Rezultat z konsoli powinien być taki:

Exception in thread "main" Dziecko nazywa się Ewa
Dziecko nazywa się Marek
Rodzic nazywa się Adam
Dziecko nazywa się Ewa
Dziecko nazywa się Marek
Rodzic nazywa się Adam
java.lang.ClassCastException: advanced.polymorph.Parent cannot be cast to advanced.polymorph.ChildOne
  at advanced.polymorph.Polimorphism.main(Polimorphism.java:24)

Wyskoczył wyjątek (czym jest wyjątek dowiesz się w kolejnych lekcjach) w miejscu, gdzie próbowałeś użyć metodę showName() dostępną dla potomka. Wyjątek posiada, krótki opis co jest nie tak. Kompilator podpowiada Ci, że rodzic nie może korzystać w takich sposób z tej metody. Nie przejmuj się w jakiej kolejności wiadomość się pojawiła. Strumień System.out nie jest synchronizowany, co oznacza, że czasami wyświetlane przez niego informacje mogą być nie po kolei.

Niech to będzie dla Ciebie przestrogą. Polimorfizm i dziedziczenie umożliwiają unikanie duplikacji kodu, z racji, że możesz tworzyć różne “hybrydy” zaprogramowanych przez Ciebie obiektów (ich różne formy). Jednak z drugiej strony łatwo jest się w tym pogubić i często możesz popełnić błąd, taki jak powyższy.

 

* Pod tym linkiem możesz znaleźć jego pełny opis, zgodnie z zasadami Dungeons and Dragons: https://roll20.net/compendium/dnd5e/Polymorph#content

 

Dodaj komentarz