Poruszone zagadnienia
- Konstruktor domyślny
- Konstruktor z parametrami
- Koncepty
this()
orazsuper()
- Stos wywołań
Wstęp
Po pierwszej części javamPokaze || Konstruktory || Inżynieria obiektów w Javie || Część 1e fundamenty pod budowę obiektów masz już przygotowane, a podstawy inżynierii obiektów masz już w małym palcu. Dziś skupimy się na pewnym mechaniźmie, który jest powiązany z dziedziczeniem. Jak myślisz? Czy konstruktory mogą być dziedziczone?
Konstruktory - krótkie przypomnienie
Dla zasady przypomnijmy. Konstruktory to kluczowy element programowania obiektowego, pozwalający nam na tworzenie instancji klas, czyli obiektów, w oparciu o wcześniej zdefiniowane “szablony”, czyli klasy. Można to porównać do kreślenia planu budynku przed jego budową, tylko że w świecie Javy.
Czy konstruktory są dziedziczone?
Nie, nie są 😆 Tzn. nie w tradycyjnym sensie.
Jeśli klasa
Dog
dziedziczy po klasie Animal
, to nie dziedziczy automatycznie konstruktorów swojej nadklasy Animal
. Każda klasa dziedziczy cechy i metody, ale konstruktory nie są częścią dziedziczenia. Jednak w Javie istnieje mechanizm związany właśnie z konstruktorami w dziedziczeniu. Nazywa się wywoływaniem konstruktora nadklasy
.Istnieją pewne zasady, które regulują, kiedy i jak konstruktor nadklasy musi być wywoływany w konstruktorze podklasy.
class Animal { public Animal() { System.out.println("Konstruktor Animal"); } } class Dog extends Animal { public Dog() { super(); // Wywołanie konstruktora nadklasy jako pierwsza instrukcja System.out.println("Konstruktor Dog"); } } public class AnimalTest { public static void main(String[] args) { Dog dog = new Dog(); } }
W powyższym przykładzie, jeśli utworzysz instancję klasy
Dog
, zostaną wywołane oba konstruktory: najpierw konstruktor nadklasy Animal
(poprzez odwołanie używając słowa kluczowego super
), a następnie kod konstruktora podklasy Dog
.Zaraz sobie wszystko wyjaśnimy 😉
Konstruktory w drzewie dziedziczenia Javy
Weźmy na tapetę powyższy przykład. Wiemy już, że konstruktory to fundamentalne elementy służące do inicjalizacji obiektów. Gdy używamy słowa kluczowego
new
, rozpoczynamy proces tworzenia nowego obiektu.Podczas tworzenia każdego obiektu w Javie konstuktor musi być wywołany. Jawnie lub nie, ale musi. Bezapelacyjnie. Nawet w przypadku klas abstrakcyjnych, wywołujemy konstruktor, choć nie tworzymy bezpośrednio instancji obiektu. Dzieje się tak, ponieważ każda zmienna instancji musi mieć zawsze przypisaną jakąś wartość początkową. Nawet jeśli konstruktor nie przyjmuje argumentów, to
Java
nadaje domyślną wartość zmiennej instancji, na przykład null lub 0. Jest to ważne, ponieważ metody operują na tych zmiennych w trakcie działania programu.Podczas tworzenia obiektu w hierarchii dziedziczenia
Javy
konieczne jest zwrócenie uwagi na fakt, że konstuktor klasy dziedziczącej odwołuje się do konstruktora klasy nadrzędnej (poprzez użycie super()
. Wynika to z faktu, że obiekt klasy podrzędnej dziedziczy wszystkie zmienne instancji superklasy. Dodatkowo dziedziczy jej metody. W powyższym przykładzie obiekt typu Dog
dziedziczy wszystkie metody z klasy Animal
i klasy Object
. Wygląda to w następujący sposób:W kontekście dziedziczenia, ważne jest zrozumienie, że obiekt klasy podrzędnej (
subclass
) dziedziczy nie tylko metody, ale również w pewnym sensie “dziedziczy” konstruktory swoich klas nadrzędnych (superclass
). Jednak słowo “dziedziczy” jest tu skrótem myślowym.W hierarchii dziedziczenia, kiedy tworzysz obiekt klasy
Dog
wywoływany jest w tym momencie konstruktor klasy Animal
. W tej koncepcji główną rolę gra słowo kluczowe super
.Rekacja łańchuchowa wywoływania konstruktorów
Z poprzedniego schematu (
Object-Animal-Dog
) możesz wywnioskować, że jeśli wywołasz konstruktor klasy Dog
to rozpoczniesz reakcję łańcuchowego wywoływania konstruktorów, która zakończy swoje działanie w momencie dotarcia do konstruktora klasy Object
.Aby dobrze zobrazować ten mechanizm rzucimy okiem na tzn.
stos wywołań
. Jeśli jeszcze nie miałeś/aś styczności z tym zagadnieniem, przeczytaj poniższy wpis i wróć do mnie spowrotem. Jeśli temat nadal będzie dla Ciebie niezrozumiały, spróbuj pogawędzić z ChatemGPT
. Z pewnością się dogadacie 😉Proces inicjalizacji konstruktorów w kontekście dziedziczenia Javy przebiega wedle zasady LIFO (Last-In, First-Out). Wyglada to następująco:
- Kiedy tworzysz obiekt klasy podrzędnej
Dog
, proces rozpoczyna się od konstruktora właśnie tej klasy. Konstruktor klasyDog
będzie pierwszym elementem, który znajdzie się nastosie wywołań
- Następnie, wewnętrznie, konstruktor klasy
Dog
natychmiast wywołuje konstruktor swojej klasy nadrzędnejAnimal
, gdyż musimy zainicjować stan superklasy. Zatem konstruktor klasyAnimal
będzie kolejnym elementem nastosie
- Proces wywołań konstruktorów nadklasy będzie kontynuowany, aż dojdziesz do samej góry hierarchii, tj. konstruktora klasy bazowej
Object
📶 Można to zobrazować używając klocków lego
- Po wykonaniu się kodu konstruktora klasy bazowej
Object
, proces zmierza w drugą stronę, tzn. w dółstosu wywołań
, usuwając z niego kolejno klocki wszystkich konstruktorów zaczynając w kolejności od najwyżej położonego nastosie
- Gdy kod konstruktora klasy
Animal
zostanie wykonany, kompilator weźmie się za ostatni element nastosie
, czyli konstruktor klasy podrzędnejDog
, który w końcu może rozpocząć inicjalizację swojego własnego stanu
📶 I hopsa! Lecimy w drugą stronę 😆
Taki proces
łańchuchowego wywoływania konstruktorów
w drzewie dziedziczenia zapewnia poprawne wywołanie i inicjalizację wszystkich konstruktorów w hierarchii, co jest kluczowe dla prawidłowego działania obiektów w języku Java. Proste, prawda? 😉Czym się różni this()
od super()
?
W programowaniu obiektowym, konstruktory odgrywają kluczową rolę w inicjalizacji obiektów. Koncepcje
this()
i super()
pomagają w zarządzaniu tym procesem.Koncept this()
Odnosi się do bieżącego obiektu i jest używany, aby odwołać się do konstruktora bieżącej klasy. Możemy używać
this()
do wywoływania innych konstruktorów, ale tylko w tej samej klasieclass Animal { String species; // Konstruktor przyjmujący jeden argument public Animal(String species) { this.species = species; } // Konstruktor bezargumentowy public Animal() { this("Cat"); // Wywołanie powyższego konstruktora // Przypisujemy tutaj wartość "Cat" do zmiennej species } } public class AnimalTest { public static void main(String[] args) { Animal animal1 = new Animal("Dog"); System.out.println("Species of animal1: " + animal1.species); // output: Species of animal1: Dog Animal animal2 = new Animal(); System.out.println("Species of animal2: " + animal2.species); // output: Species of animal2: Cat } }
Koncept super()
Odnosi się do dziedziczenia i konstruktora klasy nadrzędnej (superklasy). Jest używany, gdy chcemy wywołać konstruktor klasy nadrzędnej w konstruktorze klasy dziedziczącej. To jest istotne, gdy chcemy zainicjować zmienne i stan z klasy nadrzędnej w klasie podrzędnej
class Animal { String species; public Animal(String species) { this.species = species; } } class Dog extends Animal { String name; public Dog(String name) { super("Golden Retriever"); // Wywołanie konstruktora klasy nadrzędnej this.name = name; } } public class Main { public static void main(String[] args) { Dog dog = new Dog("Rex"); // Przekazanie tylko imienia, bo gatunek już mamy System.out.println(dog.name + " is " + dog.species); // output: Rex is Golden Retriever } }
A co jeśli zapomnisz o super()
bohaterze?
Czy w klasie dziedziczącej zawsze trzeba “oficjalnie” wywoływać konstruktor klasy nadrzędnej używając słowa kluczowego
super
? Odpowiedź brzmi: Nie, nie trzeba 🥳W języku Java konstruktory nie są dziedziczone przez podklasy, co oznacza, że nie ma konieczości jawnego deklarownia konstruktorów w drzewie dziedziczenia w pewnych okolicznościach. W innych jednak może być to konieczne.
Oto najważniejsze koncepcje z przykładami klas
Animal
i Dog
:1.
Konstruktor domyślny - niejawnie deklarowany konstruktor
1.
Jeśli klasa nadrzędna
Animal
ma konstruktor domyślny (bezargumentowy), to klasa dziedzicząca Dog
nie musi jawnie deklarować konstruktora. W tym przypadku podczas wykonywania programu kompilator Java automatycznie doda konstruktor domyślny Animal
do klasy dziedziczącej Dog
class Animal { Animal() { // Konstruktor domyślny klasy nadrzędnej } } class Dog extends Animal { // Klasa Dog nie deklaruje własnego konstruktora // Ale kompilator automatycznie doda tutaj konstruktor domyślny klasy Animal } // Tworzenie obiektu klasy Dog Dog myDog = new Dog();
2. Konstruktor parametrowy - jawnie deklarowany konstruktor
Jeśli klasa nadrzędna
Animal
nie ma konstruktora domyślnego lub chcesz po swojemu dostosować inicjalizację klasy dziedziczącej, musisz jawnie zadeklarować konstruktor w klasie dziedziczącej. W takim przypadku musisz użyć słowa kluczowego super
w konstruktorze klasy dziedziczącej Dog
, aby wywołać odpowiedni konstruktor klasy nadrzędnej Animal
class Animal { Animal(int animalAge) { // Konstruktor z argumentem klasy nadrzędnej } } class Dog extends Animal { Dog(int dogAge) { super(dogAge); // Wywołanie konstruktora klasy nadrzędnej z argumentem } } // Tworzenie obiektu klasy Dog z przekazaniem wartości 3, jako argument Dog myDog = new Dog(3);
Bonus: kilka przykładów kodu
Przypadek 1:
Jeśli klasa nadrzędna
Animal
ma tylko konstruktor domyślny (bezargumentowy), to nie ma konieczności deklarowania konstruktora w klasie podrzędnej Dog
. Kompilator Java automatycznie doda konstruktor klasy nadrzędnej Animal
w klasie dziedziczącej Dog
public class Animal { String name; public Animal() { // Konstruktor domyślny klasy nadrzędnej Animal } } public class Dog extends Animal { // tutaj nie trzeba deklarować jawni konstruktora // kompilator załatwi sam sprawę } public static void main(String[] args) { Dog dog = new Dog(); dog.name = "Rex"; } }
Przypadek 2:
Jeśli klasa nadrzędna
Animal
ma tylko konstruktor bezargumentowy, ale w klasie podrzędnej Dog
chcesz mieć konstruktor z argumentami, musisz jawnie zadeklarować konstruktor w klasie podrzędnej Dog
i wywołać konstruktor klasy nadrzędnej przy użyciu super
public class Animal { public Animal() { // Konstruktor domyślny w klasie nadrzędnej Animal } } public class Dog extends Animal { String name; int age; public Dog(String name, int age) { super(); // Konieczne jawne wywołanie konstruktora klasy nadrzędnej Animal this.name = name; this.age = age; } public static void main(String[] args) { Dog dog = new Dog("Burek", 3); } }
Przypadek 3:
Jeśli klasa nadrzędna
Animal
ma tylko konstruktor z argumentami i nie ma konstruktora domyślnego, to w klasie podrzędnej Dog
musisz jawnie zadeklarować co najmniej jeden konstruktor i wywołać konstruktor klasy nadrzędnej Animal
za pomocą słowa kluczowego super
public class Animal { String name; public Animal(String name) { this.name = name; // Konstruktor z argumentem w klasie nadrzędnej Animal } } public class Dog extends Animal { int age; public Dog(String name, int age) { super(name); // Konieczne jawne wywołanie konstruktora klasy nadrzędnej Animal this.age = age; } } public class AnimalTest { public static void main(String[] args) { Dog dog = new Dog("Rex", 3); } }
Podsumowując
- Jeśli klasa nadrzędna ma tylko konstruktor domyślny (bezargumentowy), to nie musisz jawnie deklarować konstruktora w klasie podrzędnej. Klasa podrzędna automatycznie “odziedziczy” konstruktor domyślny od klasy nadrzędnej
- Jeśli klasa nadrzędna ma konstruktor z argumentami lub nie ma konstruktora bezargumentowego, to musisz zadeklarować odpowiedni konstruktor w klasie podrzędnej i wywołać konstruktor klasy nadrzędnej za pomocą
super()
w celu dostarczenia odpowiednich argumentów konstruktora klasy nadrzędnej
Użyteczne linki
Śledź mnie na LinkedIn
Newsletter
Jeśli masz jakieś sugestie lub pytania, proszę napisz do mnie wiadomość: kuba@javampokaze.pl