Dziedziczenie to ponowne użycie typu „jest”
Dziedziczenie pozwala nowej klasie budować na istniejącej. Nowa klasa, czyli podklasa, automatycznie dostaje pola i metody nadklasy, a potem dodaje lub zmienia to, czego potrzebuje. Sięgasz po nie, gdy jeden typ jest bardziej szczegółowym rodzajem innego: Dog jest Animal, a SavingsAccount jest BankAccount.
Zapisujesz je słowem kluczowym extends. Wszystko, co w rodzicu jest public lub protected, jest dostępne w klasie potomnej bez przepisywania.
Dog nigdy nie deklaruje name ani eat(), a mimo to ma jedno i drugie. W tym właśnie rzecz: wspólne zachowanie jest w jednym miejscu.
super: sięganie do rodzica
Konstruktor podklasy musi zadbać o to, by rodzic został przygotowany jako pierwszy. Robisz to przez super(...), które wywołuje konstruktor rodzica i musi być pierwszą instrukcją w konstruktorze podklasy. Jeśli je pominiesz, Java po cichu wstawi wywołanie bezargumentowego konstruktora rodzica, a jeśli rodzic takiego konstruktora nie ma, kod się nie skompiluje.
Wynik pokazuje, że konstruktor rodzica wykonuje się przed ciałem konstruktora dziecka: tworzenie obiektu przebiega od szczytu hierarchii w dół.
Nadpisywanie metod
Podklasa może zastąpić odziedziczoną metodę, definiując ją ponownie z tą samą sygnaturą. To nadpisywanie (overriding) i zawsze warto oznaczać je adnotacją @Override. Adnotacja nie jest wymagana, ale sprawia, że kompilator sprawdza, czy faktycznie trafiłeś w metodę rodzica. Wyłapuje literówki, takie jak tostring() zamiast toString(), które inaczej po cichu utworzyłyby zupełnie nową metodę.
Choć tablica ma typ Animal, każdy element wykonuje własne speak(). Java wybiera metodę na podstawie prawdziwego obiektu w czasie działania, a nie zadeklarowanego typu zmiennej. To fundament polimorfizmu.
Wywołanie wersji rodzica przez super.method()
Nadpisanie nie musi wyrzucać pracy rodzica. Użyj super.method(), aby uruchomić odziedziczoną wersję, a potem coś do niej dodać:
Bez super. wywołanie log wewnątrz TimestampLogger.log wywoływałoby samo siebie i rekurencja trwałaby bez końca. super. jawnie oznacza „wersję rodzica”.
Odziedziczone pola i dostęp
Podklasa widzi składowe rodzica public i protected, ale nie private. Pola private nadal istnieją w obiekcie i własne metody rodzica mogą z nich korzystać, ale podklasa nie może się do nich bezpośrednio odwołać. Użyj protected, jednego z modyfikatorów dostępu Javy, gdy chcesz dać podklasom dostęp, a jednocześnie ukryć składową przed niepowiązanym kodem.
class Base {
private int secret; // niewidoczne dla podklas
protected int shared; // widoczne dla podklas
}
class Derived extends Base {
void demo() {
shared = 5; // OK
// secret = 5; // błąd kompilacji: private w Base
}
}
Dlatego też konstruktor podklasy często musi wywołać super(...): to jedyny sposób na zainicjalizowanie prywatnego stanu rodzica.
Blokowanie dziedziczenia przez final
Czasem klasy w ogóle nie powinno się rozszerzać: String jest final właśnie z tego powodu. Oznaczenie klasy jako final zabrania tworzenia podklas, a oznaczenie metody jako final zabrania jej nadpisywania, choć samą klasę nadal można rozszerzać.
final class Constants { } // nie można po niej dziedziczyć
class Config {
final void load() { } // podklasy mogą rozszerzać Config
// ale nie mogą nadpisać load()
}
Sięgaj po final, gdy zachowanie klasy musi być zagwarantowane i niezmienne w całym programie. To świadomy sygnał „nie rozszerzać”, a nie ustawienie domyślne.
Częsta pułapka: wybierz kompozycję, gdy to nie relacja „jest”
Dziedziczenie kusi, bo pozwala ponownie wykorzystać kod, ale mocno wiąże dziecko z rodzicem. Jeśli relacja nie jest prawdziwym „jest”, na przykład Car, który po prostu potrzebuje Engine, nie pisz Car extends Engine. Samochód ma silnik, a nie jest silnikiem. Zamodeluj to polem (kompozycją):
class Car {
private Engine engine = new Engine(); // Car MA Engine
void start() { engine.ignite(); }
}
Używaj dziedziczenia tylko wtedy, gdy podklasa naprawdę jest wyspecjalizowaną formą nadklasy i chcesz zarówno odziedziczyć jej zachowanie, jak i móc ją nią zastępować.
Dalej: interfejsy
Dziedziczenie przez extends daje jednego rodzica i wspólną implementację. Klasa może jednak rozszerzać tylko jedną klasę, więc jak dać niepowiązanym klasom wspólną zdolność? Do tego służą interfejsy: kontrakt, który może implementować wiele klas. To temat następnej strony.
Najczęściej zadawane pytania
Czym jest dziedziczenie w Javie?
Dziedziczenie pozwala jednej klasie (podklasie) ponownie wykorzystać pola i metody innej klasy (nadklasy) za pomocą słowa kluczowego extends. Podklasa automatycznie dostaje publiczne i chronione składowe rodzica, może dodawać nowe albo zastępować odziedziczone zachowanie przez nadpisywanie. Dziedziczenie modeluje relację „jest”: Dog jest Animal.
Do czego służy słowo kluczowe super w Javie?
super odnosi się do klasy nadrzędnej. super(...) w konstruktorze wywołuje konstruktor rodzica (i musi być pierwszą instrukcją), a super.method() wywołuje wersję rodzica metody, którą nadpisałeś. Dzięki temu podklasa może budować na logice rodzica, zamiast całkowicie ją zastępować.
Czym różni się nadpisywanie od przeciążania w Javie?
Nadpisywanie (overriding) definiuje odziedziczoną metodę na nowo w podklasie z tą samą sygnaturą i zmienia jej zachowanie: oznacz ją adnotacją @Override. Przeciążanie (overloading) definiuje w tej samej klasie kilka metod o tej samej nazwie, ale z różnymi listami parametrów. Nadpisywanie dotyczy dziedziczenia i wyboru metody w czasie działania, a przeciążanie to po prostu dwie metody, które mają tę samą nazwę.