Konstruktory to bardzo ważny element języka Java — świadomie lub nie, korzystałaś z nich już od początku Twojej przygody z programowaniem. Dzisiaj zajmiemy się nimi trochę bliżej, kładąc nacisk na kontekst dziedziczenia.
Czym są konstruktory
Żeby lepiej zrozumieć, o czym mówimy, wyjaśnijmy najpierw czym są konstruktory. Należą do specjalnych ‘metod’, uruchamianych w momencie tworzenia nowego obiektu, za pomocą słówka new. Od normalnych metod odróżnia je to, że nic nie zwracają i nie deklarują typu zwracanego. Ponadto ich nazwa musi być identyczna z nazwą klasy. Nie można ich także wywołać po utworzeniu obiektu. Czym zatem nie są? Nie są metodami typu void, ponieważ rządzą się szczególnymi prawami. Nie są też fabrykami — nie zwracają obiektów.
Po tej może nie do końca jasnej definicji, spójrzmy na przykładowy kod:
StringBuilder builder = new StringBuilder();
Ta linijka tworzy nowy obiekt typu StringBuilder, wywołując przy tym konstruktor (bezargumentowy). Za każdym razem, kiedy tworzymy obiekt za pomocą new, wywoływany jest jeden z konstruktorów.
Do czego służą konstruktory? Pozwalają wymusić podanie zestawu informacji, których najczęściej używamy do uzupełnienia pól prywatnych klasy. Czasami służą do dokonania prostych konwersji czy translacji obiektów, choć jest to wątpliwą praktyką programowania obiektowego. Zdecydowanym antywzorcem jest umieszczanie w tym miejscu logiki biznesowej. Nigdy nie powinna ona być wstawiana w konstruktorze obiektu.
Konstruktor domyslny
Pewnie zastanawiasz się w tym momencie nad faktem, że w większości obiektów nie pisałaś konstruktorów! Otóż Java wykonuje trochę pracy za Ciebie. Jeśli nie zadeklarujesz żadnego konstruktora, to program ‘stworzy’ go za Ciebie. Będzie to bezargumentowy konstruktor, który nic nie robi. Dzięki temu kod:
MojObiekt obiekt = new MojObiekt();
zadziała jak tylko utworzysz klasę MojObiekt.
Ale uwaga: zadeklarowanie dowolnego konstruktora (bezargumentowego lub nie) spowoduje, że domyślny konstruktor nie zostanie utworzony.
Definiowanie konstruktorów
Konstruktory definiujemy podobnie jak zwykłe metody — możemy określić dowolne argumenty oraz dowolną logikę w ciele metody. Możemy także stworzyć kilka konstruktorów w jednej klasie:
public class KlasaZKonstruktorami {
private int jakasLiczba;
public KlasaZKonstruktorami() {
this(10);
}
public KlasaZKonstruktorami(int jakasLiczba) {
this.jakasLiczba = jakasLiczba;
}
}
Powyższy kod definiuje konstruktor bezargumentowy oraz konstruktor z jednym argumentem typu int. Pokazuje także inny koncept — korzystanie z innych konstruktorów w tej samej klasie. Służy do tego słówko kluczowe ‘this’ użyte tak, jakby samo było metodą.
Zwróć uwagę, że pomimo wyglądu metody, sygnatura, poza nazwą, nie zawiera typu zwracanego. To ostateczny wyróżnik pomiędzy metodą, a konstruktorem.
Konstruktory prywatne
Byś może zastanawiasz się również, dlaczego przed konstruktorem stawiamy klasyfikator dostępu i czy może to być coś innego, niż public. Otóż jak najbardziej. Konstruktory mogą być protected lub private. Rządzą nimi te same zasady, co w przypadku metod, jeśli chodzi o wywoływanie. Konstruktor prywatny może być użyty w konstrukcji new … tylko wewnątrz tej klasy. Dotyczy to także metod statycznych.
Bardzo ważną kwestią jest to, że o ile konstruktory mogą rzucać wyjątki, nie jest to szczególnie czytelne. Konstruktory prywatne często są używane w połączeniu ze wzorcem factory method. Pozwala to na ‘kontrolowanie’ tworzenia nowych obiektów i np. ponownego używania istniejących (nie możemy tego zrobić w konstruktorze, ponieważ jest on uruchamiany dla utworzonego, ale nie zainicjowanego obiektu).
Uwaga pułapka!
To może Wam się przytrafić zarówno w praktyce, w kiepsko zorganizowanym kodzie, jak i podczas rekrutacji i testów. Zerknijmy na poniższą klasę (w przykładzie pominięte ciało metody i konstruktora, ale do zobrazowania wystarczą nam same sygnatury):
public class Typ { public Typ Typ() { //to jest metoda
} public Typ() { //to jest konstruktor } }
Zestawiając obie obok siebie, łatwo zauważyć różnice. W przypadku wyłącznie takiej deklaracji metody w kodzie oraz występowania wielu innych ‘rozpraszaczy’, łatwo ją przeoczyć.
Warto także nadmienić, że z punktu widzenia Javy taka metoda jest poprawna (przynajmniej od strony kodu) Kod jak najbardziej się skompiluje, a metoda będzie dostępna. Nie jest to zalecana praktyka z uwagi na czytelność kodu. Niestety, czasem można się natknąć na podobne ‘kwiatki’.
Konstruktory, a dziedziczenie
Jak wobec tego wyglądają konstruktory, kiedy dziedziczymy po danej klasie? Otóż konstruktory nie są dziedziczone. Ma to sens, biorąc pod uwagę, że ich założeniem jest inicjować dany obiekt. Dziedziczenie mogłoby prowadzić do niepełnej inicjalizacji obiektów oraz problemów z implementacją. Stanowi to powód, dla którego nie wolno umieszczać żadnej logiki w konstruktorach! Teoretycznie kod się skompiluje, ale proszę, nie rób tego innym programistom.
Jeszcze nie wszystko powiedzieliśmy sobie w kwestii konstruktorów. Każdy konstruktor musi w pierwszej operacji wywołać konstruktor klasy, po której dziedziczy. Java ponownie robi to za nas. Jeśli nie wywołamy konstruktora klasy nadrzędnej jako pierwszej operacji, Java domyślnie wywoła konstruktor bezargumentowy za nas. Najczęściej nie rodzi to problemów. Jednak w przypadku, kiedy dziedziczymy po klasie deklarującej własne konstruktory, może ona nie mieć konstruktora bezargumentowego. Wtedy nasza klasa się nie skompiluje, o ile jawnie nie wywołamy jednego z konstruktorów klasy nadrzędnej.
super
Aby odwołać się do klasy po której dziedziczmy, możemy użyć słówka ‘super’. Działa ono analogicznie jak this, pozwala odwoływać się do konstruktorów, metod oraz pól klasy, po której dziedziczymy. Dla przykładu poniższy kod:
public class Klasa {
public Klasa() {
//nic nie robimy
}
}
Jest równoważny poniższemu:
public class Klasa {
public Klasa() {
super();
//nic nie robimy
}
}
Co ważne, metod nie dotyczy taka sama zasada, co konstruktorów. Metody dziedziczące nie muszą wywoływać tych, po których dziedziczą, nie musi to być również pierwsza instrukcja. Weźmy np poniższy kod:
public KlasaBazowa {
public int iloczyn(int a, int b) {
return a*b;
}
}
public KlasaZUlepszeniami extends KlasaBazowa {
public int iloczyn(int a, int b) {
if (a==0 || b==0) {return 0;}
return super.iloczyn(a, b);
}
}
Pomijając fakt, że nie jest to wzorowy przykład dobrze zorganizowanego kodu, jest on składniowo jak najbardziej poprawny.