Get the Flash Player to see this player.

time2online Joomla Extensions: Simple Video Flash Player Module
Polimorfizm

Polimorfizm jest ściśle związany z dziedziczeniem i rzutowaniem w górę. W poprzednim artykule napisałem, że wszystkie obiekty mają jeden korzeń, czyli wywodzą się z tej samej klasy bazowej Object. Dzięki takiemu podejściu mamy dostęp do metod klasy Object, ale to nie jest największą zaletą. Największą zaletą jest to, że w każdym języku obiektowym występuje wiązanie dynamiczne, które pozwala na przypisanie obiektu klasy pochodnej do klasy bazowej. Pewnie nic wam to nie mówi, dlatego skorzystam z przykładu z książki "Thinking in Java", aby to wyjaśnić.

Załóżmy, że mamy klasę bazową Shape z dwoma metodami draw() i erase(). Po tej klasie dziedziczą inne klasy Circle, Square i Tringle, które także mają metody draw() i erase() tak jak to zaprezentowałem poniżej:

Teraz wykorzystamy rzutowanie w górę i zadeklarujemy obiekt s, typu koło w następujący sposób:

Shape s = new Circle();

Tworzony jest obiekt s typu Circle, po czym jego referencja jest przypisywana do wartości typu Shape, co mogłoby być błędem, ale tak nie jest, ponieważ Circle jest figurą Shape poprzez dziedziczenie. Dlatego kompilator nie zgłasza błędu. Co więc się stanie gdy wywołamy metodę draw() obiektu "s" w następujący sposób?

s.draw();

można było się spodziewać, że wywołana zostanie metoda draw() klasy Shape, ale w tym przypadku wywoła się metoda Circle.draw (czyli klasy pochodnej). Dzieję się tak dzięki późnemu wiązaniu, czyli polimorfizmowi. Jest to największa zaleta języków obiektowych. Ponieważ, gdy dobrze zaprojektujemy klasę bazową, to możemy komunikować się tylko i wyłącznie z interfejsem tej klasy. Program napisany w ten sposób jest rozszerzalny, ponieważ możliwe jest dodawanie nowej funkcjonalności poprzez dodawanie nowych typów dziedziczących po wspólnej klasie bazowej.

Problem pojawia się wtedy, gdy klasa pochodna ma być rozszerzona o dodatkowe metody. Załóżmy, że do naszej klasy Circle dopiszemy metodę paint(). Wtedy nie możemy odwołać się bezpośrednio do s.paint(), ponieważ rzutowaliśmy w górę klasę Cricle na klasę Shape. W klasie Shape nie ma takiej metody. Aby odwołać się do metody paint() należy rzutować w dół. Robi się to tak samo jak rzutowanie na typy w C. Czyli dopisując z przodu w nawiasach okrągłych typ zmiennej. Czyli w naszym przypadku będzie to wyglądało tak:

(Circle)s.paint();

Program automatycznie wykryje rzutowanie i wykona metodą, oczywiście o ile taka istnieje. Co stanie się jeśli rzutujemy na klasę gdzie nie ma zdefiniowanej naszej metody, albo pomylimy nazwę metody, tak jak na poniższym przykładzie?

(Square)s.paint();

W takim momencie kompilator wykryje, że w klasie Square nie ma metody paint() i wyświetli wyjątek ClassCastException, czyli błąd programu. Znając nazwę wyjątku możemy go przechwycić i wykonać odpowiedni program, ponieważ jak tego nie zrobimy program wywali nam błędy, a nawet może się zawiesić. O wyjątkach napiszę w następnym artykule.

Dzięki takiemu podejściu można zaprojektować całą aplikację w przejrzysty sposób, ale twórcy Javy poszli o krok dalej i stworzyli klasy abstrakcyjne oraz interfejsy.

Czym są owe klasy abstrakcyjne?

W naszym przykładzie stworzyliśmy klasę Shape jako klasę bazową, chociaż wiedzieliśmy, że nigdy nie użyjemy samej klasy Shape jako egzemplarza. Klasa Shape posłużyła nam tylko jako zbiór metod, które będą używane w klasach dziedziczonych po klasie Shape. Twórcy Javy, aby ułatwić tworzenie klas bazowych stworzyli klasę abstrakcyjną. Oczywiście można tworzyć klasę bazową jako zwykłą klasę, ale wtedy kompilator nie uchroni nas przed błędami programisty takimi jak utworzeniem instancji z klasy bazowej.

Klasę abstrakcyjną stworzono po to, żeby powiedzieć kompilatorowi, które klasy są klasami bazowymi. W takim podejściu kompilator nie pozwoli utworzyć egzemplarza tej klasy, będzie także oczekiwał, że stworzymy klasę pochodną po klasie abstrakcyjnej przy użyciu słowa extends. W klasie abstrakcyjnej można zdefiniować metody, które będą niezmienne dla wszystkich klas pochodnych tej klasy oraz zdefiniować metodę abstrakcyjną, którą będzie trzeba nadpisać w klasie pochodnej. Definiowanie klasy abstrakcyjnej wygląda następująco:

 

abstract class Shape{
public abstract void draw();
public void erase(){System.out.println("wymazywanie"); };
}

Oczywiście w samej klasie abstrakcyjnej mogą być także metody abstrakcyjne, czyli takie które trzeba nadpisać oraz zwykłe metody, które mają ciało i będą dostępne dla każdej klasy pochodnej. W naszym przypadku metoda draw() będzie musiała być nadpisana w każdej nowej klasie pochodnej i powie nam o tym kompilator, a metoda erase() będzie dostępna dla każdej klasy pochodnej bez dodatkowej implementacji.

Czym są interfejsy?

Twórcy Javy oprócz klas abstrakcyjnych w których można definiować metody w samej klasie abstrakcyjnej dodali także interfejsy. Interfejsy są podobnym tworem do klas abstrakcyjnych lecz są pozbawione całkowicie implementacji metod. Czyli w interfejsie nie można definiować ciała metody. Aby stworzyć klasę pochodną po interfejsie należy użyć słowa implements a nie extends. Interfejsy to nie tylko klasy bazowe o najwyższym stopniu abstrakcji. Interfejsy pozwalają także na realizację odmiany dziedziczenia wielobazowego, czyli tworzenia klas, które dadzą się rzutować w górę na więcej niż jeden typ bazowy.

Aby stworzyć typ wielobazowy wystarczy stworzyć jedną klasę bazową oraz tyle interfejsów ile nam potrzeba tak jak w przykładzie z książki:

 

interfece CanFight{
 void fight();
}
interfece CanSwim{
 void swim();
}
interfece CanFly{
 void fly();
}
class ActionCharacter{
 public void fight(){}
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly{
 public void swim(){}
 public void fly{}
}
public class Adventure {
 public static void t(CanFight x) {x.fight()}
 public static void u(CanSwim x) {x.swim()}
 public static void v(CanFly x) {x.fly()}
 public static void w(ActionCharacter x) {x.fight()}
 public static void main(String[] args){
 Hero h = new Hero();
 t(h); //Obiekt Hero jako CanFight
 u(h); //Obiekt Hero jako CanSwim
 v(h); //Obiekt Hero jako CanFly
 w(h); //Obiekt Hero jako ActionCharacter
 }
}

Widać, że metody fight() nie jest zdefiniowana w klasie Hero, ale jest dostarczona przez klasę bazową ActionCharacter, dlatego tworzenie obiektu typu Hero jest możliwe.