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

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

notion image

Poruszone zagadnienia


  • Konstruktory
  • Rodzaje konstruktorów
  • Konstruktor domyślny
  • Przeciążenie konstruktorów
  • Constructor chaining

Wstęp


Dziś pobawimy się trochę w inżynierów budownictwa. Nie będziemy projektować mostów, wieżowców, czy innych budowli. Skupimy się na stawianiu konstrukcji o zupełnie innym charakterze, a mianowicie na na schemacie tworzenia obiektów w Javie.
Nie musimy się martwić o przepisy budowlane, ani o materiały. Wszystko zadzieje się w magiczny sposób. Jedyne dyrektywy, jakie będziemy potrzebowali zgłębić, to zasady tworzenia tzn. konstruktorów, czyli tworów podobnych do metod w Javie, których znajomość musimy opanować , aby jakiś doświadczony inżynier oprogramowania nie oskarżył nas o fuszerkę podczas budowania naszych obiektów.
Konstruktory stanowią kluczowy element programowania obiektowego, umożliwiając nam tworzenie instancji klas, czyli obiektów, zbudowanych na podstawie wcześniej zdefiniowanych szablonów - klas.
W dzisiejszym wpisie zgłębimy tajniki tego ważnego zagadnienia i poznamy różne aspekty pracy z konstruktorami w Javie. Dowiemy się, jakie są ich rodzaje konstruktorów, jak je tworzyć, czym się różnią od metod oraz jakie mogą mieć zastosowanie w naszych projektach.
Zanim jednak wkroczymy na plac budowy, spójrzmy na podstawy teorii konstruktorów, co pozwoli nam na wylać beton pod solidne fundamenty.
¡Vamos! 🇪🇸
 
notion image
 

Obiekty


Najpierw przypomnimy sobie podstawy projektowania obiektowego. Jeśli potrzebujesz sobie powtórzyć ten temat, opisałem to zagadnienie tutaj:
🛠️
javamPokaze || Podstawy programowania obiektowego
Zasada tworzenia obiektów jest prosta:
public class Cat { String name; } public class TestCat{ public static void main(String [] args){ Cat kitty = new Cat(); // Tworzymy nowy obiekt typu Cat //Etapy: 1 3 2 kitty.name = "Tom"; } }
Etap 1. Deklaracja
Prosimy wirtualną maszynę Javy (JVM), aby zarezerowała dla nas miejsce w pamieci (na tzw. Stosie) dla zmiennej referencyjnej typu Cat o nazwie kitty.
Etap 2. Tworzenie Obiektu
Wnioskujemy u JVM o rezerwacje miejsca na tzw. Stercie dla nowego obiektu , który tworzymy poprzez słowo kluczowe new
Etap 3. Inicjalizacja (przypisanie wartości)
Wykorzystując znak równości przypisujemy nowy obiekt do zmiennej referencyjnej o nazwie kitty
 
Ok, stworzyliśmy nowego kotka. Nadaliśmy mu imię Tom. Ale czy zastanawiałeś się kiedykolwiek, jaki mechanizm stoi za powyższą operacją?
 
notion image
 

Konstruktory


Najpierw rzućmy okiem na ten sam kod, jednak z pewnymi zmianami:
public class Cat { String name; public Cat(){ // to jest konstruktor // miejsce na kod } } public class TestCat{ public static void main(String [] args){ Cat kitty = new Cat(); // wywołanie kontruktora poprzez użycie słowa kluczowego "new" kitty.name = "Tom"; } }
Jak pewnie zauważyłeś w klasie Cat pojawił się nowy fragment kodu public Cat() , który wyglada jak metoda. To jest właśnie ten słynny konstruktor.
Konstruktor to specjalny blok kodu, który służy do tworzenia obiektów. Jest bardzo podobny jest do metody, z tą różnicą, że metoda w Javie posiada typ zwracany, a konstruktor nie. Mechanizm, który pozwala tworzyć KAŻDY obiekt w Javie nazywa się wywoływaniem konstruktów
Konstruktor znajduje się zawsze w klasie, której instancję (obiekt) tworzy. Kiedy chcesz stworzyć dany obiekt, wykorzystujesz wspomniane wywołanie konstruktora. Wywołanie następuje w momencie, kiedy użyjesz słowa kluczowego new (w naszym przypadku new Cat(). Wirtulana maszyna Javy znajduje wtedy klasę, w której konstruktor się znajduje i wywołuje go (tak jak zwykłą metodę) wykonując kod w nim zawarty.

W dużym skrócie - jeśli tworzysz nowy obiekt, to zawsze automatycznie wywołujesz konstruktor, czyli narzędzie służące do kreowania tego obiektu. Nawet jeśli o tym nie wiesz, to nieświadomie go używasz (ale o tym zaraz)😜

WAŻNE:

⚠️ Konstruktor nie jest metodą (nie posiada typu zwracanego)

⚠️ Oznaczenie konstruktora jako public jest najbardziej liberalnym podejściem, ponieważ umożliwia dostęp do konstruktora z dowolnej klasy, również spoza pakietu

⚠️ Nazwa konstruktora musi być identyczna z nazwą klasy

 
notion image
 

Rodzaje konstruktorów / jak je tworzyć?


Główą funkcją konstruktora jest utworzenie obiektu oraz sprawne zainicjowanie zmiennych instancji. Bez niego nie stworzymy żadnego obiektu. Wyróżniamy dwa rodzaje konstruktorów:
  • Konstruktory bezparametrowe (domyślne)
  • Konstruktory parametrowe
 

🏗️ Konstruktor bezparametrowy (domyślny)


  • To pierwszy rodzaj konstruktora
  • Nie przyjmuje parametrów
  • Przypisuje wartości domyślne zmiannym instancji tworzonego obiektu
  • Wartości domyślne mogą być przypisane na dwa sposoby:
    • Konstruktor bez ciała

      public class Car{ String brand; int price; public Car(){ // Puste ciało } // Wywołanie konstruktora: // Spowoduje przypisanie zmiennym instancji wartości domyślnych: // int → 0, boolean → false, float → 0.0, String = null }

      Konstruktor z ciałem

      public class Car{ String brand; int price; public Car(){ brand = "Ford"; // ustawione wartości domyślne w ciele konstruktora price = 20000; } // Wywołanie konstruktora: // Spowoduje, że powyższe wartości będą przypisane do // zmiennych instancji każdego obiektu, który stworzymy w przyszłości // każdy obiekt będzie Fordem w cenie 20000 PLN }
 

🏗️ Konstruktor z parametrami


  • To drugi rodzaj konstruktora
  • Przyjmuje konkretne parametry
  • Wartości przekazane do parametrów konstruktora, jako agrumenty będą przypisane do zmiennych instanacji obiektu, który tworzymy
  • Rozważmy dwa przypadki:

Zmianne instancji oraz parametry konstruktora różnią się nazwą

public class Car{ String brand; int price; public Car(String marka, int cena ){ brand = marka; // przypisanie wartości marka do zmiennej instancji brand price = cena ; // przypisanie wartości cena do zmiennej instancji price } } public CarTest{ public static void main(String [] args){ Car combi = new Car("Audi", 160000); // Wywołaliśmy konstruktor poprzez słowo kluczowe "new" // Jako argumenty przekazaliśmy do konstruktora wartości "Audi" oraz 160000 // Konstruktor przypisał zmiennym instancji te wartości: // brand = "Audi"; // price = 160000; // W tym przypadku rozróżniamy parametry konstruktora od zmiennych instancji po nazwie } }

Zmianne instancji oraz parametry konstruktora mają takie same nazwy

public class Car{ String brand; int prices; public Car(String brand, int price){ // takie same nazwy (brand / brand) this.brand = brand; this.price = price; } // W tym przypadku rozróżniamy parametry konstruktora od zmiennych instancji poprzez: // this.brand = brand // Możemy to przetłumaczyć na coś takiego: /* Hej kompilator, weź tą "this." zmienną instancji brand w klasie Car i przypisz jej wartość brand, którą przekazuję jako argumenet podczas wywoływania konstruktora w metodzie public static void main ("Audi") */ } public CarTest{ public static void main(String [] args){ Car combi = new Car("Audi", 160000); // Wywołaliśmy konstruktor, do którego przekazaliśmy wartości "Audi" oraz 160000 // Konstruktor przypisze te wartości zmiennym instancji obiektu poprzez słowo kluczowe: // "this." // this.brand = "Audi"; // this.price = 160000; } }

WAŻNE:

⚠️ Każda klasa posiada konstruktor, nawet jeśli Ty sam go w tej klasie nie umieścisz. Jeśli nie stworzysz swojego konstruktora, kompilator z automatu sam stworzy w klasie “niewidzialny” konstruktor domyślny. Dowodem na to jest fakt, że nie ma możliwości stworzenia obiektu bez parametrów. Zostaną mu automatycznie przypisane wartości domyślne (0, 0.0, false, lub null)

⚠️ Jeśli stworzysz konstruktor, który przyjmuje argumenty, kompilator odpuści sobie utworzenie konstruktora domyślnego, bo stwierdzi, że go nie potrzebujesz

⚠️ Dlatego jeśli potrzebujesz zarówno konstruktora z argumentami, jaki i bez argumentów, to ten drugi będziesz musiał stworzyć dodatkowo sam

 
notion image
 

Przeciążenie konstruktorów


W każdej klasie możemy mieć zarówno jeden, jak i kilka konstruktorów. Aby utworzyć kilka różnych konstruktorów, muszą one różnić się parametrami (ilością, typami, lub kolejnością w nawiasie). Aby mieć wiele konstruktorów z różnymi listami parametrów, musisz skorzystać z techniki przeciążania konstruktorów (Overload).
 
Przeciążanie konstruktorów umożliwia tworzenie wielu konstruktorów z różną konfiguracją parametrów, co zapewnia elastyczność podczas tworzenia obiektów danej klasy. Dzięki temu programista może łatwo tworzyć obiekty tej klasy, używając konstruktora odpowiadającego konkretnym potrzebom. Jest to swego rodzaju metoda na tworzenie obiektów szytych na miarę. Dostosowujemy stan obiektu do naszych potrzeb.
 
Więcej na temat przeciążenia znajdziesz w moim wcześniejszym wpisie:
 
Weźmy w takim razie na bęben taki przykład:
public class Smartphone{ String brand; int price; int megaPixels; // Konstruktor domyślny (tworzy obiekt i przypisuje zmiennymi instacji wartości // domyślne (brand = null, price = 0, megaPixels = 0) class Smartphone(){ } // Konstruktor z 3 parametrami (które przekażemy podczas wywoływania konstruktora) class Smartphone(String brand, int price, int megaPixels){ this.brand = brand; this.price = price; this.megaPixels = megaPixels } // Konstruktor z 2 parametrami (które przekażemy podczas wywoływania konstruktora) class Smartphone(String brand, int price){ this.brand = brand; this.price = price; } // Ten sam Konstruktor z 2 parametrami co wyżej, ale z inną kolejnością zmiennych w // nawiasie class Smartphone(int price, String brand){ this.price = price; this.brand = brand; } // Konstruktor 1 parametrem (które przekażemy podczas wywoływania konstruktora) class Smartphone(String brand){ this.brand = brand; } // Konstruktor 2 parametrami i 1 bezpośrednio przypisaną wartością domyślną "iPhone" class Smartphone(int price, int megaPixels){ String = "iPhone" this.price = price; this.megaPixels = megaPixels } }
Stwórzmy zatem obiekty, wywołując odpowiednie konstruktory. Zastanawiasz się pewnie, jak kompilator ma się zorientować, który konkretnie konstruktor planujesz wywołać? Dowie się tego po tym, jaką ilość argumentów i w jakiej kolejności przekażesz do konstruktora.
Dodatkowym atutem konstruktorów jest to, że możemy zainicjować wszystkie zmienne instancji w 1 linijce kodu, ponieważ wszystkie wartości zostaną przekazane na raz (w nawiasie konstruktora)
public class TestSmartphone{ public static void main(String [] args){ // wywołanie konstruktora domyślnego (zmienne mają wartości domyślne) Smartphone phone = new Smartphone(); // wywołanie konstruktora z 3 parametrami (zmiennym przypisujemy nasze wartości) Smartphone phone = new Smartphone("LG", 1500, 10); // wywołanie konstruktora z 2 parametrami (zmiennym przypisujemy nasze wartości) Smartphone phone = new Smartphone("LG", 2500); // wywołanie konstruktora z 2 parametrami (zmiennym przypisujemy nasze wartości) Smartphone phone = new Smartphone(2500, "LG"); // wywołanie konstruktora z 1 parametrem (zmiennej przypisujemy naszą wartość) Smartphone phone = new Smartphone("Xiaomi"); // wywołanie konstruktora z 2 parametrami (zmiennym przypisujemy tylko 2 wartości // ponieważ zmienna brand została w konstrukorze zainicjowana jako brand = "iPhone") Smartphone phone = new Smartphone("5000", 20); } }

Ważne:

⚠️ Kiedy tworzysz swoje własne konstruktory, oprócz nich staraj się umieszczać także konstruktor domyślny. Może to ułatwić przyszłą ewolucję kodu

⚠️ Aby mieć kilka konstruktorów o tej samej nazwie w jednej klasie, należy zastosować przeciążanie konstruktorów, czyli definiować konstruktory z różnymi listami parametrów (ilością, typami lub kolejnością)

 
notion image
 

Wywołanie konstruktora z innego konstuktora


No teraz się zacznie robić ciekawie 😄 Tak, możemy wywołać jeden konstruktor z drugiego, a drugi z trzeciego, trzeci z czwartego… Miło, nie? 🤯
Wywoływanie konstruktora przy pomocy słowa kluczowego this jest techniką nazywaną "konstruktorem łańcuchowym" (ang. constructor chaining). Pozwala ona na uniknięcie powtarzania kodu w kolejnych konstruktorach, poprzez przekierowanie wywołania do innego konstruktora.
Kiedy w danym konstruktorze użyjemy słowo kluczowe this(...) z odpowiednimi argumentami, oznacza to, że chcemy wywołać inny konstruktor, w którym mamy możliwość niektóre ustawić paramametry wedle naszych upodobań, a resztę pozostawiamy w spokoju. Wywołując konstruktor w innym konstruktorze, unikamy konieczności powtarzania podobnego kodu kilka razy. To oszczędzić czasu.
Popatrzmy na ten przykład:
class Car{ //Nasze zmienne instancji String brand; int productionAge; double przebieg; // Konstruktor domyślny (bezargumentowy) wywołuje konstruktor Car(String brand) // z 1 argumentem. Tutaj zmiennej model przypisujemy wartość domyślną "BMW" : public Car(){ this("BMW"); } // Konstruktor z 1 argumentem wywołuje konstruktor Car(String brand, int productionAge) // z 2 argumentami. Tutaj zmiennej int przypisujemy wartość domyślną 2005 : public Car(String brand){ this(brand, 2005) } // Konstruktor z 2 argumentami wywołuje Car(String brand, int productionAge, double przebieg) // z 3 argumentami. Tutaj zmiennej double przypisujemy wartość domyślną 200.000 : public Car(String brand, int productionAge){ this(brand, productionAge, 200.000); } // Konstruktor z 3 argumentami, w którym ustawiamy wszystkie wartości // Od niego zaczynamy zabawę public Car(String brand, int productionAge, double przebieg){ this.brand = brand; this.productionAge = productionAge; this.przebieg = przebieg; } }
Po co to robić? Przykładowo, jeśli potrzebujemy stworzyć kilka podobnych samochodów klasy Car, które łączy jedna wspólna cecha (np. kilka samochodów o różnych markach, różnym roku produkcji ,ALE TAKIM SAMYM PRZEBIEGU). Wtedy takie rozwiązanie ma sens.
public CarTest{ public static void main(String [] args){ // Tworzymy pierwszy obiekt wywołując konstruktor z 3 parametrami // Ustawimy zmiennym instancji konkretne wartości przekazując je jako argumenty Car car1 = new Car("BMW", 2005, 54000); // Tworzymy 2 obiekty wywołując konstruktor z 2 parametrami // W tym konstruktorze wywołany został konstruktor z 3 parametrami // następnie zmieniamy JEDYNIE markę i rok produkcji // przebieg zostaje taki sam (54000) Car car2 = new Car("Audi", 2010); Car car2 = new Car("Toyota", 1998); } }
 

Słownik


  • Konstruktor - specjalny blok kodu, służący do tworzenia obiektów. Jest bardzo podobny do metody, z tą różnicą, że metoda posiada typ zwracany, a konstruktor nie. Główną funkcją konstruktora jest tworzenie obiektó oraz sprawne zainicjowanie zmiennych instancji. Mechanizm przy pomocy którego powstają obiekty nazywa się wywoływaniem konstruktorów.
  • Konstruktory bezparametrowe (domyślne) - przypisują zmiennym instancji wartości domyślne. Wartości te możemy ustawić sami w ciele konstruktora. Jeśli tego nie zrobimy, kompilator sam przypisze zmiennym instancji wartości domyślne (0, 0.0, null lub false, w zależności od typu zmiennych)
  • Konstruktory parametrowe - zmiennym istancji zostaną przypisane wartości przekazane przez nas jako argumenty do konstruktora
  • Przeciążenie konstruktorów - koncepcja umożliwiajaca tworzenie jednocześnie wielu konstruktorów w tej samej klasie. Konstuktory te muszą różnić się konfiguracją parametrów. Przeciążenie konstruktorów zapewnia elastyczność podczas późniejszego tworzenia obiektów. Aby utworzyć kilka różnych konstruktorów, muszą one różnić się parametrami (ilością, typami, lub kolejnością w nawiasie).
  • Constructor chaining - technika pozwalająca na wywołanie konstruktorów w innych konstruktorach przy użyciu słowa kluczowego “this.” . Celem takiego działania jest uniknięcie powtarzania kodu, jeśli w nowym konstruktorze potrzebujemy zapożyczyć funkcjonalności z poprzedniego konstruktora.
 

Przydatne linki


Śledź mnie na LinkedIn


Newsletter


👋
Jeśli masz jakieś sugestie lub pytania, proszę napisz do mnie wiadomość: kuba@javampokaze.pl