30 kwietnia 2008

SCJP - Rzutowanie referencji

Rzutowanie referencji, czyli streszczenie sekcji "Reference Variable Casting" – kontynuacja działań zapowiedzianych w artykule "Przygotowania do SCJP czas zacząć".

Wiemy już, że typ zmiennej referencyjnej i typ obiektu wskazywanego przez tą zmienną nie koniecznie są tym samym typem – typ obiektu może być także podtypem zmiennej, albo dowolnym typem implementującym, jeśli zmienna ma typ pewnego interfejsu. Zerknijmy na poniższy przykład.

class Parent {
void doSomething() { }
}

class Child extends Parent {
void doMore() { }
}

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

obj.doSomething();
((Child)obj).doMore();
}
}
Zmienna obj jest typu Parent, ale wskazuje na obiekt typu Child. Jak pisałem w artykule "SCJP – Dziedziczenie i wielodziedziczenie" o tym, jakie metody możemy wywołać na danym obiekcie decyduje typ referencji, a nie typ obiektu. Aby więc wywołać metodę doMore() zdefiniowaną w klasie Child trzeba wykonać rzutowanie (ang. casting), tak jak to pokazano w powyższym przykładzie. Jeśli byśmy tego rzutowania nie wykonali, to kod by się nie skompilował, byłby to więc błąd czasu kompilacji. Ale z rzutowaniem związane są także błędy czasu wykonania. Rozważmy następującą modyfikację klasy Test.

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

((Child)obj).doMore(); // Błąd czasu wykonania!
}
}
W czasie kompilacji wszystko jest w porządku, problem pojawia się po uruchomieniu programu – dostajemy wyjątek java.lang.ClassCastException. Rzeczywiście, obiekt wskazywany przez zmienną obj jest klasy Parent a nie Child. Kompilator zakłada, że wiemy co robimy wykonując rzutowanie i ślepo ufa, że w czasie wykonania zmienna będzie wskazywała na obiekt odpowiedniego typu. Nie ma zresztą innego wyjścia, wykonanie takiego sprawdzenia w czasie kompilacji było by często zbyt skomplikowane, a nawet nie możliwe. Aczkolwiek możliwa jest statyczna weryfikacja w pewnym zakresie. Kompilator zaprotestuje, jeśli wskazane rzutowanie nie ma szans na powodzenie, tj. jeśli typ rzutowania nie jest podtypem albo nadtypem typu zmiennej. Ilustruje to poniższy przykład.

public class Test {
public static void main(String[] args) {
Number num = new Integer(1);

String str = ((String)num); // Błąd kompilacji!
}
}
Obiekt typu Number oraz żaden jego podtyp nigdy nie może być potraktowany jako String, tak więc powyższy kod skutkuje błędem czasu kompilacji.

Myśląc o rzutowaniu zwykle mamy na myśli rzutowanie uszczegóławiające (ang. downcasting), tj. rzutowanie zmiennej do pewnego typu pochodnego, ale możliwe jest też rzutowanie uogólniające (ang. upcasting) do typu nadrzędnego, tak jak to pokazano poniżej, choć nie ma ono większego sensu.

public class Test {
public static void main(String[] args) {
Number num = new Integer(1);

((Object)num).toString();
}
}

3 komentarze:

Anonimowy pisze...

Warto dodać, że zawsze można jawnie próbować rzutować na dowolny interfejs nawet gdy nie znajduje się w hierarchi klasy rzutowanego obiektu...

Anonimowy pisze...

Parent obj = new Child();

obj.doSomething();
((Child)obj).doMore();

Rzutowanie dziecka na rodzica jest błędem w podejściu do programowania w javie.

Jedyne wytłumaczenie dla rzutowania jest wówczas kiedy rzutujemy z Object.

Proszę nie mieszać niedoświadczonym developerom w głowie.

Anonimowy pisze...

Może i trochę odkopuję ale ciekawi mnie ostatni komentarz jaki się tutaj pojawił.

Mianowicie: dlaczego rzutowanie dziecka na rodzica jest błędem w podejściu do programowania w javie?