Rzutowanie w górę czy w doł

0

Cześć,
Mam problem z rozróżnieniem rzutowania w górę czy w dół. Rozumiem oczywiście takie pojęcia jak hierarchia klas. Zobaczcie:

class A {}
class B extends A {
     void xx(){};
}

Do jakiego rzutowania dochodzi tutaj ?
A a = new B();
Zdaje mi się, że rzutujemy w dół. Po lewej stronie mamy obiekt typu A. (nikt nie ma wątpliwości :)). Po prawej stronie mamy obiekt typu B (który de facto też ma typ A). Więc z B rzutujemy w górę ?

Np, a.xx(); // nie przejdzie, kompilacji, natomiast
((B)a).xx(); już tak.
Oznacza to, że doszło do zrzutowania A na B, czyli rzutowania w dół.

Dobrze myślę ? Kiedy ogólnie rzutowanie w dół jest mozliwe ?

0

Rzutowanie "w dół" nie ma żadnego sensu, bo np...

class A {
     void xx() {}
}

class B extends A {
}
A a = (A)(new B());
A a = new B();

...w powyższym, oba przykłady są poprawne i w obu uda się wywołać

a.xx();

Takie klasy spełniają LSP więc wszędzie tam gdzie chcesz użyć A i tak możesz użyć B (dlatego że B IS-A A; "B jest A", tak na prawdę). Rzutuje się wtedy kiedy nie wiadomo tak do końca czy B jest A.

Rzutowanie ma sens tylko wtedy kiedy z poziomu języka nie można być pewnym czy jakaś klasa ma relację IS-A z inną klasą (tzn że A na prawdę jest B), ale z innych przyczyn jesteśmy pewni (lub chcemy tak zrobić) żeby skorzystać z jakiejś klasy i założyć że ma tą relację IS-A.

Natomiast w tym przypadku...

class A {
}

class B extends A {
     void xx() {}
}

...jeżeli masz jakąś zmienną typu A, i jesteś pewien że to jest tak na prawdę instancja klasy B, to możesz tą zmienną zrzutować na ten drugi typ i wtedy wywołać tę funkcję.

A instancja = getInstance(); 
instancja.xx(); // <- error, klasa A nie ma metody xx() więc nie można jej bezpiecznie wywołać.
B zrzutowana = (A) instancja;
zrzutowana.xx(); // <- działa, klasa B ma metodę xx()
0

No, ale nie odpowiedziałes na moje pytanie o rzutowania w doł i w górę,...

0
Świetny Młot napisał(a):
class A {}
class B extends A {
     void xx(){};
}

Do jakiego rzutowania dochodzi tutaj ?
A a = new B();

To nie jest rzutowanie. Wiemy że B dziedziczy z A. Przez to wiemy że B JEST A. Podobnie jak, skoro Dog dziedziczy z Animal to wiemy że Dog JEST Animal. Wszędzie tam gdzie możemy użyć zwierzęcia, możemy użyć psa.

Wiem że skoro B jest A, to wszędzie tam gdzie możemy użyć A, możemy użyć też B. Nie potrzeba rzutowania.

PS: Powiem Ci inaczej. Rozważmy taką sytuację:

class A {
    void methodA() {}
}
class B extends A {
    void methdoB(){};
}
A a = new A();
a.methodA(); // ok
a.methodB(); // nie da się, klasa A nie ma metody B
A a = new B();
a.methodA(); // ok
a.methodB(); // nie da się, mimo że to na prawdę klasa B, to jest zadeklarowana jako A i nie ma pewności że jest 
metoda methodA
B b = new A();  // błąd, to że B jest A, nie znaczy że A jest B. Nie każde zwierzę jest psem
B b = new B(); // ok
A a = new B();
B b = a;  // nie da się, mimo że wiemy że w zmiennej a tak na prawdę jest B, to jest zadeklarowana jako A, i nie ma pewności że to obiekt tej klasy
A a = new B();
B b = (B)a;  // nie ma pewności że w zmiennej a jest obiekt klasy B, ale ponieważ rzutujemy to zmuszamy język żeby zaufał że w zmiennej a jest obiekt klasy B, mimo że jest zadeklarowany jako A

Podsumowując, "rzutować w dół" nie musisz dlatego że przez sam fakt że B dziedziczy z A, i można używać B wszędzie tam gdzie B. Rzutowanie "w górę" jest wtedy kiedy coś jest zadeklarowane jako A, wiesz na pewno że to jest B, i chcesz skorzystać z jakiejś metody B.

0

<quote="1285650">Takie klasy spełniają LSP więc wszędzie tam gdzie chcesz użyć A i tak możesz użyć B

Z większością postu się zgadzam, ale to bzdura. Spełnienie LSP zależy od implementacji.

Przykładowo:
```java
public class Rectangle {
	private int height;
	private int width;

	public void setHeight(int i){
		this.height = i;
	}

	public void setWidth(int i){
		this.width = i;
	}

	public int getField(){
		return height*width;
	}
}

public class Square extends Rectangle {
	
	@Override
	public void setHeight(int i){
		this.height = i;
		this.width = i;
	}

	@Override
	public void setWidth(int i){
		this.height = i;
		this.width = i;
	}
	
}

Taka implementacja nie spełnia LSP ponieważ taki test:

public void test1(){
	Rectangle r = new Rectangle();
	r.setHeight(1);
	r.setWidth(2);

	Assert.assertEquals(2, r.getField());

	r = new Square();
	r.setHeight(1);
	r.setWidth(2);

	Assert.assertEquals(2, r.getField());
}

Nie przejdzie.

0

Rzutowanie w górę.

0

W górę i w dół jest mylące dlatego, że w hierarchii klas korzeń drzewa jest na górze, a drzewo rozrasta się w dół. Dlatego wszystkie obiekty dziedziczące (potomne/podklasy) są niżej niż obiekty bazowe (nadklasy) dla nich. Dlatego rzutowanie w górę jest trywialne bo obiekt typu dziedziczącego "jest" również obiektem typu bazowego (chociaż może się inaczej zachowywać), natomiast w drugą stronę niekoniecznie. Dlatego też referencji typu bazowego zawsze można przypisać obiekty typu pochodnego (dziecka), a rzutowanie (przypisywanie) jest w tym wypadku trywialne i wykonywane bez żadnych dodatkowych działań. Jedynym rzutowaniem nietrywialnym jest rzutowanie w dół - w przypadku gdy mamy referencję typu bazowego, która jednak wskazuje na obiekt jednego z jego typów pochodnych. Tylko w tym wypadku trzeba się upewniać co do rzeczywistego typu obiektu.

0
TomRiddle napisał(a):

Podsumowując, "rzutować w dół" nie musisz dlatego że przez sam fakt że B dziedziczy z A, i można używać B wszędzie tam gdzie B. Rzutowanie "w górę" jest wtedy kiedy coś jest zadeklarowane jako A, wiesz na pewno że to jest B, i chcesz skorzystać z jakiejś metody B.

Rozpisałeś się na dwa posty (w większości poprawnie, ale niepotrzebnie), a to co tutaj zacytowałem to jeszcze napisałeś źle.

Rzutowanie w górę = konwersja klasy pochodnej do bazowej.
Rzutowanie w doł = konwersja klasy bazowej do pochodnej.

0

Warto jeszcze autorowi wątku uzmysłowić, że w Javie nie ma żadnej konwersji jednego typu obiektowego w drugi. Istnieje tylko konwersja typu referencji jako, że w Javie jest silne typowanie, czyli w każdym momencie typ każdej zmiennej jest określony. Sama referencja jest z punktu widzenia kompilatora typem prostym tak jak int czy long. Rzutowanie polega na zmianie typu referencji właśnie, a nie samego obiektu. Typ każdego obiektu po utworzeniu jest zawsze niezmienny, a nawet w przypadku obiektów niemutowalnych niezmienna jest dostępna publicznie zawartość. Można jednak przypisać referencję do każdego obiektu zmiennej referencyjnej typu swojej nadklasy (bazowego) i w tym sensie zmienia się typ referencji. Podobnie później można zrzutować zmienną referencyjną tego typu na referencję typu pochodnego pod warunkiem, że obiekt reprezentowany tą referencją faktycznie jest typu pochodnego (lub jest jeszcze niżej w hierarchii - czyli ma właściwość assignable do tego typu). Większość osób, które dopiero uczą się obiektowości, myli (utożsamia) zmienne przechowujące referencje do obiektów z samymi obiektami. Same obiekty nie mają w Javie żadnej reprezentacji, którą można gdziekolwiek przypisać. Mają tylko miejsce w pamięci, do którego istnieje wskaźnik (adres) - a który w Javie jest właśnie referencją (bo inaczej niż wskaźniki w C++ na referencji nie można wykonywać obliczeń, żeby dostać się do innego obiektu/miejsca w pamięci).

1 użytkowników online, w tym zalogowanych: 0, gości: 1