Cześć! Dzisiaj rozważymy bardzo ważny temat dotyczący naszych obiektów. Możemy, bez przesady powiedzieć, że to zagadnienie będzie używane przez ciebie każdego dnia! Rozmawiamy o konstruktorach. Być może to określenie słyszysz po raz pierwszy, ale w rzeczywistości korzystałeś już z konstruktorów. Po prostu nie zdawałeś sobie z tego sprawy :) Przekonamy się o tym później. Konstruktory w Java - 1

Czym jest konstruktor w języku Java i jakie pełni funkcje?

Rozważmy dwa przykłady.

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car bugatti = new Car();
       bugatti.model = "Bugatti Veyron";
       bugatti.maxSpeed = 378;

   }
}
Stworzyliśmy nasz samochód, ustawiliśmy jego model i maksymalną prędkość. Lecz obiekt Car nie miałby oczywiście 2 pól w rzeczywistym projekcie. Mógłby mieć na przykład 16 pól!

public class Car {

   String model;// model
   int maxSpeed;// maximum speed
   int wheels;// wheel width
   double engineVolume;// engine volume
   String color;// color
   int productionYear;// production year
   String ownerFirstName;// first name of owner
   String ownerLastName;// last name of owner
   long price;// price
   boolean isNew;// flag indicating whether car is new
   int seatsInTheCar;// number of seats in the car
   String cabinMaterial;// interior material
   boolean insurance;// flag indicating whether car is insured
   String manufacturerCountry;// manufacturer country
   int trunkVolume;// size of the trunk
   int accelerationTo100km;// how long it takes to accelerate to 100 km/h (in seconds)


   public static void main(String[] args) {
       Car bugatti = new Car();

       bugatti.color = "blue";
       bugatti.accelerationTo100km = 3;
       bugatti.engineVolume = 6.3;
       bugatti.manufacturerCountry = "Italy";
       bugatti.ownerFirstName = "Amigo";
       bugatti.productionYear = 2016;
       bugatti.insurance = true;
       bugatti.price = 2000000;
       bugatti.isNew = false;
       bugatti.seatsInTheCar = 2;
       bugatti.maxSpeed = 378;
       bugatti.model = "Bugatti Veyron";

   }

}
Stworzyliśmy nowy obiekt Car. Jest jeden problem: mamy 16 pól, ale zainicjowaliśmy tylko 12! Spójrz teraz na kod i spróbuj znaleźć pola, o których zapomnieliśmy! Nie takie proste, co? W tej sytuacji programista może łatwo popełnić błąd i nie zainicjować jakiegoś pola. W rezultacie program będzie zachowywał się nieprawidłowo:

public class Car {

   String model;// model
   int maxSpeed;// maximum speed
   int wheels;// wheel width
   double engineVolume;// engine volume
   String color;// color
   int productionYear;// production year
   String ownerFirstName;// first name of owner
   String ownerLastName;// last name of owner
   long price;// price
   boolean isNew;// flag indicating whether car is new
   int seatsInTheCar;// number of seats in the car
   String cabinMaterial;// interior material
   boolean insurance;// flag indicating whether car is insured
   String manufacturerCountry;// manufacturer country
   int trunkVolume;// size of the trunk
   int accelerationTo100km;// how long it takes to accelerate to 100 km/h (in seconds)


   public static void main(String[] args) {
       Car bugatti = new Car();

       bugatti.color = "blue";
       bugatti.accelerationTo100km = 3;
       bugatti.engineVolume = 6.3;
       bugatti.manufacturerCountry = "Italy";
       bugatti.ownerFirstName = "Amigo";
       bugatti.productionYear = 2016;
       bugatti.insurance = true;
       bugatti.price = 2000000;
       bugatti.isNew = false;
       bugatti.seatsInTheCar = 2;
       bugatti.maxSpeed = 378;
       bugatti.model = "Bugatti Veyron";

       System.out.println("Model: Bugatti Veyron. Engine volume: " + bugatti.engineVolume + ". Trunk volume: " + bugatti.trunkVolume + ". Cabin material: " + bugatti.cabinMaterial +
       ". Wheel width: " + bugatti.wheels + ". Purchased in 2018 by Mr. " + bugatti.ownerLastName);

   }

}
Wydruk konsoli:
Model: Bugatti Veyron. Pojemność silnika: 6.3. Pojemność bagażnika: 0. Materiał wnętrza: brak. Szerokość koła: 0. Zakupiony w 2018 roku przez pana null
Twój nabywca, który poświęcił 2 miliony dolarów na samochód, oczywiście nie będzie chciał, aby ktoś zwracał się do niego "Panie null"! Tak na poważnie, chodzi o to, że nasz program stworzył obiekt nieprawidłowo: samochód o szerokości koła 0 (czyli bez kół w ogóle), brakujący bagażnik, wnętrze z nieznanego materiału, a przede wszystkim nieokreślony właściciel. Można sobie tylko wyobrazić, jak taki błąd może "eksplodować", przy uruchamianiu programu. Musimy jakoś unikać takich sytuacji. Musimy nasz program ograniczyć: podczas tworzenia nowego obiektu Car chcielibyśmy, aby pola, takie jak model i maksymalna prędkość, były zawsze określone. W przeciwnym razie, chcielibyśmy zapobiec powstaniu tego obiektu. Konstruktory poradzą sobie z tym zadaniem z łatwością. Nie bez powodu tak się nazywają. Konstruktor tworzy rodzaj "szkieletu" klasy, do którego musi pasować każdy nowy obiekt. Aby było wygodniej, wróćmy do prostszej wersji klasy Car z dwoma polami. Biorąc pod uwagę nasze wymagania, konstruktor klasy Car będzie wyglądał następująco:

public Car(String model, int maxSpeed) {
   this.model = model;
   this.maxSpeed = maxSpeed;
}

// And creating an object now looks like this:

public static void main(String[] args) {
   Car bugatti = new Car("Bugatti Veyron", 378);
}
Zwróć uwagę, jak deklarowany jest konstruktor. Jest podobny do zwykłej metody, ale nie posiada zwracanego typu. Ponadto konstruktor określa nazwę klasy (Car) zaczynającą się od wielkiej litery. Dodatkowo, konstruktor jest używany z nieznanym jeszcze dla ciebie słowem kluczowym: this. Słowo kluczowe this służy do wskazania konkretnego obiektu. Kod w konstruktorze

public Car(String model, int maxSpeed) {
   this.model = model;
   this.maxSpeed = maxSpeed;
}
może być interpretowany prawie dosłownie: "Argument model przekazany konstruktorowi to model dla tego samochodu (tego, który teraz tworzymy). Argument maxSpeed przekazany konstruktorowi to maxSpeed dla tego samochodu (tego, który tworzymy)." Tak się właśnie dzieje:

public class Car {

   String model;
   int maxSpeed;

   public Car(String model, int maxSpeed) {
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   public static void main(String[] args) {
       Car bugatti = new Car("Bugatti Veyron", 378);
       System.out.println(bugatti.model);
       System.out.println(bugatti.maxSpeed);
   }

}
Wydruk konsoli:
Bugatti Veyron 378
Konstruktor poprawnie przypisał wymagane wartości. Konstruktory w Java - 2Być może udało ci się zauważyć, że konstruktor jest bardzo podobny do zwykłej metody! Tak właśnie jest. Konstruktor to tak naprawdę metoda, ale o specyficznych cechach :) Podobnie jak w przypadku metod, przekazaliśmy argumenty do naszego konstruktora. Tak jak wywołanie metody, wywołanie konstruktora nie zadziała, jeśli tego nie sprecyzujesz:

public class Car {

   String model;
   int maxSpeed;

   public Car(String model, int maxSpeed) {
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   public static void main(String[] args) {
       Car bugatti = new Car(); // Error!
   }
  
}
Widać, że konstruktor dokonuje tego, co staraliśmy się osiągnąć. Teraz nie możesz stworzyć samochodu bez prędkości lub modelu! Podobieństwo między konstruktorami i metodami na tym się nie kończy. Podobnie jak metody, konstruktory można nadpisywać. Wyobraź sobie, że masz w mieszkaniu 2 koty domowe. Jednego z nich wychowujesz od kocięta. Lecz drugi został przez ciebie zabrany z ulicy, gdy był już dorosły i nie wiesz dokładnie ile ma lat. W tym przypadku chcemy, aby nasz program był w stanie stworzyć dwa rodzaje kotów: te z imieniem i wiekiem (dla pierwszego kota) oraz te z samym imieniem (dla drugiego kota). W tym celu spróbujmy nadpisać konstruktor:

public class Cat {

   String name;
   int age;

   // For the first cat
   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   // For the second cat
   public Cat(String name) {
       this.name = name;
   }

   public static void main(String[] args) {

       Cat smudge = new Cat("Smudge", 5);
       Cat streetCatNamedBob = new Cat("Bob");
   }

}
Oprócz oryginalnego konstruktora z parametrami "imię" i "wiek", dodaliśmy kolejny, zawierający tylko jeden parametr "imię". Dokładnie ten sam sposób, w jaki przeciążaliśmy metody w poprzednich lekcjach. Teraz możemy stworzyć oba rodzaje kotów :) Pamiętasz, że na początku lekcji powiedzieliśmy, że korzystałeś już z konstruktorów, nie zdając sobie z tego sprawy? Wiemy, co mówimy. Faktem jest, że każda klasa w Javie ma tak zwany konstruktor domyślny. Nie wymaga żadnych argumentów, ale jest wywoływany za każdym razem, gdy tworzysz obiekt dowolnej klasy.

public class Cat {

   public static void main(String[] args) {

       Cat smudge = new Cat(); // The default constructor is invoked here
   }
}
Jest niewidoczny na pierwszy rzut oka. Stworzyliśmy obiekt i co z tego? Gdzie tu ten konstruktor cokolwiek robi? Aby to zobaczyć, napiszmy wyraźnie pusty konstruktor dla klasy Cat. Wyświetlmy w nim jakąś frazę. Jeśli dana fraza będzie widoczna, to konstruktor został wywołany.

public class Cat {

   public Cat() {
       System.out.println("A cat has been created!");
   }

   public static void main(String[] args) {

       Cat smudge = new Cat(); // The default constructor is invoked here
   }
}
Wydruk konsoli:
Stworzono kota!
Tu masz potwierdzenie! Domyślny konstruktor jest zawsze niewidoczny w twoich klasach. Ale musisz wiedzieć jeszcze jedną rzecz. Domyślny konstruktor usuwany jest z klasy po utworzeniu konstruktora z argumentami. W rzeczywistości, zobaczyliśmy już dowód na powyższą tezę. Był w tym kodzie:

public class Cat {

   String name;
   int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public static void main(String[] args) {

       Cat smudge = new Cat(); //Error!
   }
}
Nie mogliśmy stworzyć Cat bez nazwy i wieku, ponieważ zadeklarowaliśmy konstruktor Cat z parametrami string i int. To spowodowało, że domyślny konstruktor natychmiast zniknął z klasy. Pamiętaj więc, że jeśli potrzebujesz kilku konstruktorów w swojej klasie, w tym konstruktora bez argumentów, będziesz musiał zadeklarować go oddzielnie. Załóżmy na przykład, że tworzymy program dla kliniki weterynaryjnej. Nasza klinika chce robić dobre uczynki i pomagać bezdomnym kociętom, których imiona i wiek są nieznane. Wtedy nasz kod powinien wyglądać tak:

public class Cat {

   String name;
   int age;

   // For cats with owners
   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   // For street cats
   public Cat() {
   }

   public static void main(String[] args) {

       Cat smudge = new Cat("Smudge", 5);
       Cat streetCat = new Cat();
   }
}
Teraz, gdy napisaliśmy już wyraźny konstruktor domyślny, możemy stworzyć oba typy kotów :) Jak w przypadku każdej metody, kolejność argumentów przekazywanych do konstruktora jest bardzo ważna. Zamieńmy w naszym konstruktorze argumenty name i age.

public class Cat {

   String name;
   int age;

   public Cat(int age, String name) {
       this.name = name;
       this.age = age;
   }

   public static void main(String[] args) {

       Cat smudge = new Cat("Smudge", 10); // Error!
   }
}
Wystąpił błąd! Konstruktor zaznacza wyraźnie, że gdy tworzony jest obiekt Cat, należy przekazać mu liczbę i ciąg, dokładnie w takiej kolejności. Więc, nasz kod nie działa. Pamiętaj o tym i miej to na uwadze, przy deklarowaniu własnych klas:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}

public Cat(int age, String name) {
   this.age = age;
   this.name = name;
}
To dwa zupełnie inne konstruktory! Gdybyśmy w jednym zdaniu mieli wyrazić odpowiedź na pytanie "Po co mi konstruktor?", moglibyśmy odpowiedzieć: "Aby zapewnić, że obiekty zawsze mają prawidłowy stan". Kiedy używasz konstruktorów, wszystkie twoje zmienne zostaną poprawnie zainicjowane. Twoje programy nie będą zawierały samochodów z prędkością 0 ani żadnych innych "nieprawidłowych" obiektów. To przede wszystkim programista korzysta z ich stosowania. Jeśli ręcznie inicjujesz pola (po utworzeniu obiektu), istnieje duże ryzyko, że coś przeoczysz i wprowadzisz błąd. Lecz to się nie stanie w przypadku konstruktora: jeśli nie przekażesz wszystkich wymaganych argumentów lub przekażesz niewłaściwe typy argumentów, kompilator natychmiast zarejestruje błąd. Trzeba również poinformować odrębnie, że nie należy umieszczać logiki programu wewnątrz konstruktora. Do tego służą metody. W metodach należy zdefiniować wszystkie wymagane funkcje. Zobaczmy, dlaczego dodawanie logiki do konstruktora jest złym pomysłem:

public class CarFactory {

   String name;
   int age;
   int carsCount;

   public CarFactory(String name, int age, int carsCount) {
   this.name = name;
   this.age = age;
   this.carsCount = carsCount;

   System.out.println("Our car factory is called " + this.name);
   System.out.println("It was founded " + this.age + " years ago" );
   System.out.println("Since that time, it has produced " + this.carsCount +  " cars");
   System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
}

   public static void main(String[] args) {

       CarFactory ford = new CarFactory("Ford", 115 , 50000000);
   }
}
Otrzymaliśmy klasę CarFactory, która opisuje fabrykę samochodów. Wewnątrz konstruktora, inicjujemy wszystkie pola i wprowadzamy logikę: wyświetlamy pewne informacje o fabryce. Wydaje się, że nie ma w tym nic złego. Program działa dobrze. Wydruk konsoli:
Nasza fabryka samochodów nazywa się Ford Została założona 115 lat temu Od tego czasu wyprodukowała 50000000 samochodów Średnio produkuje 434782 samochodów rocznie
Ale tak na prawdę, podłożyliśmy sobie minę o opóźnionym zapłonie. Ten rodzaj kodu może bardzo łatwo prowadzić do błędów. Przypuśćmy, że teraz nie mówimy o Fordzie, ale o nowej fabryce o nazwie "Amigo Motors", która istnieje niecały rok i wyprodukowała 1000 samochodów:

public class CarFactory {

   String name;
   int age;
   int carsCount;

   public CarFactory(String name, int age, int carsCount) {
   this.name = name;
   this.age = age;
   this.carsCount = carsCount;

   System.out.println("Our car factor is called " + this.name);
   System.out.println("It was founded " + this.age + " years ago" );
   System.out.println("Since that time, it has produced " + this.carsCount +  " cars");
   System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
}


   public static void main(String[] args) {

       CarFactory ford = new CarFactory("Amigo Motors", 0 , 1000);
   }
}
Wydruk konsoli:
Nasza fabryka samochodów nazywa się Amigo Motors Exception in thread "main" java.lang.ArithmeticException: / by zero Została założona 0 lat temu O d tego czasu wyprodukowała 1000 samochodów w CarFactory(CarFactory.java:15) at CarFactory.main(CarFactory.java:23) Process finished with exit code 1
Boom! Program kończy się jakimś niezrozumiałym błędem. Spróbujesz odgadnąć przyczynę? Problem tkwi w logice, którą wstawiliśmy do konstruktora. Mianowicie, w tej lini:

System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
Tutaj wykonujesz obliczenia i dzielisz liczbę wyprodukowanych samochodów przez wiek fabryki. Ponieważ nasza fabryka jest nowa (tzn. ma 0 lat), dzielimy przez 0, czego nie możemy zrobić w matematyce. W konsekwencji działanie programu zakończy się z błędem. Co powinniśmy zrobić? Umieścić całą logikę w osobnej metodzie. Nazwijmy ją printFactoryInfo(). Możesz przekazać do niej obiekt CarFactory jako argument. Możesz umieścić tam całą logikę i jednocześnie poradzić sobie z potencjalnymi błędami (takimi jak nasze, które dotyczą zera lat). Co kto lubi. Konstruktory są potrzebne do ustawienia prawidłowego stanu obiektu. Metody mamy do biznesowej logiki. Nie mieszaj jednego z drugim.
Ten artykuł przeczytasz także po angielsku.
Read the English version of this article to understand constructors in Java. Constructors are more interesting and subtle than you may realize.