Polimorfizm  Jak w tytule wpisu.

Jak pamiętasz każdy obiekt w Javie, który jest w związku IS-A może mieć wiele form czyli być polimorficzny. Każdy obiekt z wyjątkiem typu Object jest polimorficzny, ponieważ zdają one test IS-A dla swojego typu i dla typu Object

 

Cel certyfikatu – Objectives 5.2

Exam Icon

5.2. Given a scenario, develop code that demonstrates the use of polymorphism. Futher, determine when casting will be necessary and recognize compiler vs. runtime errors related to object reference casting.

W zależności od scenariusza. Stwórz kod, który demonstruje użycie polimorfizmu. Ponadto określ, kiedy rzutowanie będzie koniecznie i wystąpią błędy kompilatora oraz błędy aplikacji przy rzutowaniu obiektu referencji.

Istnieje tylko jeden sposób na uzyskania dostępu  do obiektu i jest to referencja. Przypomnijmy  sobie  kluczowe rzeczy na temat referencji.

  • Zmienne referencyjne mogą być tylko jednego typu i raz zadeklarowany typ nie może ulec zmianie.
  • Zmienne referencyjne mogą zmieniać referencje w wyniku przypisania, chyba że zmienna jest zadeklarowana ze słowem final.
  • Typy zmiennych referencyjnych determinują metody, które mogą być wywołane w obiekcie za pomocą  zmiennej, do której on się referuje.
  • Zmienna referencyjna może   referować się do każdego obiektu tego samego typu, jaki został zadeklarowany ,ale też może referować się do każdego podtypu zadeklarowanego typu.

Wcześniej stworzyłem klasę Shape, która była rozszerzona przez inne klasy jak : Star czy Triangle. Teraz pomyśl sobie ,że chcesz animować te kształty ,ale niekoniecznie wszystkie. Co wtedy zrobimy z dziedziczeniem.

Mógłbym stworzyć klasę z metodą “animate()” i tylko niektóre kształty dziedziczyłyby po tej klasie. Czyli przykładowo klasa kwadrat Square dziedziczyłaby po klasie Shape i Animation.
To nie zadziała. Java wspiera pojedyncze dziedziczenie klas.

image

Klasa może rozszerzać tylko jedną klasę. To oznacza ,że istnieje tylko jeden rodzic dla każdej klasy. Klasa może mieć wiele przodków ,ale jest to możliwe, ponieważ klasa B rozszerza klasę  A, i klasa C rozszerza klasę B i tak dalej.

Więc co możesz jeszcze zrobić. Mógłbyś dodać metodę animate() do klasy Shape ,a później wyłączyć tę metodę dla obiektów, które nie mogą być animowane. Jest to zły pomysł z wielu powodów sprawiłoby to ,że klasa Shape byłaby już niespójna. Klasa Shape mówiłaby ,że każda klasa dziedzicząca po niej miałaby tę metodę, gdy w rezultacie tak nie jest, ponieważ niektóre klasy nie będą mogły tego zrobić.
Co jeszcze pozostało. Możemy stworzyć interfejs i nazwać go Animatable zgodnie z konwencją. To dobry pomysł.

interface Animatable
{
    public void animate();
}

Oto klasa Square, która implementuje ten interfejs.

interface Animatable
{
    public void animate();
}

class Shape
{
    public void drawShape()
    {
        System.out.println("wyświetlę kształty");
    }
}

class Square extends Shape implements Animatable
{

    @Override  
    public void animate() 
    {
        System.out.println("Animuje");
    }    
}

Teraz klasa Square może przejść test IS-A i dla klasy Shape, jak i dla interfejsu Animatable. Oznacza to ,że Square może być traktowany polimorficznie jako jedna z czterech rzeczy w każdym momencie w zależności od deklaracji typu i zmiennej referencyjnej jak:

  • Object (każdy obiekt dziedziczy po Object)
  • Shape (klasa Square rozszerza Shape)
  • Square (tym właśnie jest ten typ)
  • Animatable (ponieważ Square implementuje Animatable)


Wszystkie wyrażenia poniżej są bezbłędne.

Square square =  new Square();
Object obj = square;
Shape shape = square;
Animatable storyboard = square;


Istnieje jednak tylko jeden obiekt tutaj i jest on instancją klasy Square ,ale są aż cztery różne typy zmiennych referencyjne, które mogą referować się do tego jednego obiektu na stercie. Chociaż tylko dwie z nich mogą wywołać metodę drawShape  jest to zmienna referencyjna typu “Square” i “Shape”. Dlaczego?

Pamiętaj ,że inwokacja metody, która jest dozwolona przez kompilator jest bazowana na zadeklarowanym typie referencyjnym niezależnie od typu obiektu.  Poza tym dla kompilatora Square IS-A Shape więc i ona posiada tę metodę w wyniku dziedziczenia.

Idąc tym przykładem dalej zmienna referencyjna Animatable może wywołać tylko metodę “animate()”. Najważniejszą siłą interfejsu jest jednak fakt ,że interfejs może być implementowany w każdym punkcie w drzewie dziedziczenia. Interfejs może referować się do obiektów, które nie są ze sobą powiązane rodzinnie. Przykładowo metoda przyjmująca interfejs Animatable mogłaby też animować punkty czy kreski, które byłyby niezależne od klasy Shape.

Wciąż jeszcze nie dotarłem do największej i najciekawszej rzeczy w tym wpisie. Pomimo iż kompilator wie tylko o  zadeklarowanym typie referencyjnym maszyna wirtualna Java w czasie wykonywania programu doskonale wie czym tak naprawdę jest obiekt. Oznacza to ,że nawet jeśli wywołujesz metodę “drawShape()”  w zmiennej referencyjnej Shape,  która zawiera obiekt Square, to ta metoda może zwrócić coś innego, jeśli jest ona nadpisana w klasie Square.

Maszyna wirtualna Java patrzy na prawdziwy obiekt zawarty w tej referencji i widzi ,że ma ona nadpisaną wersję metody i wywołuje metodę z aktualnego obiektu w klasie. Czy o czymś trzeb jeszcze pamiętać?

Polimorficzne inwokacje metod są stosowane tylko w instancjach metod. Możesz zawsze referować się do obiektu, który ma głównie wiele zmiennych referencyjnych (super klasa ,interfejs) ,ale w czasie wykonywania programu jedna rzecz  jest dynamicznie wybrana z aktualnego obiektu (niż typu referencji )  są to instancje metod. Nie statycznych metod. Nie zmiennych. Tylko nadpisanych instancji metod, które są dynamicznie wywoływane na bazie prawdziwego typu obiektu.

Dalszy część wpisu jest zależna od opisu nadpisania metod więc nic nie stoi na przeszkodzie by i to omówić.

Nadpisywanie Overriding

Cel certyfikatu – Objectives 1.5 i 5.4

Exam Icon

1.5. Given a code example, determine if a method is correctly overriding or overloading another method, and identify legal return values (including covariant returns), for the method.

[Brak tłumaczenia]

5.4. Given a scenario, develop code that declares and/or invokes overridden or overloaded methods and code that declares and/or invokes superclass, overridden, or overloaded constructors

[Brak tłumaczenia]

Za każdym razem, gdy klasa dziedziczy metodę po super klasie masz do dyspozycji jej nadpisanie (override), chyba że metoda jest oznaczona jako finalna. Kluczowa zaleta nadpisywani metody pozwala nam na zmianę zachowania specyficznej klasy pochodnej. Oto przykład klasy Dolphin, która ma nadpisaną metodę “move()” z klasy mammal.

class Mammal
{
    void move()
    {
         System.out.println("Ssak porusza się na swój sposób");
    }
}

public class Dolphin extends Mammal 
{
    @Override  void move() 
    {
        System.out.println("Delfin pływa");
    }
}

Mając abstrakcyjną metodę nie masz wyboru musisz ją zaimplementować chyba ,że klasa też jest abstrakcyjna. Abstrakcyjne metody muszą być, zaimplementować przez klasę pochodną ,ale to też wygląda jak nadpisywanie metod. Możesz pomyśleć ,że abstrakcyjne metody wymuszają nadpisywanie.

Twórca klasy Mammal Ssak zdecydował ,że dla celu polimorficznych wszystkie zwierzęta, które są ssakami muszą mieć metodę “move()”.  Polimorficznie, gdy ktoś ma zmienną referencyjną Mammal, która nie referuje się do instancji Mammal (przykładowo do obiektu Dolphin) ,ale do swojego podtypu i wywoła w nim metodę move(), to tak naprawdę wywoła on specyficzną odmianę tej metody w obiekcie Dolphin.

Wypadałoby oznaczyć tę metodę jako abstrakcyjną. Nie ma ona sensu dla samej klasy Mammal ,a w ten sposób dla wszystkich programistów powiedziałbym ,że wszystkie podtypy tej klasy muszą nadpisywać tę metodę.

Nie abstrakcyjne użycie polimorfizmu wygląda tak.

class Mammal
{
    void move()
    {
         System.out.println("Ssak porusza się");
    }
}

class Dolphin extends Mammal 
{
    @Override  
    void move() 
    {
        System.out.println("Delfin pływa");
    }

    void jumpingAndPlaying(){}
}

public class TestMammal 
{
    public static void main(String[] args) 
    {
        Mammal m1 = new Mammal();
        Mammal m2 = new Dolphin();
        
        m1.move();
        m2.move();
    }
}

Wynik kodu:

Ssak porusza się
Delfin pływa

W poprzedzającym kodzie klasa testująca używa referencji Mammal do wywołania metody w obiekcie Dolphin. Pamiętaj ,że kompilator pozwala tylko na użycie metod w klasie Mammal w wyniku zmiennej referencyjnej.

Oznacza to ,że nie mogę wywołać metody z klasy Dolphin mając zmienną referencyjną Mammal.

image

Kompilator patrzy tylko na typ referencji nie na instancje typu. Polimorfizm pozwala na użycie bardziej abstrakcyjnych super typów, włączając w to interfejs,  pozwalając na referowanie się do subtypów wszelkiego rodzaju.

Czas na ograniczenia metod nadpisanych. Metody nadpisane nie mogą mieć innego poziomu dostępu niż ten zadeklarowany oryginalnie w metodzie. Przykładowo nie możesz nadpisać metody z modyfikatorem dostępu protected i dać jej modyfikator public.  Ma to sens, ponieważ, gdyby to było możliwe wszystko w programie mogłoby eksplodować (żart). Powiedzmy ,że stosowanie modyfikatorów w metodach straciłoby jakikolwiek sens.

Zmienny modyfikator dostępu w poprzednim przykładzie i zobaczmy co się stanie.

image

Kompilator oczywiście zgłosi błąd.

W referencji Mammal, która przechowuje obiekt Dolphin możesz wykonać wszystkie metody z klasy Mammal co znaczy ,że klasa Dolphin musiała spełnić kontrakt super klasy Mammal.

Zasady nadpisywania metod są następujące:

  • Lista argumentów musi być dokładnie zgodna w metodzie nadpisanej. Gdyby tak nie było skończysz z metodą przeciążoną, której nie chciałeś stworzyć.
  • Typ zwracany musi być dokładnie taki sam ,albo być subtypem,który został zadeklarowany oryginalnie.
  • Poziom dostępności nie może być mniej i bardziej restrykcyjny niż metoda nadpisana.
  • Instancje metod mogą być nadpisane, jeżeli tylko są  one odziedziczone w klasie pochodnej. Klasa pochodna może nadpisać metody wewnątrz paczki, jeśli nie są oznaczone jako private, czy final. Klasa pochodna z innej paczki może nadpisać tylko metody niefinalne i oznaczone jako public i protected.
  • Metoda nadpisana może wyrzuć niesprawdzony wyjątek bez względu na to, czy metoda nadpisana deklaruje wyjątek. Przydałoby się to omówić później.
  • Metoda nadpisana nie może wyrzucać sprawdzonych wyjątków, które są nowe lub są szersze niż te zadeklarowane w metodzie nadpisanej. Na przykład metoda, która deklaruje wyjątek “FileNotFoundException” nie może być nadpisana przez metodę, która deklaruje inny wyjątek.
  • Metoda nadpisana może wyrzucić szersze bądź węższe wyjątki.
  • Nie możesz nadpisać metody oznaczonej jako final.
  • Nie możesz nadpisać metody oznaczonej jako static. O tym już niedługo.
  • Jeśli metoda nie może być odziedziczona to znaczy też ,że nie może być nadpisana.

Oto dowód ostatniej zasady.

java override

Wywołanie wersje metody z super klasy w metodzie nadpisanej

Czasami chcesz wykorzystać już istniejący kod z super klasy i  dodać do niego trochę więcej kodu, czyli wciąż nadpisując go. Kod = metoda

To tak jakbyś mówił programowi “Uruchom metodę z superklasy ,a potem wróć do metody przeciążonej klasy pochodnej aby wykonać dodatkowy kod”. Chociaż twój kod może wykonać się w każdym momencie.

Tutaj pojawia się słowo kluczowe “super”.

class Mammal
{
    void giveMeInformationsAboutIt()
    {
        System.out.println("Jest to też Ssak...");
    }
}

class Dolphin extends Mammal 
{
    @Override void giveMeInformationsAboutIt() 
    {
        System.out.println("Delfiny potrafią...");
        super.giveMeInformationsAboutIt();
        System.out.println("Sen delfinow jest...");
    }
}

Untitled5

Jeśli metoda jest nadpisana ,ale używa polimorficznej referencji, która referuje się do podtypu obiektu, w którym metoda jest nadpisana, kompilator zakłada ,że wywołujesz  wersję metody z superklasy.

Jeśli metoda w super klasie deklaruje sprawdzony wyjątek,ale nadpisująca klasa pochodna w metodzie jej nie deklaruje – kompilator wciąż będzie myślał ,że wywołujesz metodę z wyjątkiem. Oto przykład:

class Mammal
{
    public void move() throws Exception
    {
        //throws wyrzuca Exception wyjątek 
    }
}

class Cat extends Mammal
{
    @Override 
    public void move()
    {
         //nie ma wyjątków 
    }
}

public class TestMammal 
{
    public static void main(String[] args) 
    {
        Mammal m = new Cat();
        Cat c = new Cat();
        c.move(); //ok        
        m.move();

        //błąd kompilatora  
        //unhandled exception type Exception     
    }
}

Ten kod się nie skompiluje z powodu wyjątku, który jest zadeklarowany w metodzie Mammal.move(). Dzieje się tak, pomimo iż wiadomo ,że użyjemy metody move() w wersji klasy Cat. , która nie deklaruje żadnych wyjątków.

Podsumowanie błędnego i poprawnego nadpisania metod

class Mammal
{
    public void move()
    {
        
    }
}

Patrząc na kod powyżej, jak myślisz, w jaki sposób można błędnie nadpisać tą metodę.

Błędny napisany kodSzczegóły problemu
private void move() {}Liczby całkowite
public void move() throws IOError {}Deklaruje sprawdzony wyjątek, który nie jest zdefiniowany w super klasie.
public void move(int foot) {}Poprawne przeciążenie, błędne nadpisanie, ponieważ liczba argumentów, jak i ich typ uległ zmianie
public String move() {}Typ zwracany się nie zgadza. Nie jest to też przeciążenie metody, ponieważ lista argumentów nie uległa zmianie.


To wszystko.

Co dalej:

Przeciążanie metod.