Dziedziczenie jest szeroko wykorzystywane w programowaniu obiektowym. Wywodzi się ono niejako w sposób bezpośredni z idei programowania obiektowego. W założeniu polega na pomyśle, że niepotrzebnym jest wielokrotne powielanie kodu programu w różnych jego blokach funkcjonalnych, które się między sobą nie za bardzo różnią, bądź nie różnią się w ogóle, a powielenie wynika z tego, że w ten sam (lub podobny) sposób mamy się zająć dwiema lub więcej funkcjonalnościami. Takie powielanie kodu powoduje jego znaczące rozbudowanie i nie służy prostocie programowania, sprzyja powielaniu błędów, czy też pomyłkom programistycznym w modyfikacji, poprawianiu czy ewaluacji oprogramowania. To tak jakbyśmy nie używali pętli for dla wypisania 500 linijek zdania „Zawsze będę używał pętli, aby napisać to zdanie!!!„, a zamiast tego 500 razy wypisali linię: System.out.println(„Zawsze będę używał pętli, aby napisać to zdanie!!!„);. Można? Można! Tylko jest to niewłaściwe. Już w Biblii (nie w „Java. Biblia programowania” 😉 ) jest użyta sentencja:

"Wszystko mi wolno, ale nie wszystko przynosi korzyść." (1 Kor 6,12)

Tak i w programowaniu mechanizm dziedziczenia umożliwia uzyskanie automatycznego powielenia kodu celem użycia go dla różnych funkcjonalności1 to zwykłe klasy 😉 z modyfikacją jego zawartości i rozbudową celem dopasowania do potrzeb programisty w danej, specyficznej sytuacji 2to już dziedziczenie.

Przykładem użycia dziedziczenia jest poniższy kod:

public class Dziedziczenie {
    public static void main(String[] argumenty) {
        Jon a=new Jon(1,1,2);
        // Atom a=new Jon(1,1,2);  // wypróbuj tę notację zamiast powyższej i sprawdź czy działa tak samo, czy też widzisz jakieś różnice?!
    }    
}

class Atom {
    int neutrony,protony,elektrony;
    
    Atom()
    {
        System.out.println("Tworzę model atomu... brak podstawowych informacji o jego strukturze.");
    }        
    Atom(int n, int p, int e) {
        neutrony=n; protony=p; elektrony=e;
        System.out.printf("Tworzę model atomu... neutronów: %d, protonów: %d, elektronów: %d. \n\r",n,p,e);
    }
}


class Jon extends Atom {
    Jon() {
        super();
        System.out.println("Obiekt jest jest zrównoważony elektrycznie.");
    }
    Jon(int n, int p, int e) {
        super(n,p,e);
        if (p!=e) System.out.print("Atom tworzy jon i jest zjonizowany "+((p>e)?"dodatnio":"ujemnie")+".\n\r");
    }
}

W powyższym przykładzie zastosowaliśmy dziedziczenie, aby wprowadzić dodatkową funkcjonalność do klasy Atom. Dzięki tej modyfikacji mamy możliwość sprawdzenia czy dany atom jest jonem, czy też jest obojętny elektrycznie. Informacja o dziedziczeniu została wprowadzona za pomocą słowa kluczowego extends3„rozszerza”. Dzięki temu obiekty oparte na klasie Atom nie zmieniają zachowania, a obiekty oparte na klasie Jon są uzupełnione o odpowiednią informację. Dziedziczenie pozwala także wprowadzić dodatkowe metody manipulujące danymi, także dodatkowe konstruktory. W powyższym przykładzie widzimy konstruktory bezparametrowe i trójparametrowe.

Uwaga: W przypadku języka Java nie występuje dziedziczenie wielobazowe (z wielu rodziców) dostępne np. w C++, wynika to z przemyśleń deweloperów języka Java i określenia braku takiej konieczności. Argumentują to zachowaniem jasności struktury drzewa dziedziczenia. Dziedziczenie jest zawsze z jednego rodzica.

Metoda super();

W przykładzie widać też tajemnicze słowo super(); Jego działanie sprawdźmy empirycznie i wyłączmy linijkę (wstawmy ją w komentarz liniowy) // super(n,p,e); i co się stało4 Komenda super(); to wywołanie konstruktora z klasy nadrzędnej (superklasy) z której odziedziczyliśmy (czyli: rodzica/przodka ang: parent/anchestor) i powinna być użyta jako pierwsza komenda w linii konstruktora klasy potomnej (subklasy), dziedziczącej (czyli: dziecka/potomka ang: child/descendant)4W nawiasach wymienione są inne nazwy związane z nazewnictwem klas w dziedziczeniu w programowaniu obiektowym. Tym niemniej zalecane jest aby używać jednej notacji – stąd dobrze wybrać dla siebie te nazwy (są one w tej samej kolejności wymieniane w obu przypadkach), by nie mieszać różnych nazw.!?

Zwróćcie uwagę też na to, że w klasie dziedziczącej Jon nie ma deklaracji protonów, neutronów i elektronów, a mimo to są one dostępne dla klasy Jon. Pobawmy się teraz… duplikowaniem zmiennych (noszących te same nazwy). Oto i zmodyfikowany kod:

public class Dziedziczenie {

    public static void main(String[] args) {
        Jon a=new Jon(1,1,2);
      
        System.out.printf("Liczba protonów w badanym jonie wynosi %d.\r\n",a.pobierzLiczbeProtonow());
    }
    
}


class Atom {
    int neutrony,protony,elektrony;
    
    Atom()
    {
        System.out.println("Tworzę model atomu... brak podstawowych informacji o jego strukturze");
    }        
    Atom(int n, int p, int e) {
        neutrony=n; protony=p; elektrony=e;
        System.out.printf("Tworzę model atomu... neutronów: %d, protonów: %d, elektronów: %d. \n\r",n,p,e);
    }
}


class Jon extends Atom {
    //int neutrony, protony, elektrony;  //Na pierwszy raz ta linijka jest wyłączona, włącz ją do drugiego testu.
    Jon() {
        super();
        System.out.println("Obiekt jest jest zrównoważony elektrycznie");
    }
    Jon(int n, int p, int e) {
        super(n,p,e);
        if (p!=e) System.out.print("Atom tworzy jon i jest zjonizowany "+((p>e)?"dodatnio":"ujemnie")+".\n\r");
    }
    
    int pobierzLiczbeProtonow() {
        return protony;
    }
    
}

Powyższy program działa… spodziewanie. Nic nowego tutaj nie nastąpiło. Ciekawostką jest jednak, gdy uwolnimy od komentarza linijkę z deklaracją zmiennych: neutrony, protony, elektrony w klasie Jon włączając ją do użycia. Tutaj już program nie zgłasza co prawda protestów co do zduplikowania nazw zmiennych, ale wydaje się jakby… zgubił wartości. Nic z tych rzeczy. Zasada bezpośredniego zasięgu mówi kompilatorowi, że interesują nas zmienne z aktualnej definicji klasy.

Przesłanianie zmiennych i metod (@Overraid) oraz przedrostki super.this.

Nazywa się to przykrywaniem/przesłanianiem (ang: overraid) i dotyczy tak też zmiennych jak i metod. Nie znaczy to jednak, że nagle tracimy dostęp do zmiennych z obiektu nadrzędnego, aby umożliwić dotarcie do tych zmiennych ponownie posłużymy się słowem super., patrząc na jego zapis tym razem nie jako metodą, a przedrostkiem wskazującym.

public class Dziedziczenie {

    public static void main(String[] args) {
        Jon a=new Jon(1,1,2);
      
        System.out.printf("Liczba protonów w badanym jonie wynosi %d.\r\n",a.pobierzLiczbeProtonowAtomu());
    }
    
}


class Atom {
    int neutrony,protony,elektrony;
    
    Atom()
    {
        System.out.println("Tworzę model atomu... brak podstawowych informacji o jego strukturze");
    }        
    Atom(int n, int p, int e) {
        neutrony=n; protony=p; elektrony=e;
        System.out.printf("Tworzę model atomu... neutronów: %d, protonów: %d, elektronów: %d. \n\r",n,p,e);
    }
}


class Jon extends Atom {
    //int neutrony, protony, elektrony;
    Jon() {
        super();
        System.out.println("Obiekt jest jest zrównoważony elektrycznie");
    }
    Jon(int n, int p, int e) {
        super(n,p,e);
        if (p!=e) System.out.print("Atom tworzy jon i jest zjonizowany "+((p>e)?"dodatnio":"ujemnie")+".\n\r");
    }
    
    int pobierzLiczbeProtonowAtomu() {
        return super.protony;
    }
    int pobierzLiczbeProtonowJonu() {
        return this.protony;
    }
    
}

Jak teraz działa program?! Jak widać, nagle znów dostał się do naszych zmiennych. Idąc „za ciosem” wprowadźmy przedrostek this., który oznacza „z tego obiektu”, w ten sposób w sposób wyraźny zaznaczamy o które elementy nam chodzi.

Dalsza modyfikacja naszego programu testowego pokaże przesłanianie metody pobierzLiczbeProtonowAtomu(). Zerknijmy na kod, pojawiła się tam w klasie Atom dodatkowa deklaracja. Program działa, ale pytanie – którą metodę tak naprawdę uruchamiamy w programie?! Tę z klasy Atom, czy tę z klasy Jon?!

public class Dziedziczenie {

    public static void main(String[] args) {
        Jon a=new Jon(1,1,2);
      
        System.out.printf("Liczba protonów w badanym jonie wynosi %d.\r\n",a.pobierzLiczbeProtonowAtomu());
    }
    
}


class Atom {
    int neutrony,protony,elektrony;
    
    Atom()
    {
        System.out.println("Tworzę model atomu... brak podstawowych informacji o jego strukturze");
    }        
    Atom(int n, int p, int e) {
        neutrony=n; protony=p; elektrony=e;
        System.out.printf("Tworzę model atomu... neutronów: %d, protonów: %d, elektronów: %d. \n\r",n,p,e);
    }

    int pobierzLiczbeProtonowAtomu() {
        return protony;
    }
}


class Jon extends Atom {
    //int neutrony, protony, elektrony;
    Jon() {
        super();
        System.out.println("Obiekt jest jest zrównoważony elektrycznie");
    }
    Jon(int n, int p, int e) {
        super(n,p,e);
        if (p!=e) System.out.print("Atom tworzy jon i jest zjonizowany "+((p>e)?"dodatnio":"ujemnie")+".\n\r");
    }
    
    int pobierzLiczbeProtonowAtomu() {
        return super.protony;
    }
    int pobierzLiczbeProtonowJonu() {
        return this.protony;
    } 
}

Aby uniknąć takich nieporozumień musimy dopisać do klasy dodatkową adnotację – zatem do dzieła. Dla tych, którzy mają z tym problem poniżej jest kod do odkrycia:

public class Dziedziczenie {

    public static void main(String[] args) {
        Jon a=new Jon(1,1,2);
      
        System.out.printf("Liczba protonów w badanym jonie wynosi %d.\r\n",a.pobierzLiczbeProtonowAtomu());
        System.out.println("A teraz skorzystamy z analogicznej metody z klasy Atom...");
        System.out.printf("Liczba protonów w badanym jonie wynosi %d.\r\n",a.pobierzLiczbeProtonowAtomu(1));
    }
    
}


class Atom {
    int neutrony,protony,elektrony;
    
    Atom()
    {
        System.out.println("Tworzę model atomu... brak podstawowych informacji o jego strukturze");
    }        
    Atom(int n, int p, int e) {
        neutrony=n; protony=p; elektrony=e;
        System.out.printf("Tworzę model atomu... neutronów: %d, protonów: %d, elektronów: %d. \n\r",n,p,e);
    }

    int pobierzLiczbeProtonowAtomu() {
        System.out.println("Uruchomiłem metodę z klasy Atom");
        return protony;
    }
}


class Jon extends Atom {
    int neutrony, protony, elektrony;
    Jon() {
        super();
        System.out.println("Obiekt jest jest zrównoważony elektrycznie");
    }
    Jon(int n, int p, int e) {
        super(n,p,e);
        if (p!=e) System.out.print("Atom tworzy jon i jest zjonizowany "+((p>e)?"dodatnio":"ujemnie")+".\n\r");
    }
    
    @Override
    int pobierzLiczbeProtonowAtomu() {
        System.out.println("Uruchomiłem metodę z klasy Jon");
        return super.protony;
    }
    
    int pobierzLiczbeProtonowAtomu(int v) {
        System.out.println("Sięgam do metod klasy Atom");
        return super.pobierzLiczbeProtonowAtomu();
    }
    
    int pobierzLiczbeProtonowJonu() {
        return this.protony;
    }
    
}

W tym przypadku, aby pokazać sposób sięgnięcia do przesłoniętej metody użyłem przeciążania metod poznanego ostatnio. W przypadku wywołania metody pobierzLiczbeProtonowAtomu() bez wprowadzenia jakiegokolwiek argumentu w parametrze uruchamia się metoda pierwsza, przy wywołaniu drugim, gdzie w parametrze metody wprowadzam dowolną wartość typu int zgodnie z zasadą przeciążania użyta do ewaluacji zostanie metoda druga, która to „sięgnie” do metody klasy Atom.

Jak widać na powyższych przykładach dobrze jest znać słowa super this, które umożliwiają dostęp do właściwych pól danych oraz konkretnych metod. Używanie przedrostka this. nie jest konieczne, bowiem kompilator sam zrozumie do którego elementu sięgamy, ale jednoznacznie określa go dla nas, programistów. Powoduje to estetykę w kodzie co przekłada się na jego lepsze zrozumienie i czytelność.

class Jon extends Atom {
    int neutrony, protony, elektrony;
   
    int pobierzLiczbeProtonowJonu() {
        return this.protony;
    }
    
    void ustawLiczbeProtonowJonu(int protony){
        this.protony=protony;
    }
}

Słowo-selektor this. ma jeszcze jedną ważną funkcję. Spójrzmy na powyższy przykład5dla prostoty usunięto wszelki inny kod z poprzedniego listingu poza nas interesującym. Widać tu metodę ustawLiczbeProtonowJonu() – setter, która ustawia naszą zmienną o nazwie protony zdeklarowaną w klasie Jon. To co jest ciekawe, jako parametr ta metoda przyjmuje wartości int i zapisuje je do zmiennej lokalnej także o nazwie protony. Użycie selektora this. powoduje, że kompilator „wie”, iż w tym przypisaniu po lewej stronie równania jest użyta zmienna protony z deklaracji klasy, a po prawej stronie równania zmienna lokalna protony z parametrów metody. Taki zapis jest wygodny z uwagi na brak potrzeby rozróżniania nazwami kolejnych zmiennych, które są nam potrzebne chwilowo, a co za tym idzie nie trzeba wymyślać dla nich szczególnych nazw, co również sprzyja uniwersalizmowi i uproszczeniu zapisów, mniejszej ilości błędów i pomyłek. Jedyne co trzeba w tym przypadku znać ponad wszelką wątpliwość to zasadę działania selektora this. Pominięcie w powyższym przykładzie użycia tego selektora spowoduje, że program pomimo, że się wykona, to nie przypisze wartości do pola protony w klasie, a zmodyfikuje zmienną lokalną co jest bez sensu, jako że to jest modyfikacja jej samej siebie. W efekcie działanie programu przestanie być wiarygodne, a wyniki w dalszym działaniu nieprawidłowe.

Zadanie do samodzielnego wykonania I: Kod zamieszczony powyżej i rozwijany poprzez cały ten temat spróbuj zmodyfikować używając modyfikatorów dostępu (public/private/protected) celem przetestowania zasięgu ich działania i nakładanych ograniczeń przy dziedziczeniu np.: private int proton; w deklaracji klasy Atom.

Zadanie do samodzielnego wykonania II: Napisz deklarację klasy Człowiek zawierającą niezbędne pola danych (prywatne) opisujące podstawowe cechy człowieka i metody je modyfikujące, następnie utwórz klasę Obywatel dziedziczącą z klasy Człowiek i zaproponuj dodatkowe pola charakteryzujące cechy obywatela i metody je obsługujące. Na podstawie tak stworzonej klasy potomnej stwórz kolejne dwie klasy dziedziczące – jedną Kierowca, drugą Kierownik. Zastanów się nad polami cech i metodami, oraz… hierarchią dziedziczenia.