Kurs Java

Klasy - Dziedziczenie (Inheritance)

Tytuł tego rozdziału brzmi pewnie niepokojąco, ale tak naprawdę za pojęciem dziedziczenia nie kryje się nic niezwykłego. Stykając się po raz pierwszy z tym terminem w programowaniu, mogą przychodzić nam do głowy skojarzenia z zasłyszanymi stwierdzeniami, że jakieś cechy wyglądu albo zachowania odziedziczyliśmy po naszych rodzicach, dziadkach itd. Nie zdajemy sobie sprawy z tego, że tak naprawdę rozumiejąc ten rodzaj dziedziczenia, mamy już podstawy do tego, aby zrozumieć czym jest dziedziczenie w Javie.

Wiemy już, że tworząc program obiektowy tworzymy najpierw klasy, które są szablonem (wzorem) do tworzenia obiektów na ich podstawie. Klasy te najczęściej reprezentują podmiot faktycznie istniejący w rzeczywistości (dokument, film, osoba...) i mają swoje pola, w których są przechowywane cechy (własności) tych podmiotów (np. nazwa dokumentu, tytuł filmu...). Mamy też metody, które definiują nam działania jakie będą wykonywane przez dany obiekt.

Wracając teraz do tematu dziedziczenia musimy wiedzieć, że pola i metody niekoniecznie muszą być definiowane tylko w danej klasie, którą stworzyliśmy. Możemy je również odziedziczyć z innej klasy. Przejdźmy do przykładu...

Słowo kluczowe extends

Mamy dwie klasy. Pierwsza z nich, to znana już nam klasa Item (tutaj nieco okrojona). Druga natomiast, to całkiem nowa klasa o nazwie DocumentItem. Tak więc pierwsza klasa reprezentuje "jakiś tam item w programie", a druga także przedstawia item, ale będący czymś więcej niż tylko zwykłym, przeciętnym itemem. Nasz item jest tutaj dokumentem...ale zaraz zaraz...dlaczego właściwie twierdzimy, że DocumentItem też jest itemem? Czy dlatego, że w nazwie tej klasy zawarte jest słowo Item? Otóż nie, oczywiście że nie. Stwierdzamy, że klasa DocumentItem również jest itemem, ponieważ dziedziczy z klasy Item!

Widzimy, że klasa dziedzicząca z innej klasy musi po swojej nazwie mieć wpisane słowo kluczowe extends, poprzedzające nazwę klasy, z której dziedziczy. Inaczej mówiąc klasa DocumentItem rozszerza klasę Item, co zresztą jest dosłownym tłumaczeniem słowa extends.
public class Item {


} 
public class DocumentItem extends Item {


} 
Tylko po co nam takie dziedziczenie? Jak narazie mamy dwie puste klasy i nic ciekawego się tam nie dzieje. Dodajmy więc teraz do naszego przykładu trochę kodu i zobaczmy co faktycznie daje nam dziedziczenie.

Dostęp do pól i metod

Mamy wiec klasę Item, która posiada jedno pole, dwie metody i bezparametrowy konstruktor domyślny (jeśli nie wiecie o jakim konstruktorze mowa, to polecamy powrót do początku rozdziału Tworzenie własnych obiektów). Jak już wiecie z poprzednich rozdziałów, możemy stworzyć obiekt tej klasy, ustawić wartość pola z nazwą za pomocą metody setName, a później pobrać tą nazwę z obiektu w celu wydrukowania jej na konsoli:
public class Item {

    String name;       
    
    public void setName(String name) {
        this.name = name;
    }    
    
    public String getName() {
    	return name;
    }        
} 
public class Start {

    public static void main(String[] args) {
    
        Item item = new Item();
        item.setName("Appa Item 1");
        
        System.out.println(item.getName());
    }
}
Oczywiście po wykonaniu programu na konsoli zostanie wydrukowany tekst "Appa Item 1" i to będzie zgodne z naszymi oczekiwaniami. W tym momencie przyszedł czas na wykorzystanie naszej nowej klasy DocumentItem. Dodamy do niej jedno nowe pole type oraz metody umożliwiające ustawienie i pobranie wartości w tym polu. Następnie zmodyfikujemy nieco naszą metodę main:
public class DocumentItem extends Item {

    String type; 
    
    public void setType(String type) {
        this.type = type;
    }    
    
    public String getType() {
        return type;
    }        
} 
public class Start {

    public static void main(String[] args) {
    
        Item item = new Item();
        item.setName("Appa Item 1");
        
        System.out.println(item.getName());
        
        DocumentItem item2 = new DocumentItem();
        item2.setName("Appa Item 2");
        item2.setType("Appa Type 1");
        
        System.out.println(item2.getName());
        System.out.println(item2.getType());        
        
    }
}
Klasa po lewej to zwykła, regularna klasa java, której najbardziej interesującą umiejętnością jest to, że rozszerza klasę Item. A skoro ją rozszerza, to przy okazji dziedziczy jej pola i metody. Nie widać tego co prawda tak jawnie w kodzie, ale patrząc na taki zapis zawsze powinniśmy mieć swiadomość, że to co znajduje się w klasie zdefiniowanej po słowie extends, jest również bezpośrednio dostępne w ramach klasy roszerzającej, tak jakby było jawnie wpisane do tej klasy (pomijamy tu kwestię modyfikatorów dostępu, aby nie zaciemniać - niech wszystkie metody mają modyfikatory public, a pola niech nie mają żadnego modyfikatora, czyli tak jak w naszym przykładzie).

Uzbrojeni w tą wiedzę możemy teraz przystąpić do analizy kodu klasy Start. Widzimy, że do wcześniej istniejącego kodu dopisaliśmy kilka linijek. W pierwszej z nich dzieje sie coś ciekawego. Tworzymy tam obiekt klasy DocumentItem i przypisujemy go do zmiennej typu DocumentItem. Czyli jak narazie wszystko wygląda tak, jakbyśmy się tego spodziewali. Następnie ustawiamy w obiekcie item nazwę za pomocą metody setName. I tutaj pojawia się pierwsza niejasność. Wiemy co prawda, że metoda taka jest dostępna w klasie Item, ale czy możemy jej użyć w tym miejscu jeśli właściwie korzystamy tutaj z obiektu klasy DocumentItem? Oczywiście, że tak. Mamy dostęp do tej metody, więc możemy jej użyć. Wszystko zgodnie z zasadą mówiącą o dostępności metod i pól (wytłuszczoną w poprzednim akapicie).

Dodajmy jeszcze, że tak samo jak ustawiliśmy nazwę korzystając z metody klasy Item, tak samo linijkę niżej ustawiamy wartość pola type znajdującego się w klasie DocumentItem. Wszystkie te dane "lądują" w jednym obiekcie. Dlatego też w ostatnich dwóch linijkach możemy je pobrać i wydrukować.

Rozszerzam Cię więc jestem Tobą

Spójrzmy jeszcze raz na ten kod, ale teraz w delikatnie zmienionej formie. Jedyną rzeczą, na którą teraz chcemy zwrócić uwagę jest tworzenie obiektu klasy DocumentItem. Wygląda to podobnie jak poprzednio, ale tym razem tak stworzony obiekt przypisujemy do zmiennej o typie Item:
public class DocumentItem extends Item {

    String type; 
    
    public void setType(String type) {
        this.type = type;
    }    
    
    public String getType() {
        return type;
    }        
} 
public class Start {

    public static void main(String[] args) {
    
        Item item = new Item();
        item.setName("Appa Item 1");
        
        System.out.println(item.getName());
        
        Item item2 = new DocumentItem();
        item2.setName("Appa Item 2");
        
        System.out.println(item2.getName());           
    }
}
Można teraz zadać pytanie: "co tu się właśnie wydarzyło...?!". Czy to w ogóle jest poprawnie działający kod? Odpowiadamy. Tak to jest poprawnie działający kod i od razu przystępujemy do tłumaczenia.

Skoro nasz klasa DocumentItem dziedziczy z klasy Item, to poza tym że dziedziczy niejawnie pola i metody, to również obiekt tej klasy staje się obiektem tego samego typu, co klasa z której się wywodzi. Pójdźmy teraz jeszcze o krok dalej i stwórzmy klasę TextDocumentItem, która będzie dziedziczyła z klasy DocumentItem:
public class TextDocumentItem extends DocumentItem {

} 
Taki zapis mówi nam nie tylko to, że klasa TextDocumentItem rozszerza DocumentItem, ale też że obiekt tej klasy jest typu DocumentItem. I teraz skoro klasa DocumentItem rozszerza klasę Item i jej obiekt jest typu Item, to obiekt klasy TextDocumentItem również staje się obiektem typu Item.
Dziedziczenie klas
Mając tak zdefiniowane klasy możemy powiedzieć, że:
  • Każdy obiekt tworzony za pomocą: new Item jest obiektem typu Item.
  • Każdy obiekt tworzony za pomocą: new DocumentItem jest obiektem typu DocumentItem oraz Item.
  • Każdy obiekt tworzony za pomocą: new TextDocumentItem jest obiektem typu TextDocumentItem oraz DocumentItem oraz Item.

Klasa rozszerzająca vs klasa rozszerzana

Bardzo istotne jest to aby zrozumieć, że taka zależność działa tylko w jedną stronę. Czyli nie możemy powiedzieć, że obiekt klasy Item jest obiektem typu DocumentItem. Innymi słowy, zapis:
Item item = new DocumentItem();
oznacza, że obiekt jest tylko i wyłącznie obiektem typu Item i nie mamy tutaj bezpośredniego dostępu do pól i metod klasy DocumentItem. Z tego powodu metody setType i getType nie są widoczne w obiekcie typu Item. Taki kod się nie skompiluje:
item.setType("Appa Type 1");
Oczywiście my jako programiści doskonale wiemy, że pod obiektem item kryje się instancja klasy DocumentItem i na upartego możemy wykonać tak zwane rzutowanie na obiekt typu DocumentItem.
((DocumentItem) item).setType("Appa Type 1");
Jednak coś takiego nie jest zalecane. Raz, że zmniejsza czytelność kodu, a dwa że uderza bezpośrednio w wykonany przez nas projekt klas. Z jakiegoś powodu klasa Item przecież nie posiada dostępu do metod setType i getType. Może tak właśnie ma być, że programista projektując taką klasę nie chciał udostępniać tej metody dla ogólnego typu Item, tylko jego zamysłem było użycie w tym celu dedykowanej klasy DocumentItem.

Taka jest powszechnie stosowana praktyka, że ta część metod, która ma być dostępna dla wszystkich klas dziedziczących jest wstawiana do klasy nadrzędnej (takiej jak Item), a te metody które mają być dedykowane tylko konkretnej klasie dziedziczącej są dostępne bezpośrednio w tej klasie (takiej jak u nas DocumentItem).

Podsumowując takie rzutowanie jest trochę takim hakiem stosowanym przez programistę, aby złamać zaprojektowany wcześniej schemat.

Klasa Object

Na koniec pytanie. Czy klasa Item dziedziczy z jakiejś klasy? Patrząc to, że w jej definicji nie ma słowa kluczowego extends, odpowiedź wydaje się prosta - nie dziedziczy. Jednak to nie jest całkiem dobra odpowiedź. Wszystko sprowadza się do stwierdzenia jawnie/niejawnie. Jawnie, czyli z użyciem extends. Niejawnie, to znaczy, że z definicji każda klasa w Javie dziedziczy z klasy o nazwie Object, a skoro mówimy, że każda klasa, to znaczy, że klasa Item również.

Co nam to daje?

Po pierwsze możemy być pewni, że obiekt każdej naszej klasy zawsze będzie obiektem typu Object, co jest dosadnym dowodem stwierdzenia, że w Javie wszystko jest obiektem. Po drugie oznacza to, że wszystkie klasy w Javie dziedziczą możliwości klasy Object. W tym przede wszystkim dziedziczą kilka kluczowych metod, z których najbardziej popularne to: toString, equals i hashCode. Już niebawem poświęcimy im nieco więcej czasu.

Przykład w programie

Teraz zobaczmy jak kod naszego programu wygląda w IDE:
public class Start {

    public static void main(String[] args) {
    
        Item item = new Item();
        item.setName("Appa Item 1");
        
        System.out.println(item.getName());
        
        Item item2 = new DocumentItem();
        item2.setName("Appa Item 2");
        
        System.out.println(item2.getName());           
    }
}
Program zawierający dziedziczenie
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
kursjava@javappa.com

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE