CodeGym /Kursy Java /Moduł 2: Rdzeń Java /Polimorfizm i nadpisywanie

Polimorfizm i nadpisywanie

Moduł 2: Rdzeń Java
Poziom 1 , Lekcja 6
Dostępny

– Amigo, czy lubisz wieloryby?

– Wieloryby? Nie, nigdy o nich nie słyszałem.

– No wiesz, takie jak krowy, tylko większe i pływają. Nawiasem mówiąc, wieloryby pochodzą od krów. A przynajmniej mają wspólnego przodka. Mniejsza z tym.

Polimorfizm i nadpisywanie - 1

– Słuchaj. Chciałbym Ci opowiedzieć o kolejnym, potężnym narzędziu OOP: polimorfizmie. Charakteryzuje się ono czterema cechami.

1) Nadpisywanie metod.

Wyobraź sobie, że napisałeś klasę "Cow" na potrzeby pewnej gry. Ma ona wiele części składowych i metod. Obiekty tej klasy mogą robić przeróżne rzeczy: chodzić, jeść czy spać. Jakby tego było mało, to poruszające się krowy dzwonią jeszcze dzwoneczkami. Załóżmy, że wszystko, co do najmniejszego szczegółu, już w tej klasie zostało zaimplementowane.

Polimorfizm i nadpisywanie - 2

Wtedy nagle okazuje się, że klient chce wypuścić kolejny poziom gry, tylko tym razem jej akcja ma toczyć się na morzu, a główny bohater będzie wielorybem.

Zaczynasz zatem projektować klasę Whale i zdajesz sobie sprawę, że tylko nieznacznie różni się ona od klasy Cow. Obie klasy korzystają z bardzo podobnej logiki i decydujesz się użyć dziedziczenia.

Klasa Cow jest idealnie dopasowana do klasy macierzystej: posiada już wszystkie niezbędne zmienne i metody. Jedyne, co musisz zrobić, to dodać wielorybowi umiejętność pływania. I wtedy pojawia się problem: Twój wieloryb ma nogi, rogi i dzwoneczek! Klasa Cow implementuje przecież również i te funkcjonalności. Co możesz zrobić w takiej sytuacji?

Polimorfizm i nadpisywanie - 3

Oto na ratunek przybywa nadpisywanie metod. Jeżeli dziedziczymy metodę, która nie robi dokładnie tego, czego potrzebujemy w naszej nowej klasie, to możemy zastąpić ją inną metodą.

Polimorfizm i nadpisywanie - 4

Jak to się robi? W naszej klasie pochodnej deklarujemy metodę, którą chcemy zmienić (z taką samą sygnaturą metody jak w klasie macierzystej). A następnie piszemy dla tej metody nowy kod. To wszystko. To tak, jakby dawna metoda klasy macierzystej nie istniała.

Oto, jak to działa:

Kod Opis
class Cow
{
public void printColor()
{
System.out.println("Jestem białego koloru");
}
public void printName()
{
System.out.println("Jestem krową");
}
}class Whale extends Cow
{
public void printName()
{
System.out.println("Jestem wielorybem");
}
}
Tutaj definiujemy dwie klasy: Cow i WhaleWhale dziedziczy Cow.

Klasa Whale nadpisuje metodę printName();.

public static void main(String[] args)
{
Cow cow = new Cow();
cow.printName();
}
Kod wyświetla na ekranie «Jestem krową».
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
Kod wyświetla na ekranie «Jestem wielorybem»

Następnie dziedziczy Cow i nadpisuje printName, więc klasa Whale zawierać będzie następujące dane i metody:

Kod Opis
class Whale
{
public void printColor()
{
System.out.println("Jestem białego koloru");
}
public void printName()
{
System.out.println("Jestem wielorybem");
}
}
Nie mamy żadnych informacji o dawnej metodzie.

– Szczerze mówiąc, właśnie tego się spodziewałem.

2) Ale to nie wszystko.

– Załóżmy, ze klasa Cow posiada printAll, czyli metodę, która wywołuje dwie pozostałe metody. Kod będzie wtedy działał w następujący sposób:

Na ekranie wyświetli się:
Jestem białego koloru
Jestem wielorybem

Kod Opis
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("Jestem białego koloru");
}
public void printName()
{
System.out.println("Jestem krową");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.println("Jestem wielorybem");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
Na ekranie wyświetli się:
Jestem białego koloru
Jestem wielorybem

Zauważ, że kiedy metoda printAll () klasy Cow jest wywoływana na obiekcie Whale, to zostaje użyta metoda printName() klasy Whale, a nie klasy Cow.

Liczy się nie klasa, w której metoda została napisana, tylko typ (klasa) obiektu, na którym metoda jest wywoływana.

– Rozumiem.

– Możesz dziedziczyć i nadpisywać tylko metody niestatyczne. Metody statyczne nie są dziedziczone i nie mogą być nadpisywane.

A oto, jak wygląda klasa Whale, kiedy zastosujemy dziedziczenie i nadpiszemy metody:

Kod Opis
class Whale
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("Jestem białego koloru");
}
public void printName()
{
System.out.println("Jestem wielorybem");
}
}
A oto, jak wygląda klasa Whale, kiedy zastosujemy dziedziczenie i nadpiszemy metodę. Nie mamy żadnych informacji o dawnej metodzie printName.

3) Rzutowanie typów.

Jest jeszcze jedna ciekawa kwestia. Ponieważ klasa dziedziczy wszystkie metody i dane jej klasy macierzystej, to obiektem tej klasy mogą być zmienne, do których przypisane są odniesienia do klasy macierzystej (i klasy macierzystej tej klasy macierzystej itd, aż do klasy Object). Przeanalizuj ten przykład:

Kod Opis
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printColor();
}
Na ekranie wyświetli się:
Jestem białego koloru.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printColor();
}
Na ekranie wyświetli się:
Jestem białego koloru.
public static void main(String[] args)
{
Object o = new Whale();
System.out.println(o.toString());
}
Na ekranie wyświetli się:
Whale@da435a.
Metoda toString() jest dziedziczona po klasie Object.

– Brzmi dobrze. Ale właściwie, czy to jest nam do czegoś potrzebne?

– To niezwykle przydatna funkcja. Później zrozumiesz, że jest ona naprawdę bardzo, bardzo ważna.

4) Późne wiązanie (dynamiczne przydzielanie).

Oto, jak to wygląda:

Kod Opis
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
Na ekranie wyświetli się:
Jestem wielorybem.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printName();
}
Na ekranie wyświetli się:
Jestem wielorybem.

Zauważ, że to nie typ zmiennej determinuje, którą konkretną metodę printName wywołamy (tę z klasy Cow czy klasy Whale), ale raczej typ obiektu, do którego odnosi się ta zmienna.

Zmienna Cow przechowuje referencję do obiektu Whale, zostanie zatem wywołana metoda printName zdefiniowana w klasie Whale.

– Cóż, to tłumaczenie nie sprawia, że jest to bardziej zrozumiałe.

– Zgadzam się, to wcale nie jest takie oczywiste. Pamiętaj o jednej ważnej zasadzie.

Zestaw metod, które wywołujesz na zmiennej, jest determinowany przez typ zmiennej. Ale to, która konkretna metoda/implementacja zostanie wywołana, jest determinowane przez typ/klasę obiektu, do którego odnosi się zmienna.

– Spróbuję zapamiętać.

– Będziesz się ciągle z tym spotykał, więc szybko to zapamiętasz i zrozumiesz.

5) Rzutowanie typów.

Rzutowanie w odniesieniu do typów referencji działa inaczej, niż do typów prostych. A jednak, konwersji rozszerzającej i zawężającej można używać także w stosunku do typów referencji. Przeanalizuj ten przykład:

Konwersja rozszerzająca Opis
Cow cow = new Whale();

Klasyczna konwersja rozszerzająca. Teraz możesz wywoływać na obiekcie Whale tylko metody zdefiniowane w klasie Cow.

Kompilator pozwoli Ci użyć zmiennej cow tylko do wywołania metod zdefiniowanych przez typ Cow.

Konwersja zawężająca Opis
Cow cow = new Whale();
if (cow instanceof Whale)
{
Whale whale = (Whale) cow;
}
Klasyczna konwersja zawężająca ze sprawdzeniem typu. Zmienna cow typu Cow przechowuje referencję do obiektu Whale.
Sprawdzamy, czy tak właśnie jest, a następnie przeprowadzamy (rozszerzającą) konwersję typu. Nazywa się to także rzutowaniem typu.
Cow cow = new Cow();
Whale whale = (Whale) cow; //wyjątek
Możesz także przeprowadzić konwersję zawężająca typu referencji bez sprawdzania typu danego obiektu.
W tym przypadku, jeśli zmienna cow wskazuje na coś innego niż obiekt Whale, zostanie wyrzucony wyjątek (InvalidClassCastException).

6) A teraz będzie coś fajnego. Wywoływanie pierwotnej metody.

Czasami, podczas nadpisywania dziedziczonej metody, nie chcesz jej zastąpić całkowicie. Czasem chcesz po prostu coś do niej dodać.

W tym przypadku potrzebujesz, aby kod nowej metody wywoływał tę samą metodę, ale na klasie bazowej. I w Javie możesz tak zrobić. Używasz do tego: super.method().

Oto kilka przykładów:

Kod Opis
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("Jestem białego koloru");
}
public void printName()
{
System.out.println("Jestem krową");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.print("To nieprawda: ");
super.printName();

System.out.println("Jestem wielorybem");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
Na ekranie wyświetli się:
Jestem białego koloru
To nieprawda: Jestem krową
Jestem wielorybem

– Hmm. To dopiero była lekcja! Moje robocie uszy prawie się stopiły.

– Tak, to nie jest prosty temat. To jedno z najtrudniejszych zagadnień, jakie spotkasz. Profesor zadeklarował, że da Ci linki do materiałów innych autorów, więc jeśli nadal czegoś nie rozumiesz, możesz to nadrobić.

Komentarze (2)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
l.jargielo Poziom 18, Poland, Poland
14 stycznia 2023
Klasyczna konwersja zawężająca ze sprawdzeniem typu. Zmienna cow typu Cow przechowuje referencję do obiektu Whale. Sprawdzamy, czy tak właśnie jest, a następnie przeprowadzamy (rozszerzającą) konwersję typu. Nazywa się to także rzutowaniem typu. (rozszerzającą) <- is this mistake? Shouldn't it be "zawężająca" here?
TheVirus Poziom 22
21 sierpnia 2023
Chyba po prostu ktoś się tu pogubił w tłumaczeniu z angielskiego. Powinno się raczej stosować pojęcia "Rzutowanie w górę" i "Rzutowanie w dół". Rzutowanie w górę - konwersja klasy pochodnej do bazowej (tutaj - konwersja rozszerzająca) Rzutowanie w doł - konwersja klasy bazowej do pochodnej (tutaj - konwersja zwężająca)