5 maja 2008

SCJP - Konstruktory i inicjalizacja

Kilka słów o konstruktorach było już w artykule "SCJP - Deklaracja konstruktora" a teraz nadeszła pora na więcej. Będzie mniej o deklaracji a więcej o implementacji.

Poprzedni, wspomniany wyżej artykuł zacząłem od stwierdzenia, że każda klasa ma konstruktor. Podkreślam to jeszcze raz – każda, w tym także abstrakcyjna! Konstruktory mają także typy wyliczeniowe, ale interfejsy nie. Po co konstruktor w klasie abstrakcyjnej, której instancji przecież nie można skonstruować? Wszystko wyjaśni się już za chwilę.

Konstruktory uruchamiane są, gdy tworzymy nowe instancje/obiekty a więc najczęściej w wyniku użycia operatora ‘new’ (wyjątkiem są stałe wyliczeniowe). Jednak, aby utworzyć instancję danej klasy nie wystarczy wywołać konstruktora z tej klasy, trzeba wywołać także konstruktory z wszystkich nadklas, do klasy Object włącznie. Poniższy przykład pomoże nam zrozumieć, dlaczego.

abstract class Parent {
private String str;

Parent() {
this.str = "some data";
}

public String getStr() {
return str;
}
}

class Child extends Parent { }

public class Test {
public static void main(String[] args) {
Child child = new Child();

System.out.println(child.getStr());
}
}

Cóż robi ten program? Oczywiście wypisuje napis "some data". Dzieje się tak tylko dlatego, że oprócz wygenerowanego przez kompilator, domyślnego konstruktora z klasy Child wywołany został także konstruktor z klasy Parent. W celu zapewnienia poprawnej inicjalizacji obiektów obowiązuje prosta zasada – pierwszą instrukcją każdego konstruktora musi być wywołanie super(…) albo this(…) (z dowolną liczbą parametrów), przy czym jeśli nie umieściliśmy jednego z tych wywołań kompilator automatycznie doda wywołanie super() (bez parametrów). Instrukcja super(…) to wywołanie konstruktora z nadklasy a this(…) to wywołanie innego, przeciążonego konstruktora z klasy bieżącej, aczkolwiek któryś z kolei konstruktor będzie musiał w końcu wywołać konstruktor z nadklasy. Ilustruje to poniższy przykład.

abstract class Parent {
private String str;

Parent(String str) {
this.str = str;
}

public String getStr() {
return str;
}
}

class Child extends Parent {
Child() {
this("some text");
}

Child(String str) {
super(str);
}
}

public class Test {
public static void main(String[] args) {
Child child = new Child();
System.out.println(child.getStr());

child = new Child("another text");
System.out.println(child.getStr());
}
}

Efektem działania tego programu będą napisy kolejno "some text" i "another text". Podkreślę jeszcze, że konstruktora nie można wywołać tak jak metody, posługując się jego nazwą, zawsze posługujemy się konstrukcją super(…) lub this(…). Konstruktora nie można także wywołać explicite inaczej jak tylko z innego konstruktora, tak więc instrukcje super(…) i this(…) nie są dozwolone w metodach. Wiem, że wydaje się to być oczywiste, ale warto sobie pewne rzeczy uświadomić wprost, pod kątem SCJP. To, że instrukcje super(..) i this(…) muszą być pierwszymi instrukcjami w konstruktorze oznacza również, że w każdym konstruktorze możemy użyć tylko jednej z nich i tylko co najwyżej raz. W przeciwnym wypadku któraś z nich nie byłaby pierwsza. Skądinąd, wielokrotne występowanie tych instrukcji nie miałoby sensu.

Wywołanie przeciążonego konstruktora z klasy bieżącej, albo konstruktora z nadklasy musi być bezwzględnie pierwszą instrukcją i dopóki instrukcja ta nie zostanie wykonana obiekt nie jest jeszcze skonstruowany. Z tego względu, dopóki nie zostanie wykonana instrukcja super(…) nie jest możliwe wykonywanie operacji na właśnie konstruowanej instancji. Mówiąc wprost, dopóki nie zostanie wykonana (nie wywołana, wykonana!) instrukcja super(…) nie można wywoływać nie statycznych metod ani używać nie statycznych zmiennych. Parametrami tych wywołań mogą być natomiast zmienne statyczne i wartości zwracane z wywołań statycznych metod. Poniżej przykład, który przy okazji pokazuje, jak wykonać pewne operacje przed wywołaniem super-konstruktora, aczkolwiek na SCJP przydatne będą raczej inne triki.

class Child extends Parent {
Child() {
super(doSomethingBefore());
}

static String doSomethingBefore() {
System.out.println("before call to super()");

return "some text";
}
}

I jeszcze słówko, co do domyślnego konstruktora generowanego przez kompilator w przypadku, gdy programista nie zapewnił żadnego. Oprócz tego, że jest to konstruktor bez argumentowy i zawierający jedynie bezargumentowe wywołanie super() trzeba wiedzieć, że ma on modyfikator dostępu taki jak jego klasa. I ostatni przykład.

class Parent {
String str = "some value";
}

class Child extends Parent {
String childStr = "some other value";
}

Jak można się domyślić chodzi o inicjalizację zmiennych instancyjnych. Instrukcje przypisania wartości tym zmiennym też muszą się przecież kiedyś i gdzieś wykonać. I jak najbardziej, wykonują się one jako część konstruktora. Aby zrozumieć, kiedy to się dzieje prześledźmy kolejne operacje wykonywane w efekcie utworzenia nowej instancji klasy Child, tj. wywołania ‘new Child()’. Oto one:

- wywoływany jest domyślny konstruktor klasy Child, jego pierwszą instrukcją jest wywołanie super()

- wywoływany jest domyślny konstruktor klasy Parent, jego pierwszą instrukcją jest wywołanie super()

- wywoływany jest konstruktor w klasie Object, ta nie ma już nadklas, więc po wykonaniu kodu konstruktora następuje powrót do konstruktora z klasy Parent

- wykonywana jest inicjalizacja zmiennych instancyjnych w klasie Parent, a więc przypisanie ‘str = "some value"’ po czym następuje powrót do konstruktora z klasy Child

- wykonywana jest inicjalizacja zmiennych instancyjnych w klasie Child, a więc przypisanie ‘childStr = "some other value"’ co jest ostatnim krokiem, obiekt został utworzony i zainicjalizowany

1 komentarz:

Anonimowy pisze...

Warto dodać, że na tym samym etapie co "inicjalizacja zmiennych instancyjnych" wykonywane są bloki inicjalizacyjne (te nie-statyczne).

Przestudiuj wykonie...

class X {
int x = 10;

{
x = 12;
y = 5;
}

int y = 666;

{
z = 4;
x = 28;
}

int z;

public X() {
super();
// co sie wypisze na ekranie?
System.out.println(x + ", " + y + ", " + z);
// 28, 666, 4
}
}