Rzutowanie w góre

0

Witam, dopiero zaczynam uczyć się programowania i jestem w temacie Polimorfizm. Staralem się znaleŹĆ w google odpowiedź na moje pytanie ale nigdzie nie jest napisane to wprost.

Czym różni się taki zapis wykorzystujący polimorfizm

KlasaBazowa a1 = new KlasaPochodna();

od zapisu:

KlasaPochodna a1 = new KlasaPochodna();

po licznych testach z kodem i metodami dochodzę do wniosku, że oba zapisy dają ten sam rezultat i nie różnią się od siebie niczym, na przykładzie chociażby takiego kodu:

 import static uzytkowe.Print.*;

class Bazowa{
	public void metoda1(){
		metoda2();
	}
	public void metoda2(){
		
		println("Metoda2 klasy bazowej");
	}
}

class Pochodna extends Bazowa{
public void metoda2(){
		
		println("Metoda2 klasy pochodnej");
	}
}

public class P1 {
	public static void main(String[] args){
		Bazowa a1 = new Pochodna(); //Rzutowanie w góre
		Pochodna a2 = new Pochodna();
		
		a1.metoda1();
		a2.metoda1();
	}
}

/* Wynik:
*Metoda2 klasy pochodnej
*Metoda2 klasy pochodnej
*/

Jak widać wynik jest taki sam w obu przypadkach, a więc chciałem się dowiedzieć co odróżnia te dwa zapisy?

1
  1. Nie możesz podać wartości typu Bazowa do metody oczekującej typu Pochodna.
  2. Typy bazowe są pomocne w konstruowaniu bibliotek - możesz np stworzyć bibliotekę przyjmującą wartości typu Bazowa, a potem w programie tworzyć podklasy i przekazywać instancje tych podklas.
  3. Pola w Javie nie są wirtualizowane (w przeciwieństwie do metod - dzięki temu że wszystkie metody są wirtualne metody obiektu zawsze zachowują się tak samo, niezależnie na co go rzutujesz, co widać w twoim kodzie). Jeżeli ustawisz pole i w klasie Bazowa i jeszcze raz w klasie Pochodna to będą to dwie różne zmienne. Jeśli wtedy chciałbyś dostać się do pola i z klasy Bazowa będąc w klasie Pochodna to musisz napisać super.i. Z tego względu (oraz innych nawet jeszcze ważniejszych) nie poleca się bezpośrednie dobieranie do pól klasy z zewnątrz. Należy używać akcesorów (getCośtam/ setCośtam).
0

Dzięki za szybką odpowiedź Wibowit :)

Czyli rozumiem, że różnica uwidacznia się jedynie przy próbie dostępu bezpośrednio do pól tych klas (czego jednak powinno się unikać, jak napisałes)?

A więc, jeśli zastosuje się do tych zaleceń to nie będzie istotne, którą formę wybiorę? Np. tutaj http://www.mimuw.edu.pl/~sroka/archiwalne/2006niezbednik/Java_niezbednik_4.pdf w przykładzie na str.3 tworzone są obiekty wymiennie raz rzutowaniem w góre, a za drugim razem nie. Która forma jest preferowana i powinno się używać?

1

Sprawa jest taka, że przy zmiennych lokalnych (w jakiejś procedurze czy konstruktorze), ale nie parametrach czy zwracanemu typowi, to nie ma to wielkiego znaczenia. Z zewnątrz nie ma żadnej różnicy, o ile twój kod robi to co ma robić. Dobrze jest jednak zawsze używać najbardziej ogólnej klasy bazowej, dzięki temu możemy bezboleśnie podmienić implementację. Oczywiście, jeżeli korzystamy z jakiejś fajnej własności klasy pochodnej to użycie pola typu klasy pochodnej jest uzasadnione.

Zaleta używania klas bazowych uwidacznia się np przy stosowaniu kolekcji. Powiedzmy, że stworzyliśmy sobie listę poleceniem:
List<Obiekt> lista = new ArrayList<Obiekt>();

Wszystko pięknie fajnie, lista jest kompaktowa, ale któregoś dnia otrzymaliśmy dodatkowe warunki na naszą funkcjonalność i np teraz często musimy wstawiać elementy w środek listy. Wiemy, że LinkedList lepiej nadaje się do insertów w środek listy niż ArrayList. Dzięki temu, że utworzyliśmy pole o typie wspólnej klasy bazowej tych różnych implementacji list to możemy bardzo szybko zmienić wykorzystywaną implementację. Wystarczy, że zmienimy tą linijkę na:
List<Obiekt> lista = new LinkedList<Obiekt>();

Oczywiście moglibyśmy nie stosować tutaj typu bazowego jako typu pola, ale np gdybyśmy przez przypadek stosowali metody klasy ArrayList, które nie należą do klasy LinkedList, to zamiana nie byłaby taka trywialna.

Jeżeli tworzymy jakieś API, które ma być używane przez ludzi z zewnątrz to zawsze należy zwracać najbardziej ogólny typ, który oferuje potrzebną funkcjonalność. Można sobie np tworzyć interfejsy i je implementować w prywatnych klasach (ale akurat to trochę rzadko się stosuje przynajmniej z tego co ja widziałem). Dzięki temu użytkownicy naszej klasy dostaną funkcjonalność, którą chcą, ale np nie będą mogli zrobić sobie podklasy naszej prywatnej klasy w prosty sposób (będą mogli jednak użyć wzorca dekorator na interfejsie - dekorator i tak jednak nie złamie potencjalnej hermetyzacji).

Zwracanie ogólnych klas jest nawet zalecane wewnątrz jakiegoś projektu - znacznie ułatwia to refaktoryzację. Biorąc na przykład powyższy przykład (sorry za gramatykę) to jeśli używalibyśmy ArrayList w wielu klasach rozrzuconych po całym projekcie to potem ciężko by było zmienić to na LinkedList. Oczywiście nie należy na siłę robić ogólnych typów czy interfejsów - jeżeli różnice w funkcjonalności są bardzo duże to nie ma sensu tworzyć ogólnego typu. Ustalenie kiedy uogólnianie jest opłacalne, jest raczej kwestią doświadczenia, musisz porobić trochę projektów, porefaktoryzować i sam się przekonasz.

0

Dzięki Wibowit za wyczerpującą odpowiedz - teraz już jest to dla mnie jaśniejsze :)

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