Kurs Java Podstawy - rozszerzony

Polimorfizm

Kończąc rozważania na temat programowania obiektowego nie można nie wspomnieć o rzeczy, na której to wszystko się opiera, chodzi oczywiście o wspominany już w lekcji o interfejsach, czy klasach abstrakcyjnych polimorfizm. Znakomicie oddaje on zasadę IS-A, czyli "jest" (na przykład koń jest zwierzęciem). W Javie każda klasa, czy tego chcemy, czy nie, rozszerza klasę Object. Jej instancję można więc przypisać zarówno do referencji o jej typie, a także do referencji typu Object. Poprawna jest więc poniższa klasa:

public class Main {
	public static void main(String[] args) {
		String str1 = new String("Siabada1");
		Object str2 = new String("Siabada2");
		System.out.println(str1);
		System.out.println(str2);
	}
}

Tworzymy dwa obiekty typu String, jednak przypisujemy je do referencji dwóch różnych typów. Pierwszy przypadek jest oczywisty. Drugi jest dozwolony, ponieważ jak wspomniałem zachodzi tutaj zasada "String jest obiektem". Wyświetlenie obu wartości także wiąże się z ciekawym zjawiskiem - wywołana jest metoda toString() (przypominam, że o ile nie jest to dla nas uciążliwe, powinno się przeciążać metodę toString() klasy Object) z typu obiektu, a nie referencji, u nas wyświetlą się dwa ładne napisy (dla testu przypisz do drugiej referencji obiekt typu Object).

I tutaj zaczyna się ujawniać po co tak właściwie są nam interfejsy i klasy abstrakcyjne. Wyobraźmy sobie, że tworzymy naprawdę duży projekt, składający się z co najmniej kilkudziesięciu tysięcy linii kodu. Używaliśmy w nich obiektów o nazwach X01, X02, ..., X99. Na szczęście zmienne tworzyliśmy tylko w kilku punktach programu, a później wykonywaliśmy na nich bardzo dużo operacji. Wszystkie posiadały takie same metody, jedynie różniły się implementacją. Ponieważ referencje były różnych typów niestety nie bardzo możemy skorzystać z pętli. Mniej więcej:

public class Suma {
	X1 x1 = new X1();
	X2 x2 = new X2();
	...
	X99 x99 = new X99();

	void pewnaMetoda() {
		x1.doSomething();
		x2.doSomething();
		x3.doSomething();
		x4.doSomething();
		...
	}
}

Jak widać nie jest to niestety zbyt piękne rozwiązanie, ale właśnie dlatego powstał polimorfizm. Skoro wiemy, że wszystkie klasy X1 ... X99 reprezentują tak naprawdę bardzo podobne obiekty o podobnych cechach możemy je powiązać ze sobą za pomocą interfejsu, który będzie pewnym "kontraktem", mówiącym o tym, że klasy go implementujące, muszą zawierać pewną metodę doSomething(). Dzięki temu, że wszystkie takie obiekty moglibyśmy teraz umieścić przykładowo w tablicy, nieprawdopodobnie zmniejszyli byśmy rozmiar kodu, a tym samym zwiększyli jego czytelność.

public interface X {
	public void doSomething();
}

public class X1 implements X {
	public void doSomething(){
		System.out.println("X1");
	}
}

public class Suma {
	X[] tab = new X[100];
	tab[0] = new X1();
	tab[1] = new X2();
	...
	tab[99] = new X99();

	void pewnaMetoda() {
		for(X x: tab)
			x.doSomething();
	}
}

Jak widać dzięki zastosowaniu tablicy skróciliśmy kod o 100 linii.

Istnieją jeszcze oczywiście dużo gorsze przypadki. Tym razem wyobraźmy sobie, że mamy równie ogromny kod, z niezliczoną ilością metod, które cechują się mniej więcej takimi sygnaturami:

import java.util.ArrayList;

public class X {
	public void doSomething1(ArrayList<String> list) {
		//coś tam robi z listą
	}

	public void doSomething2(ArrayList<String> list) {
		//też coś tam robi
	}
//itd
}

Metody te wywołujemy w tysiącach miejsc, przekazując im oczywiście listę tablicową ArrayList, którą natomiast utworzyliśmy tylko w jednym miejscu. Następnego dnia przychodząc do pracy stwierdzamy, że jednak wykorzystując listę wiązaną LinkedList wydajność wzrośnie o kilkaset %. Co możemy zrobić? Oczywiście zmienić typ listy, ale zaraz zaraz ... przecież przekazywaliśmy ją w kilku tysiącach miejsc jako argument, a klasa LinkedList nie ma związku z ArrayList i otrzymamy błąd kompilatora informujący o niezgodności typu. No cóż nie pozostaje nic innego jak zmienić te kilka tysięcy miejsc ręcznie, lub używając Refactoringu, co może prowadzić do powstawania nowych błędów. Uf, po problemie, ale zaraz, znalazłem jeszcze wydajniejsze rozwiązanie ...

Można to kontynuować bez końca. Powyższy przykład jest dosyć życiowy, w Javie mamy bowiem dostępny Collections Framework, który udostępnia różne struktury danych, w tym wspomniane listy. Gdybyśmy nasz powyższy kod (sygnatury metod) zmienili na poniższy:

import java.util.ArrayList;

public class X {
	public void doSomething1(List<String> list) {
		//coś tam robi z listą
	}

	public void doSomething2(List<String> list) {
		//też coś tam robi
	}
}

Uniknęlibyśmy wszystkich nieprzyjemności. Niezależnie, czy przekazalibyśmy teraz metodom obiekty ArrayList, czy LinkedList to z pewnością w większości działałyby bez problemu i nie wymagały zmian (chyba, że często używaliśmy metod specyficznych dla danej klasy, wtedy już nic nas nie ratuje).

Z interfejsami nie można przesadzać też w drugą stronę, jeżeli coś spełnia regułę "jest", i można by ją powiązać z innymi klasami ponieważ zawierają 1 podobną metodę, natomiast dodatkowo posiadają 20 innych, specyficznych dla siebie to stosowanie interfejsu nie ma tu sensu. Polimorfizmu, interfejsów, a także klas abstrakcyjnych używajmy więc przede wszystkim tam gdzie wiele obiektów posiada podobną funkcjonalność i można je powiązać ze sobą, a tym samym uprościć kod i uczynić go bardziej elastycznym na zmiany - dodając nową "wytyczną" w interfejsie, będziemy wiedzieli gdzie powinniśmy zaimplementować daną funkcjonalność.

O ile interfejsy i budowanie abstrakcji daje wielkie możliwości, tak również trzeba sobie zdawać sprawę, że podejście takie nie jest wolne od błędów. Załóżmy, że posiadamy interfejs A oraz klasę B, która go implementuje:

public interface A {
	public void x();
}

public class B implements A{
	@Override
	public void x() {
		//do something
	}

	public void y() {
		//do something
	}
}

public class Test {
    public static void main(String[] args) {
    	A obiekt = new B();
    	obiekt.x();
    	obiekt.y(); //błąd, interfejs A nie posiada metody y()
    	((B)obiekt).y(); //ok, dzięki rzutowaniu uzyskujemy dostęp do metody y()
    }
}

Przypisując obiekt B do referencji typu A mamy dostęp tylko to metod, które znajdują się w typie referencji, w tym przypadku interfejsu A. Bardzo dobrym przykładem jest tutaj ponownie interfejs List i klasy go implementujące ArrayList i LinkedList.

Interfejs udostępnia jedynie podstawowy zestaw metod, takich jak add(), remove(). Klasy ArratList i LinkedList posiadają jednak dużo innych przydatnych metod, do których jednak dostęp uzyskamy, albo po rzutowaniu, albo od razu deklarując referencje jako odpowiedni typ (a nie ogólny List). Budując abstrakcję w oparciu o interfejsy należy więc dążyć do tego, aby posiadały one wszystkie metody, które mają mieć klasy je implementujące.

<- Poprzednia LekcjaNastępna Lekcja ->

Komentarze

rh

W przykładzie:
public class X1 {
public void doSomething() {
System.out.println("X1");
}
}
brakuje "implements X" ?

Pawel

Tu: tab[99] = new X99(); powinno być :
tab[99] = new X100();
Cały kurs do tej pory zrozumiałem, ale 3-ch ostatnich lekcji nie wiem o co chodzi.

Michal

Czy w javie jest coś jak typy ogólne(z C#) czy szablony(z C++)?

Jeśli tak to miło by było, gdyby kurs był o to uzupełniony. Bo w sumie z ważniejszych rzeczy tego brakuje.

Slawek

oczywiście są typy generyczne.

wojtekm

bo są strasznie abstrakcyjne przykłady:) mnie uczyli polimorfizmu na takim przykładzie:
public interface Figura {
public void obliczPole();
}
public class Koło implements Figura{
int promien;
//jakiś konstruktor
Koło(int promień){
this.promien=promien;
}
@Override
public void obliczPole() {
return pi*promien*promien;
}
}

public class Prostokąt implements Figura{
int a,b;
//jakiś konstruktor
Prostokat(int a,int b){
this.a=a;
this.b=b;
}
@Override
public void obliczPole() {
return a*b;
}
}
public class Test {
public static void main(String[] args) {
Figura obiekt = new Koło(10);
Figura obiekt1 = new Prostokąt(10,2);
obiekt.obliczPole(); // i w tym momencie program wie, że ma obliczyć pole ze wzoru na koło, bo obiekt jest kołem, a z drugiej strony program wie, że obiekt jest też figurą, możemy zrobić sobie jakąś listę elementów typu figura, i przelecieć ją pętlą nie wnikając czy w danej chwili figura jest kołęm czy kwadratem, a jak nam się zamarzy dodać trapez to jest to tylko kwestia dodania nowej klasy i zaimplementowania metody obliczPole w niej.

}
}

Slawek

Jak tak na to teraz spojrzałem, to faktycznie mogłem tą lekcję zacząć od czegoś "lżejszego", figury geometryczne - klasyka :) Dzięki za przykład, jak wrócę do domu to sformatuję Twój komentarz, żeby były wcięcia.

m

Trochę długo Pan wraca do domu, Panie Sławku. Czyżby eksmisja?

shiva

nadgodziny

pain_elemental

może po prostu wcięcia się skończyły?

Piotrek

Tako moja mała obserwacja, w powyższym kodzie masz błędy, w miejscach gdzie implementujesz metody obliczPole stosujesz return, a w metodach gdzie tryp zwracanych danych jest void nie użyje się return, a można zastosować np. System.out.println(a*b)

snt.banzai

coś jest na rzeczy

mandragorn

totalna pustka w glowie mi sie zrobila :D przydalyby sie jakies zadania

dami

czy mozna prosic o jakies zadania i rozwiazania zeby mozna bylo sobie pocwiczyc? :) na pewno sporo osobom by sie przydało:)

andrzejK

Do interfejsów i polimorfizmu wymyśliłem sobie przykład w którym:

1) klasa [pojazd]
2) ma zaimplementowany interfejs [kieruj] z metodą [hamuj]
3) klasy[auto, statek, motorynka] rozszerzają klasę [pojazd]

i w jednym momencie wywołuję, że wszystkie pojazdy (auta, statki, motorynki) hamują !!!

Oczywiście dla każdego obiektu hamowanie wygląda inaczej (zależnie od zdeklarowanej metody hamuj w poszczególnych klasach).

przekroczyłem 5 lini, więc rozwiązanie umieszczam na FORUM:
http://forum.javastart.pl/Thread-Interfejsy

może się komuś przyda ;p

Królik

W tym przypadku bez problemu można dopisać do klasy auto całkiem osobną metode np "pal gume"?

velmafia

w listingu 2 i 5 dwukrotnie powtarzasz deklaracje klasy:
public class Suma {
public class Suma {

to chyba błąd? :P

Sławek Ludwiczak

To tylko schemat problemów jakie pozwala rozwiązać polimorfizm, 5 jest swego rodzaju uogólnieniem 2 listingu.

spinkey

Czy mógłby ktoś wytłumaczyć pętlę: for(X x: tab)? Z góry dzięki :)

Lolo

Zamieńmy zapis for(X x: tab) na cos mniej abstrakcyjnego np.
for(int i: tablicaInt) i taki zapis mówi nam:
Dla każdego kolejnego obiektu o nazwie tablicaInt typu int przypisz wartość do zmiennej o nazwie i typu int.
Teraz zasada działania takiej pętli.

Z każdym "obrotem" pętli do zmiennej i będzie przypisywana kolejna wartość z tablicaInt aż do przejścia całej tablicy.

Mam nadzieję, że moje tłumaczenie było zrozumiałe.
Wydaje mi się, że ten przykład jest bardziej zrozumiały niż gdybym miał tłumaczyć na tamtym przykładzie z klasą X.

Janosch

Chciałbym się spytać czy dobrze rozumiem polimorfizm i te interfejsy na wymyślonym przeze mnie przykładzie:
Przykład: Chcę narysować koło lub kwadrat. Jestem przy biurku, mam do dyspozycji zeszyt i długopis.

Tak to sobie wyobrażam:
Żeby narysować koło muszę w kolejności:
1. przygotować biurko
2. wziąć zeszyt i wyrwać kartkę
3. wziąć do ręki długopis
4. narysować koło

Żeby narysować kwadrat muszę w kolejności:
1. przygotować biurko
2. wziąć zeszyt i wyrwać kartkę
3. wziąć do ręki długopis
4. narysować kwadrat

W powyższym przypadku interfejsem będą punkty 1,2,3, a klasy je implementujące będą miały dodatkowo punkt 4.

Kod interfejsu, klas i klasy testującej jest na forum, link poniżej.
http://forum.javastart.pl/Thread-Polimorfizm?pid=1055#pid1055

Pootin

Napisane jest "Używaliśmy w nich obiektów o nazwach X01, X02, …, X99", a za chwilę "Skoro wiemy, że wszystkie klasy X1 … X99 reprezentują'

Tak jak patrzę na kod:
X1 x1 = new X1();

to X1 to klasa a x1 to obiekt, tak? Więc powinno być x1,x2... zamiast X01,X02,... w pierwszym zacytowanym zdaniu, zgadza się?