Dziedziczenie
Podstawowe pojęcia obiektowości
Trzy zasady programowania obiektowego:
Hermetyzacja
Klasa składa się ze składowych klasy, metod klasy i konstruktorów. Składowe i metody klasy mogą być dostępne tylko dla metod tej samej klasy (prywatne). Można ukryć implementację. Można ukryć złożoność obliczeniową. Obiekty utworzone na podstawie klas zachowują te własności.
Składowe i metody klasy mogą być dostępne na zewnątrz klasy (publiczne i zaprzyjaźnione - szczegóły będą omówione później). Wykonanie takich metod jest jedynym sposobem wykorzystania implementacji. Dzięki w ten sposób udostępnionym metodom można w pełni wykorzystać impementację.
Dziedziczenie
Obiekt dziedziczący otrzymuje własności obiektu, z którego dziedziczy. Klasy opisujące dziedziczenie tworzą hierarchię klas. Jeśli klasa B dziedziczy z klasy A, to mówimy że A jest klasą bazową, klasą nadrzędną, nadklasą dla klasy B. B jest klasą wywiedzioną, rozszerzającą, podklasą dla klasy A. Klasa B ma własności klasy A i jest rozszerzona o dodatkowe własności (składowe, metody).
Polimorfizm
Polimorfizm to inaczej wielopostaciowość. Polimorfizm rozpatruje się w hierarchii klas. Pozwala na użycie jednego interfejsu do wykonania różnych implementacji.
Miejsce inicjowania zmiennych
W tym przykładzie jeszcze nie ma dziedziczenia. Podsumujemy wszystkie dotychczasowe informacje o miejscu inicjowania zmiennych, wszystkie możliwości pojawiły we wcześniejszych przykładach. Prześledźmy kolejny program.
P15Dziedzicz.java
Opis programu
Program składa się z dwóch klas: ParaFloat, P15Dziedzicz.
W klasie ParaFloat jest jeden konstruktor i jedna metoda toString().
W klasie P15Dziedzicz mamy jeden konstruktor oraz dwie metody drukuj() i main.
Miejsca inicjowania
Inicjowanie wraz z definicją. W klasie P15Dziedzicz obiekt s1 jest zainicjowany w miejscu, w którym zostało zdefiniowane pole klasy. Nie ma potrzeby zainicjowania obiektu s1 w żadnym z konstruktorów, chociaż można to zrobić.
Inicjowanie w konstruktorze. Pola s2, f1. W konstruktorze klasy P15Dziedzicz inicjowane są obydwa te pola. Po wywołaniu konstruktora P15Dziedzicz pola s1,s2,f1 mają określone wartości. Pole s3 nadal jest pustą referencją, nie zostało określone.
Inicjowanie leniwe, inicjowanie zminnych następuje wtedy, gdy staje się niezbędne, wtedy gdy należy wykorzystać to pole. Pole s3 jest inicjowane w metodzie drukuj(), jest inicjowane pod warunkiem, że wcześniej nie zostało zainicjowane.
Klasa bazowa, klasa pochodna, kolejność wołania konstruktorów
Poniższy program zilustruje sposób wykonania konstruktorów w hierarchii klas.
P16Dziedzicz.java
Przyjrzyjmy się dokładniej ostatniemu przykładowi
Program składa się z dwóch klas: Pierwsza oraz P16Dziedzicz
Klasa P16Dziedzicz dziedziczy po klasie Pierwsza, inaczej klasa P16Dziedzicz jest rozszerzeniem klasy Pierwsza. Słowo kluczowe extends pozwala na określenie klasy, która jest aktualnie rozszerzana.
W obydwu klasach jest funkcja main - często służy to do przetestowania klasy.
W klasie P16Dziedzicz nie ma konstruktora. Jeśli w klasie nie ma żadnego konstruktora, to automatycznie dostępny jest konstruktor bezargumentowy, konstruktor domyślny. Jeśli w klasie zapiszemy jeden (lub więcej) konstruktor, to nie jest dostępny konstruktor domyślny.
W obydwu klasach jest umieszczona funkcja o nazwie istotna().
Wszystkie metody klasy bazowej (klasa Pierwsza) mają modyfikator dostępu protected i wszyskie są dostępne w klasie pochodnej (klasa P16Dziedzicz), ale nie na odwrót.
Metody klasy bazowej mogą być nadpisane w klasie pochodnej. Taką metodą jest metoda o nazwie istotna().
Metoda nadpisana w klasie pochodnej oraz odpowiednia metoda z klasy bazowej mogą być wykorzystywane w klasie pochodnej. Jeśli chcemy wykorzystać metodę z klasy pochodnej, wystarczy wywołać ją bezpośrednio. W przykładzie jest ona wywoływana na rzecz obiektu klasy P16Dziedzicz.
Jeśli chcemy wykorzystać metodę z klasy bazowej, należy odwołanie do niej poprzedzić słowem kluczowym super. W ciele metody istotna() z klasy P16Dziedzicz jest wykonywana metoda istotna() z klasy bazowej, czyli klasy Pierwsza.
Bardzo ważnym zagadnieniem jest kolejność wywoływania konstruktorów. Konstruktor klasy bazowej jest wywoływany przed wykonaniem konstruktora klasy pochodnej - tekst "Konstruktor" pojawia się przed tekstem "Po wykonaniu Konstruktora klasy P16Dziedzicz". Tak więc wykonanie konstruktora klasy bazowej nastąpiło domyślnie w chwili wywołania konstruktora klasy pochodnej. Prywatne pole s klasy Pierwsza przyjęło wartość "Pierwsza ".
Wykonanie metody p1() na rzecz obiektu b spowodowało nadanie wartości "Pierwsza, p1" polu s.
Wykonanie metody p2() na rzecz obiektu b spowodowało nadanie wartości "Pierwsza, p1, p2" polu s.
Wykonanie metody istotna() na rzecz obiektu b spowodowało wywołanie metody istotna() z klasy pochodnej. Nadało wartość "Pierwsza, p1, p2, P16Dziedzicz.istotna()" polu s. Następnie została wykonana metoda istotna() z klasy bazowej (dzięki wywołaniu super.istotna();). Wywołanie to powoduje zmianę pola s i teraz ma ono wartość "Pierwsza, p1, p2, P16Dziedzicz.istotna(), istotna".
Wywołanie metody r1 na rzecz obiektu b zmieni wartość pola s na "Pierwsza, p1, p2, P16Dziedzicz.istotna(), istotna, r1".
Wywołanie metody pisz() na rzecz obiektu b spowoduje wydruk tekstu "Pierwsza, p1, p2, P16Dziedzicz.istotna(), istotna, r1 protected".
Składowa x klasy Pierwsza jest dostępna w klasie wywiedzionej, składowa s nie jest dostępna w klasie wywiedzionej.
Wywołanie metody main() na rzecz klasy Pierwsza spowoduje wykonanie algorytmu zawartego w metodzie main(). Będzie to budowa obiektu klasy Pierwsza o nazwie a - czyli nadanie wartości polu s równej "Pierwsza" i wyprowadzenie na monitor tekstu "Konstruktor".
Wykonanie metody p1() na rzecz obiektu a; pole s przyjmie wartość "Pierwsza, p1".
Wykonanie metody p2() na rzecz obiektu a; pole a przyjmie wartość "Pierwsza, p1, p2".
Wykonanie metody istotna() z klasy Pierwsza na rzecz obiektu a; pole s przyjme wartość "Pierwsza, p1, p2, istotna".
Wykonanie metody pisz() na rzecz obiektu a; jest to wyprowadzenie tekstu "Pierwsza, p1, p2, istotna".
Uwaga!! Wywołanie konstruktora klasy pochodnej powoduje wywołanie odpowiedniego konstruktora klasy bazowej !!!!!
Podsumowując
Składowe klasy bazowej z modyfikatorem dostępu protected i public są dostępne klasie wywiedzionej.
Metody klasy bazowej z modyfikatorem dostępu protected i public są dostępne klasie wywiedzionej.
W klasach wywiedzionych można nadpisać metody z klasy bazowej. Dostęp do metody z klasy bazowej, nadpisanej klasie wywiedzionej, można uzyskać za pomocą słowa kluczowego super.
Wywołanie konstruktora klasy wywiedzionej powoduje wywołanie konstruktora z klasy bazowej.
Kolejność wołania konstruktorów
Kolejny przykład ilustruje kolejność wywołania konstruktorów w łatwej do przeanalizowania formie.
P17Dziedzicz.java
Opis programu
Klasa Jeden ma jeden konstruktor wyprowadzający tekst "Jeden".
Klasa Dwa jest klasą pochodną klasy Jeden. Ma jeden konstruktor tekst wyprowadzający tekst "Dwa".
Klasa P17Dziedzicz jest klasą pochodną klasy Dwa. Ma jeden konstruktor wyprowadzający tekst "P17Dziedzicz", w klasie tej jest również metoda main().
W metodzie main() (od tej metody zaczyna się wykonanie tego programu) jest budowany jeden obiekt i jest to obiekt klasy P17Dziedzicz. Wywoływany jest konstruktor P17Dziedzicz, jest to konstruktor z klasy wywiedzionej. Zanim zostanie wykonany konstruktor P17Dziedzicz, zostanie wywołany konstruktor klasy bazowej, czyli konstruktor klasy Dwa.
Przechodzimy do wykonania konstruktora klasy Dwa, zanim zostanie on wykonany zostanie wywołany konstruktor klasy bazowej, czyli konstruktor klasy Jeden.
Przechodzimy do wykonania konstruktora klasy Jeden, spowoduje on wydruk tekstu "Jeden".
Wracamy do wykonania konstruktora klasy Dwa, spowoduje on wydruk tekstu "Dwa".
Kontynuujeny wykonanie konstruktora klasy P17Dziedzicz, spowoduje on wydruk tekstu "P17Dziedzicz".
Pierwszą instrukcją w konstruktorze powinna być instrukcja określająca, który z konstruktorów klasy nadrzednej należy wykonać (super(...)). Jeśli takiej instrukcji nie zapiszemy, to jest wywoływany konstruktor bezargumentowy.
Otrzymujemy więc wydruki, takie jak zostały przedstawione powyżej. Rzecz oczywiście musi się co nieco skomplikować, jeśli w budowanych klasach są konstruktory z argumentami, w szczególności wiele takich konstruktorów. Nie wszystko może przebiegać automatycznie. Jak wtedy wygląda wywoływanie konstruktorów zobaczymy w następnych przykładach.
Wołanie konstruktorów z argumentami
Następny przykład ilustruje sposób wywoływania konstruktorów innych niż domyślne w hierarchii klas.
P18Dziedzicz.java
Opis programu
Klasa KlasaJeden zawiera jeden konstruktor. Konstruktor ten ma jeden argument typu float.
Klasa KlasaDwa zawiera jeden konstruktor. Konstruktor ten ma jeden argument typu int. Klasa ta jest rozszerzeniem klasy KlasaJeden.
Klasa P18Dziedzicz zawiera jeden konstruktor bezargumentowy. Klasa ta wywodzi się z klasy KlasaDwa. W klasie tej jest również metoda main(). Od tej metody zaczyna się wykonanie programu.
W metodzie main() wywoływany jest konstruktor klasy P18Dziedzicz. Jak wiemy, zanim zostanie wykonany konstruktor klasy wywiedzionej, musi zostać zrealizowany konstruktor klasy bazowej. Jeśli konstruktorów jest więcej lub jeśli są to konstruktory z argumentami, to jako pierwsza instrukcja musi wystąpić instrukcja odwołująca się do konstruktora klasy nadrzędnej. Instrukcja ta ma postać: super(lista_argumentów_konstruktora klasy _bazowej)
Konstruktory zostaną wykonane w kolejności od najwyżej stojącego w hierachii klas. Wystarczy spojrzeć na otrzymane rezultaty, zauważyć tę prawidłowość.
Odwołanie do konstruktora klasy bazowej musi byćć pierwszą instrukcją w konstruktorze klasy wywiedzionej.
Dla przypomnienia, konstruktor bezargumentowy jest tworzony domyślnie, tylko wtedy, gdy w klasie nie ma żadnego konstruktora.