====== 2 - Polymorfismus, přepisování metod, double dispatch ====== * pro vyučující: [[:courses:a0b36pr2:internal:tutorialinstruction:02:start|]] /* ===== Semestrální práce ===== Poslední upozornění, vybírejte si zadání semestrální práce, pokud si jej do příštího týdne nevyberete, bude Vám přiděleno. Návrhy semestrálních prací naleznete v sekci [[:courses:a0b36pr2:tutorials:semestralky:start|Témata semestrálních prací]]. */ ===== Polymorfizmus ===== ==== Úvod do problematiky - Úkol 1. ==== Stáhnětě si výchozí projekt {{:courses:a0b36pr2:labs:lab02.zip|lab02}}, který obsahuje základ pro naši další dnešní práci. Je těžce inspirován [[http://docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html|turoriálem]] od Oracle. Projekt obsahuje následující třídy: * ''Bicycle'' - reprezentuje jízdní kolo * ''TestBikes'' - obsahuje pouze ''public static void main()'', kde budeme naše výtvory zkoušet Slovníková definice polymorfismu odkazuje na zásady v biologii, v níž organismus nebo druh může mít mnoho různých forem nebo fáze. Tento princip lze také použít k objektově orientovanému programování a jazyků, tedy i v jazyku Java. Podtřídy třídy mohou definovat své vlastní jedinečné chování a přesto sdílejí některé stejné funkčnosti nadřazené třídy. Polymorfismus může být demonstrován na třídě ''Bicycle''. Ta obsahuje metodu ''printDescription()'', která vypíše informace o všech datech dané instance. * Napište třídy ''MountainBike'' a ''RoadBike'', které budou reprezentovat horské a silniční kolo, a které budou dědit od třídy ''Bicycle''. * ''MountainBike'' bude mít navíc atribut ''String suspension'' označující, jakým druhem odpružení kolo disponuje (''"Front"'' pro přední, ''"Dual"'' pro přední i zadní). * ''RoadBike'' bude mít navíc atribut ''int tireWidth'' označující šířku pneumatiky v milimetrech (protože siniční kola mívají pneumatiky úzké). Nyní bychom chtěli, aby ''printDescription()'' vypisovalo všechny informace o daném kolu: Bicycle bike01, bike02, bike03; bike01 = new Bicycle(20, 10, 1); bike02 = new MountainBike(20, 10, 5, "Dual"); bike03 = new RoadBike(40, 20, 8, 23); bike01.printDescription(); bike02.printDescription(); bike03.printDescription(); Jenže výstup je následující: Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10. Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10. Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20. Informace o odpružení (u druhého kola) a o šířce pneumatik (u třetího kola) chybí. Návrhy jak to vyřešit? * Přepište metodu ''printDescription()'' u potomků třídy ''Bicycle'' tak, aby vždy zahrnovala data specifická pro dané kolo. Tedy, existují tři třídy: ''Bicycle'', ''MountainBike'' a ''RoadBike''. Dvě podtřídy přepisují metodu ''printDescription()'' a tisknou jedinečné informace. Jak to, že se volá správná metoda, ačkoliv všechny proměnné jsou typu ''Bike''? Java Virtual Machine (JVM) volá příslušnou metodu pro objekt, na který se odkazuje každá z proměnných. Není to volání metody určené podle typu proměnné. Toto chování se označuje jako //volání virtuální metody// a ukazuje určitý aspekt důležitých polymorfních rysů v jazyce Java. ==== Servis kol - Úkol 2.==== Navrhněte servis kol. Servis příjmá různá kola a může mít různé specializace na opravy. Například může být servis, který se specializuje jen na ''RoadBike'' ale zvládne i údržbu běžného kola. Zkusme navrhnout následující 3 servisy: * ''BasicService'' s metodou ''void accept(Bicycle)'' - vypíše ''"fixing Bicycle"'' * ''MountainBikeService extends BasicService'' s metodami ''void accept (Bicycle)'' a ''void accept (MountainBike)'' - druhá z metod přijme jen ''MountainBike'' a vypíše ''"fixing MountainBike"'' * ''RoadBikeService extends BasicService'' s metodami ''void accept (Bicycle)'' a ''void accept (RoadBike)'' - druhá z metod přijme jen ''RoadBike'' a vypíše ''"fixing RoadBike"'' Nyní zkusme zakomponovat do metody main tyto servisy a předejme jim kola. ==== Život není snadný, ale nevzdávejme se - Úkol 3.==== Servis nám příjmá kola jako ''Bicycle''. Co s tím? Nápady? * Detekce a přetypování * Atribut typ pro každou třídu kola a dle něj přetypujeme * Nějaký magický trik Zkusme tedy použít kouzlo, jistě ho oceníte. Nejprve, ale řekněme co je zde za problém. Pokud uděláme toto ''Bicycle bike = new MountainBike(20, 10, 5, "Dual");'' a zavoláme ''bike.printDescription();'' tak se správně zavolá virtuální metoda z ''MountainBike''. Pokud ale zavoláme na ''mountainBikeService.accept(bike)'', tak si compiler myslí, že má Bicycle. Jak na to? Znovu pro pořádek: - zavoláme ''bike.printDescription();'' a správně se zavolá virtuální metoda z MountainBike - zavoláme na ''mountainBikeService.accept(bike)'' a compiler si myslí, že má Bicycle. Co tedy s tím? Spojme to a udělejme //double dispatch//. Přidejme metodu ''visit(BasicService bs)'' do každého typu kola! Každý typ kola pak zavolá v metodě visit ''bs.accept(this);''. Zopakujme si to: - Zavoláme ''bike.visit(mountainBikeService)'' - to zavolá virtuální metodu ''visit'' na ''MountainBike'' - nic nového. - V těle ''visit'' se do ''mountainBikeService'' předá ''this'', jenže co je nyní this? ''this'' je nyní ''MountainBike'' a ne ''Bicycle''. Proč? Protože jsme zavolali virtuální metodu nad ''MountainBike''. Tento trik, je opět "best practice" a nazývá se visitor. - Je třeba aby každý servis implementoval metody ''accept'' pro všechny typy kol. - Každé kolo pak musí mít vlastní metody ''visit'', i když vypadají stejně. ===== Řešení ===== Řešení celého cvičení ke stažení: {{:courses:a0b36pr2:labs:lab02-solved.zip|zde}}. Naší práci budeme potřebovat příští týden, takže toto téma nepodceňujte. ===== Bonusy, poznámky, další... ===== ==== Přetížení vs přepsání (overloading vs overriding) ==== Zdánlivě podobné, avšak zcela rozdílné koncepty, které si nesmíte plést. === Přetížení === Přetížení (angl. //overloading//) je případ, kdy jedna třída obsahuje více metod se stejným jménem, ale **různou signaturou**. Příklad: class Printer { public void print(String string) { System.out.println(string); } public void print(int integer) { System.out.println(integer); } } Třída obsahuje dvě metody. Obě se jmenují ''print'', nicméně jedna z nich bere argument typu ''String'' a druhá argument typu ''int''. O metodě ''print'' řekneme, že je přetížená. Ve cvičení jsme se s přetížením setkali na samém závěru u třídy ''BasicService'', která měla 3 varianty metody ''accept''. === Přepsání === Přepsání (angl. //overriding//) je případ, kdy třída A má nějakou metodu a třída B, která je potomkem třídy A, definuje vlastní implementaci této metody, neboli ji přepisuje. Při volání takové metody na objektu deklarovaného jako typ A se až v momentě volání zvolí, zda-li se volá metoda třídy A (pokud objekt je skutečně typu A), nebo metoda třídy B (pokud objekt je skutečně typu B). Příklad: class Printer { public void print(String string) { System.out.println(string); } } class VerbosePrinter extends Printer { @Override public void print(String string) { System.out.println("I am now printing the following string: \"" + string + "\""); } } Třídou "A" je zde třída ''Printer'', třídou "B" je zde třída ''VerbosePrinter'', přepisovanou (a jedinou) metodou je zde metoda ''print''. ==== Abstraktní třídy a metody ==== V Javě je možné vytvořit takzvanou abstaktní metodu. Jedná se o metodu, u které je definovaná pouze její signatura, ale nikoliv její implementace. Příklad definice abstraktní metody: public abstract void foo(); Třída, která obsahuje alespoň jednu abstraktní metodu, musí být definována jako abstraktní třída: public abstract class Abstr { public abstract void foo(); } Obráceně však tato implikace neplatí, tzn. můžeme vytvořit abstraktní třídu (použitím klíčového slova ''abstract''), která však bude mít všechny své metody implementované. Abstraktní třídy se vyznačují tím, že není možné vytvářet jejich instance. Tzn. volání typu Abstr x = new Abstr(); nebude možné zkompilovat. Nicméně, levá část (tzn. před rovnítkem) je zcela v pořádku a je možné mít proměnné typované na abstraktní třídu. Budeme-li tedy mít nějakého ne-abstraktního potomka (ne-abstraktní = nemá žádnou abstraktní metodu = všechny abstraktní metody má implementované), můžeme vesele vytvářet instance. Příklad: public class Concr extends Abstr { public void foo() { System.out.println("foo"); } } Potom již lze udělat následující volání Abstr x = new Concr(); x.foo(); a správně se zavolá metoda ''foo'' na třídě ''Concrete'' (klasické přepsání či overriding metody a virtuální volání). ==== Rozhraní (interface) ==== Rozhraní je velice podobné abstraktním třídám. Jedná se o "něco jako třídu", zkrátka rozhraní, které definuje metody, ale nikoliv jejich implementace. To znamená, že v rozhraní jsou vždy všechny metody abstraktní, a proto se také nemusí psát (a ani nepíše) klíčové slovo ''abstract''. Příklad definice rozhraní: public interface Iface { public void foo(); } Rozhaní pak jednotlivé třídy **implementují**: public class Impl implements Iface { public void foo() { System.out.println("foo"); } } Třída, která implementuje nějaké rozhraní, **musí** implementovat všechny metody rozhraní. Nicméně existuje výjimka z tohoto pravidla, a to abstraktní třídy. Pokud je třída abstraktní, tak nemusí implementovat všechny metody (nebo žádnou) ze svých rozhraní. Nebo obráceně - pokud necheme implementovat všechny metody z rozhraní, musíme třídu označit jako abstraktní. Příklad: public abstract class AbstractImpl implements Iface { } Rozhraní navíc fungují jako typ. Tzn. je možné (a velmi často vhodné) volání Iface x = new Impl(); Vám známý ''List'' je také rozhraní, nikoliv třída. Jednotlivé implementace (''ArrayList'', ''LinkedList'') toto rozhraní implementují. Rozhraní mezi sebou také mohou mít hierarchii, tzn. jedno rozhraní může být potomkem (dědit od) rozhraní jiného. Příkladem budiž opět známá rozhraní ''List'' a ''Collection'', kde ''List'' je potomkem ''Collection''. Chová se to pak podobně jako u tříd, tzn. potomek má metody jak své tak metody předka, ale jelikož u rozhraní nejsou žádné implementace, tak přepisování (overriding) zde postrádá smysl. === Proč rozhraní === Nabízí se otázka: proč máme v Javě dva podobné mechanismy, a to abstraktní třídy a rozhraní? Důvod je jednoduchý: v jazyce Java může třída dědit pouze od jedné třídy, ale může implementovat libovolný počet rozhraní. Rozhraní se obecně využívají právě pro definici **rozhraní**. Tzn. v rozhraní popíši, jak chci, aby s objektem šlo manipulovat (jaké operace na něm budou existovat) a implementaci nechám na konkrétních třídách. Jelikož jsem udělal definici skrze rozhraní, konkrétní třídy stále mohou dědit od jiných tříd. Krásný příklad je třída ''Thread'' a rozhraní ''Runnable''. Jedná se o vlákna, ke kterým se dostaneme až za několik týdnů, ale o samotná vlákna tu nejde. Jde o to, že vlákno je možné vytvořit dvěma způsoby: - Vytvořím potomka třídy ''Thread''. - Vytvořím třídu implementující ''Runnable'' a tu pak vložím jako argument do konstruktoru ''Thread''. První možnost je nejjednodušší, ale už si tím zavřu dveře k dědění od jiné třídy, takže nemůžu použít jinou funkcionalitu. Když ale implementuji rozhraní ''Runnable'', tak jsem pořád schopen vlákno vytvořit, ale stále můžu dědit od jiné třídy.