Menu

Polimorfizm w Javie: jeden interfejs, wiele form

Jak polimorfizm w Javie pozwala jednej zmiennej wskazywać na obiekty wielu typów, dlaczego nadpisane metody są wybierane w czasie działania i jak bezpiecznie używać upcastingu, downcastingu i instanceof.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Jedna referencja, wiele typów

Polimorfizm to nagroda za dziedziczenie i interfejsy. Oznacza, że jedna zmienna zadeklarowana z typem nadrzędnym (albo interfejsem) może przechowywać obiekt dowolnego podtypu, a gdy wywołujesz na niej metodę, Java uruchamia wersję należącą do rzeczywistej klasy obiektu, a nie wersję wynikającą z zadeklarowanego typu zmiennej.

Przygotowanie do tego widać już na stronie o dziedziczeniu: podklasa nadpisuje metodę rodzica. Polimorfizm sprawia, że takie nadpisanie ma sens: pozwala pisać kod dla typu ogólnego i zostawić każdemu konkretnemu obiektowi jego własne zachowanie.

Zarówno a, jak i b są zadeklarowane jako Animal, a mimo to każdy wypisuje swój własny dźwięk. Ten wybór następuje w czasie działania, na podstawie rzeczywistego obiektu.

Dynamiczne wiązanie metod

Mechanizm, który za tym stoi, to dynamiczne wiązanie metod (dynamic method dispatch): dla nadpisanej metody instancji JVM sprawdza klasę obiektu w czasie działania, żeby zdecydować, którą implementację wywołać. Kompilator sprawdza tylko, czy metoda istnieje w zadeklarowanym typie; faktyczny wybór jest odkładany do chwili uruchomienia programu.

Dzięki temu jedna pętla obsługuje całą mieszankę typów, nigdy nie pytając, czym jest każdy z nich:

Pętla zna tylko Shape. Dodaj później Triangle extends Shape, a ten kod dalej działa bez zmian i właśnie o to chodzi. Kod zależy od abstrakcji, a nie od konkretnej listy typów.

Upcasting i downcasting

Zapisanie Dog w zmiennej Animal to upcasting, czyli przejście w górę hierarchii do ogólniejszego typu. Jest zawsze bezpieczny i Java wykonuje go niejawnie, bo każdy Dog jest Animal.

Ruch w drugą stronę to downcasting: bierzesz referencję typu nadrzędnego i traktujesz ją jako konkretny podtyp. To forma rzutowania typów między typami referencyjnymi i jest poprawna tylko wtedy, gdy obiekt naprawdę jest tym podtypem. Dlatego rzutowanie trzeba zapisać jawnie, a jeśli się pomylisz, grozi ci ClassCastException:

Ostatnie rzutowanie kompiluje się bez problemu, bo kompilator nie potrafi udowodnić, że jest błędne, ale wysypuje się w czasie działania, bo Cat nie jest Dog. Nigdy nie rób downcastingu na wiarę.

Zabezpieczaj downcasting przez instanceof

Przed downcastingiem sprawdź rzeczywisty typ przez instanceof. Nowoczesna Java pozwala od razu związać wynik w tym samym wyrażeniu (dopasowanie wzorca dla instanceof), więc osobne rzutowanie nie jest potrzebne:

instanceof zwraca false dla null, więc to sprawdzenie chroni też przed NullPointerException. Mimo to, jeśli piszesz długie łańcuchy instanceof, często jest to znak, że zachowanie powinno znaleźć się wewnątrz klas jako nadpisana metoda. Niech rozgałęzianie wykona za ciebie polimorfizm.

Nadpisywanie a przeciążanie

Te dwa pojęcia brzmią podobnie, ale nie mają ze sobą związku, a ich mylenie to klasyczne źródło nieporozumień.

Nadpisywanie (overriding) to podklasa zastępująca metodę rodzica metodą o identycznej sygnaturze. Jest rozstrzygane w czasie działania według typu obiektu; to polimorfizm, którego używaliśmy do tej pory.

Przeciążanie (overloading) to kilka metod o tej samej nazwie, ale różnych listach parametrów w jednej klasie. Jest rozstrzygane w czasie kompilacji według typów argumentów, bez żadnego wiązania w czasie działania:

Kompilator wybiera pasujące describe wyłącznie na podstawie statycznego typu argumentu. Nie ma tu obiektu rodzica ani potomka, więc to nie jest polimorfizm w czasie działania, a jedynie ponowne użycie nazwy metody.

Częsta pułapka: pola nie są polimorficzne

W czasie działania wiązane są tylko metody instancji. Pola i metody statyczne są rozstrzygane według zadeklarowanego typu, na czym potyka się wiele osób:

p.name() uruchamia wersję z Child (polimorfizm), ale p.label odczytuje pole z Parent, bo pola są przesłaniane, a nie nadpisywane. Rozwiązanie jest proste: trzymaj pola jako private i udostępniaj je tylko przez metody, żeby zawsze wygrywało wywołanie polimorficzne.

Dalej: modyfikatory dostępu

Polimorfizm działa czysto tylko wtedy, gdy podklasy widzą i mogą nadpisać właściwe składowe, a reszta kodu nie może sięgnąć do środka i złamać niezmienników. Tę równowagę kontrolują public, protected, private i dostęp pakietowy, czyli modyfikatory dostępu, o których jest następna strona.

Najczęściej zadawane pytania

Czym jest polimorfizm w Javie?

Polimorfizm oznacza, że jeden typ referencji może wskazywać na obiekty wielu różnych klas, a metoda, która faktycznie się uruchamia, jest wybierana w czasie działania na podstawie rzeczywistego typu obiektu, a nie zadeklarowanego typu zmiennej. Zmienna Shape shape może więc przechowywać Circle albo Square, a wywołanie shape.area() automatycznie uruchamia właściwą wersję.

Czym różni się nadpisywanie od przeciążania w Javie?

Nadpisywanie (overriding) to sytuacja, w której podklasa zastępuje metodę nadklasy metodą o tej samej sygnaturze; to ono napędza polimorfizm w czasie działania. Przeciążanie (overloading) to kilka metod o tej samej nazwie, ale różnych listach parametrów w jednej klasie; kompilator wybiera jedną w czasie kompilacji na podstawie argumentów. Nadpisywanie jest rozstrzygane w czasie działania według typu obiektu, przeciążanie w czasie kompilacji według typów argumentów.

Czym różni się upcasting od downcastingu w Javie?

Upcasting traktuje obiekt klasy pochodnej jak typ nadrzędny (Animal a = new Dog();); jest zawsze bezpieczny i zwykle niejawny. Downcasting działa w drugą stronę (Dog d = (Dog) a;) i jest bezpieczny tylko wtedy, gdy obiekt naprawdę jest tym podtypem; w przeciwnym razie rzuca ClassCastException. Każdy downcast najpierw zabezpiecz przez instanceof.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ