javamPokaze || Konstruktory || Inżynieria obiektów w Javie || Część 2
🛠️

javamPokaze || Konstruktory || Inżynieria obiektów w Javie || Część 2

notion image

Poruszone zagadnienia


  • Konstruktor domyślny
  • Konstruktor z parametrami
  • Koncepty this() oraz super()
  • Stos wywołań
 

Wstęp


Po pierwszej części
🛠️
javamPokaze || Konstruktory || Inżynieria obiektów w Javie || Część 1
e 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.
notion image
 

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


notion image
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:
notion image
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.
 
notion image
 

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:

  1. Kiedy tworzysz obiekt klasy podrzędnej Dog, proces rozpoczyna się od konstruktora właśnie tej klasy. Konstruktor klasy Dog będzie pierwszym elementem, który znajdzie się na stosie wywołań
  1. Następnie, wewnętrznie, konstruktor klasy Dog natychmiast wywołuje konstruktor swojej klasy nadrzędnej Animal, gdyż musimy zainicjować stan superklasy. Zatem konstruktor klasy Animal będzie kolejnym elementem na stosie
  1. 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

notion image
  1. 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 na stosie
  1. Gdy kod konstruktora klasy Animal zostanie wykonany, kompilator weźmie się za ostatni element na stosie, czyli konstruktor klasy podrzędnej Dog, który w końcu może rozpocząć inicjalizację swojego własnego stanu

    📶 I hopsa! Lecimy w drugą stronę 😆

    notion image
    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? 😉
     
    notion image
     

    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 klasie
    class 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 } }
     
     
    notion image
     

    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

    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);
     
    notion image
     

    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


    1. 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
    1. 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