Kapitola 1. Administrativa & Jemný úvod do OOP 1.1. Organizační informace základní zdroj informací https://portal.zcu.cz/wps/myportal/predmety/kiv/uur [https://portal.zcu.cz/wps/portal/predmety/kiv/uur] a odkaz na KIV/UUR letos (2010/2011) předmět běží teprve čtvrtý rok pravděpodobně se ještě pořád budou objevovat různé mouchy a vady na kráse (a překlepy) přednáší Ondřej Rohlík je zodpovědný za předmět, takže veškeré problémy řešte s ním rohlik@kiv.zcu.cz +420 736 105 259 garantem předmětu je doc. Pavel Herout je také původním autorem asi poloviny přednášek tímto mu děkuji za jejich poskytnutí k rukám doc. Herouta směřujte jen stížnosti na mne ;-) předpokládaná vstupní úroveň znalostí studentů není zcela jasná zdola je ohraničená zkouškou KIV/PPA1 snahou bylo, aby si předmět zapsali studenti, kteří přes zkoušku z PPA1prošli hladce dotazník snad napoví cíl předmětu: přesně podle anotace na courseware: pochopení teorie a získání zkušeností s technologiemi používanými při vytváření prezentační vrstvy programu v Javě semestrální práce je jich hodně a jsou jednoduché (skoro všechny) (možná) suplované přednášky 21.3. a 9.5. (ESA) a 18.4. (NASA) literatura Herout, P., Učebnice jazyka Java, České Budějovice, Kopp 2000 Herout, P., Java : grafické uživatelské prostředí a čeština, České Budějovice, Kopp 2004 1
je to dobrá volba, ale je třeba jasně říct, že kniha je pouze o AWT, zatímco KIV/UUR je o převážně o Swing; samozřejmě Swing je postaven nad AWT a většina knihy má obecnou platnost; knížku obecně doporučuji, ale také doporučuji nejprve si knížku prolistovat v knihovně The Swing Tutorial (na WWW) Swing API (na WWW) 1.2. Jemný technologický úvod do OOP, dědičnosti a rozhraní OOP je zkratka pro Objektově Orientované Programovaní cílem přednášky je vybavit studenty základními znalostmi (povědomím) o objektovém programování ještě před tím než se pustíme do uživatelského rozhraní jako takového -- je-li to por Vás nové, asi budete z přednášky odcházet zmateni a do příštího dne strávíte nějaký čas samostudiem a konzultacemi cílem není diskuse o ideových základech OOP (tj. v čem jsou výhody zapouzdření, dědičnosti, polymorfismu) ani o OO návrhu (to se dozvíte jinde - v předmětu doc. Pavla Herouta OOP), i když se tomu nedá úplně uniknout cílem přednášky tedy je odstranit strach z čtení dokumentace Java Tutorial http://java.sun.com/docs/books/tutorial/ Swing API http://java.sun.com/javase/6/docs/api/ rozumět kódu který od přístě budu používat v přednáškách znát klíčová slova z OOP, abyste si mohli vyhledat a nastudovat detaily na Internetu nebo v knížce od Pavla Herouta 1.2.1. Třída a Objekt znáte z PPA1 (kapitola 7.2 "Třída jako datový typ" v materiálech na PPA1) třída definuje data metody (operace) pracující nad těmito daty public class Tlacitko { private String napis; // data jsou privátní tj. nejsou vidět z venku public String getnapis() { return this.napis; public void setnapis(string napis) { this.napis = napis; 2
třída je šablona podle které se vytvářejí datové objekty (lze říci (ač ne zcela přesně), že třída je typem těchto objetků, tak jako String je typem pro řetězec "foo") objekty se vytvářejí z šablony třídy voláním konstruktoru pomocí operátoru new příklad: JButton tlacitkobt = new JButton("Zrušit"); 1.2.2. Zapouzdření třída realizuje zapouzdření (v terminologii OOP), to znamená, že něco schovává před okolním světem -- v příkladu výše schovává proměnnou napis se schovaným (tj. s proměnnou napis) lze manipulovat pouze prostřednictvím metod (též operací, nebo postaru funkcí) třídy; je důležité, že kromě operací třídy Tlačítko s proměnnou napis nikdo a nic nemůže manipulovat; to dává autorovi třídy jistotu, že jiní programátoři něco nepokazí schvovat lze nejen proměnnou, ale i metodu (operaci) v Javě rozlišujeme čtyři úrovně viditelnosti: private, protected, public a implicitní; pro implicitní viditelnost není definováno žádné klíčové slovo 1.2.3. Dědičnost příklad napoví UML Class Diagram ukazuje dvě třídy: Person a Customer šipka znázorňuje dědičnost a říká: třída Customer je specializovaným (rozšířeným) případem třídy Person tj. (každý) Customer je zároveň i Person (řekněte si to česky a uvidíte, že to dáva smysl) a má/umí něco navíc Person je třída, které může využívat atributy (proměnné) name a address Customer je třída, které může využívat atributy (proměnné) name, address a accountnumber šipka znázorňující relaci dědičnost je na obrázku správně i když se to někomu může zdát nelogické -- směřuje od podtřídy k nadtřídě (from subclass to superclass) říkáme, že třída Customer dědí od třídy Person atributy address a name stejně se dědí i metody -- to na obrázku není vidět, protože Person, žádné metody nemá 3
příslušný kód je uložen v souborech Person.java a Customer.java, všiměte se části "Customer extends Person" public class Person { String address; String name; public class Customer extends Person { int accountnumber; public String tostring() { return " Account number: " + accountnumber + "\n Account holder: "+ name + "\n Address: " + address + "\n"; dědičnost má řadu výhod většinu oceníme teprve později už teď je vidět, že nám může ušetřit hodně kódování, protože naprogramujeme-li třídu Person pořádně (například přidáme operace/metody setname, getaddress apod.), můžeme pak snadno děděním získat třídy Employee, Boss, Benefactor, Child... a u žádné z nich nemusíme znovu kódovat to, co už jsme nakódovali u třídy Person příklad z praxe: třída JComponent má přes 100 metod; třídy JButton nebo JPopupMenu už těchto 100 metod nemusí implementovat -- prostě a jednodušše je zdědí od svého předka -- od nadtřídy JComponent viz http://java.sun.com/j2se/1.4.2/docs/api/javax/swing/jcomponent.html u dědičnosti je běžné, že při návrhu software vznikne netriviální hierarchie objektů (odpusťte autorovi obrázku, že zapoměl nakreslit šipky -- všechny by směřovaly vzhůru) 1.2.4. Polymorfismus, virtální metoda základem polymorfismu je virtuální metoda překrývání metod -- overriding uvažujme třídu odeděděnou od Customer public class NobleCustomer extends Customer { String title; // sir, baron, knight, lord, duke, princ, king public String tostring() { 4
return " Account number: " + accountnumber + "\n Honorable account holder: " + title + " " + name + "\n Address: " + address + "\n"; podívejme se teď na to jaké metody jsou kdy volány public class Test { public static void main(string args[]) { Customer c; NobleCustomer nc; c = new Customer(); c.name = "Ondrej Rohlik"; c.address = "Pilsen"; c.accountnumber = 420820; System.out.println(c.toString()); // Vypíše očekavané: // Account number: 420820 // Account holder: Ondrej Rohlik // Address: Pilsen nc = new NobleCustomer(); nc.name = "Albert II"; nc.address = "Monaco"; nc.accountnumber = 111777; nc.title = "prince"; System.out.println(nc.toString()); // Vypíše očekavané: // Account number: 111777 // Honorable account holder: prince Albert II // Address: Monaco c = nc; // to je BTW pěkný trik: podtřídu lze "uložit" do proměnné typované jako nadtřída System.out.println(c.toString()); // Díky polymorfismu vypíše: // Account number: 111777 // Honorable account holder: prince Albert II // Address: Monaco tostring() je virtuální metoda (ono vlastně všechny metody v Javě jsou virtuální -- narozdíl od některých jiných jazyků jako např. C++, kde je třeba explicitně uvést které metoda je virtuální a které je obyčejná/nevirtuální) selský rozum říká, že když voláme c.tostring() a c je typu Customer, výpis by měl být 5
Account number: 420820 Account holder: Ondrej Rohlik Address: Pilsen trik je právě v polymorfismu volání virtualních metod; JVM si "všimne", že objekt na který referenční proměnná c ukazuje je typu NobleCustomer a tedy zavolá příslušnou metodu tostring(), tak jak je definovaná v NobleCustomer nikoliv tu, která je definovaná v Customer tato vlastnost se jmenuje polymorfismus protože výsledek volání metody mujobjekt.mojemetoda() je závislý na tom, na jaký objekt právě odkazuje mujobjekt tj. při pohledu na kód mujobjekt.mojemetod() není možné říct, co se při stane POKUD existuje více implementací metody mojemetoda() v různých třídách A ZAROVEN POKUD není známo jakého typu je mujobjekt říká se tomu pozdní vazba (late binding or dynamic binding) -- teprve za běhu programu, kdy dojde na volání metody, se zjistí, která z metod mojemetoda() se provede (a to je celkem pozdě, proto se říká late -- je to takříkajíc na posledni chvíli) pozn.: samozřejmě že kdyby žádná tostring() v NobleCustomer definovaná nebyla, JVM zavolá metodu tostring() definovanou v Customer příklad z praxe: třída JMenuItem definuje metodu updateui(); její podtřída JCheckBoxMenuItem má jinou implementaci metody updateui() 1.2.5. Abstraktní metoda někdy je výhodné deklarovat ve třídě virtuální metodu, ale nechat jí nedefinovanou tj. nenapsat jí kód těla třída GrObj2D reprezntuje obecný grafický objekt, u kterého předpokládáme, že má nějakou plochu, a proto deklaruje třída metodu getarea(), ačkoliv sama nemá na výpočet plochy dostatek dat (pozn: atributy x a y v obrázku níže jsou jen souřadnice nějakého význačného bodu tohoto 2D objektu, třeba středu kružnice -- nejsou to rozměry jako např. šířka a výška, ale pouze souřadnice) je logické, že každý "placatý" objekt by měl být schopen poskytnout metodu pro výpočet své plochy rádi bychom do těla metody getarea() napsali i nějaký smysluplný kód, ale u obezného placatého objektu nevíme, jak to spočítat -- to lze jen u konkrétního tvaru jako čtverec, kruh, šestiúhelník atp. tak alespoň oznámíme všem potomkům třídy GrObj2D, že je potřeba oznamovat světu svoji plochu a ať programátoři každému potomku třídy GrObj2D kód doplní podle toho, jak ten který potomek vypadá -- programátor třídy Circle tak doplní tělo: return radius*radius*3.14f; a programátor třídy SnehovaVlocka se asi pěkně zapotí 6
Třída GrObj2D má abstraktní metodu a proto musí být také definovaná jako abstraktní public abstract class GrObj2D { int x; int y; public abstract int getarea(); Třída Circle metodu implementuje public class Circle extends GrObj2D { int radius; public int getarea() { return Math.round(radius*radius*3.14f); Třída Rectangle také public class Rectangle extends GrObj2D { int width; int height; public int getarea() { return width*height; Instanciace a demostrace použití abstraktní metody public class Test { public static void main(string args[]) { GrObj2D[] list = new GrObj2D[2]; Circle c = new Circle(); Rectangle r = new Rectangle(); c.radius = 10; r.width = 10; r.height = 20; list[0] = c; // list[0] je typovaný na GrObj2D, takže do něj můžeme vložit Circle 7
list[1] = r; // i do list[1] lze vložit libovolného potomka for (int i=0; i<2; i++) { System.out.println("Area: " + list[i].getarea()); Vytiskne: Area: 314 Area: 200 Takže jaká je situace? getarea() třídy GrObj2D je sice abstraktní (a nemá definované žádné chování), ale je deklarovaná ve třídě GrObj2D a tedy na jakýkoliv objekt, který lze typovat jako GrObj2D (je odděděný od GrObj2D), lze volat metodu getarea() díky polymorfismu se zavolá "ta správná implemetace metody getarea()" Třídě, které má nějakou (jednu nebo více) abstraktní metodu se říká abstraktní třída 1.2.6. Rozhraní / Interface v dalším textu budu z pedagogických důvodů používat termín interface, i když i český překlad rozhraní je zcela běžně používaný (i když méně často než interface) interface je "něco jako třída, jejíž všechny operace jsou abstraktní" tj. "něco jako abstraktní třída, jejíž všechny operace jsou abstraktní" za takovou definici by mne OOP puristi ukamenovali ve skutečnosti i koncepčně je to trochu jinak, ale pro nás začátečníky je to velmi dobré přirovnání existuje množství literatury, kde se lze dočíst jak to přesně je ukažme si použití interface na příkladě bankovní aplikace která modeluje klienty banky třídou Custmer s atributy name, address, account pobočky banky třídou Branch s atributy address, customer, branch manager bankovní účty třídou Account a atributy owner, balance, currency a mějme následující problém aplicakce by měla periodicky archivovat stav všech klientů, poboček i účtů archivovaná data se budou ukládat do archivního souboru nejprve si ukážeme, špatné řešení -- tzv. object-based programování (bez použití interface): 8
při naivním řešení problému se aplikace poskládá jako množina interagujících objektů jeden Archiver objekt a spousta Customer, Branch a Account objektů ke každé kategorii objektů se nadefinuje konkrétní (tj. ne abstraktní) třída část třídy Customer může vypadat třeba takto: a Archiver takto: metoda doarchive() uloží stav aplikace (tj. stav všech objektů v aplikaci); zpracovává tři typy objektů: Customer, Branch a Account Customer[] customerlist; Branch[] branchlist; Account[] accountlist; void doarchive() { // zpracuj data klientu for (all items c in customerlist) { name = c.getname(); address = c.getaddress(); account = c.getaccount();... // zapis jmeno, adresu a ucet do archivniho souboru 9
// zpracuj data pobocek for (all items b in branchlist) {... // zpracuj data uctu for (all items a in accountlist) {... instanciace aplikace pak může vypadat nějak takto Archiver archiver = new Archiver(); // vytvor archivare Customer c1 = new Customer(); // vytvor objekty (instance) klientu Customer c2 = new Customer();... Customer cn = new Customer(); Branch b1... // vytvor objekty (instance) pobocek Account a1... // vytvor objekty (instance) uctu archiver.addcustomer(c1); archiver.addcustomer(c2);... // nahraj objekty (instance) klientu jeden po druhem do archivare archiver.addbranch(b1);... // nahraj objekty (instance) pobocek jeden po druhem do archivare archiver.addaccount(a1);... // nahraj objekty (instance) uctu jeden po druhem do archivare archiver.doarchive(); // trigger archive toto řešení má řadu nevýhod Archiver je velmi náchylný na změny aplikace -- např. změní-li se třída Customer, musí se změnit i Archiver :-( Archiver sice potřebuje přístup pouze ke get* metodám tříd Customer, Branch a Account, ale ve skutečností má přístup i k dalším datům, protoze get* metody vracejí objekty (v tomto případě String), nad kterými lze provádět další operace a tím je například i měnit (!), což nechce programátorovi třídy Archiver dovolit Tento přístup k objetům typu String není potřeba -- stačilo by nám méně není to pěkné z pohledu zapouzdření 10
a hlavně je to potenciálně velmi nebezpečné, protože Archiver by mohl (kdyby se jeho programátor dopusil chyby, a to se stavá, veřte nebo ne) měnit data klientů, poboček i účtů (a to by od archivačního subsystému jistě nikdo nečekal) Lepší řešení našeho bankovního problému je následující definujeme interface, které zapouzdří operace archivovatelných objektů které Archiver potřebuje volat tím oddělíme Archiver a archivovatelné třídy, takže už na sobě nebudou závislé -- změna jedné třídy tak nevynutí změnu třídy Archiver (a to je pro softwarevé inženýry velmi cenná vlastnost) toto oddělění (anglicky decoupling) se realizuje prostrřednictvím interface Archivable, které budou Customer, Branch a Account implementovat ("jakoby dědit".. je to stejné jako dědičnost, ale v případě interface se neříká "dědit" ale "implementovat") protože interface metody pouze deklaruje, ale nedefinuje (stejně jako abstraktní třída), nutí tím "podtřídy" (správně se jim říká implementující třídy) tyto metody dodefinovat (= napsat jim tělo) Archiver se pak může dívat na třídy Customer, Branch a Account jen jako na Archivable objekty, které implementují metodu getarchiveimage() a nemusí ho zajímat, jak přesně jsou tyto tři třídy implementovány -- stačí, že ví, že metoda getarchiveimage() vrací řetězec (String), který Archiver jen uloží do souboru; získávání dat z tříd Customer, Branch a Account bylo delegováno na třídy samostné (které koneckonců nejlépe ví, jak řetězec nejlépe poskládat... resp. jejich programátoři to ví nejlépe) díky polymorfismu si Archiver může být jistý, že bude vyvolána vždy ta správná metoda kód Archivable je: Interface Archivable { public String getarchiveimage(); 11
kód Archiveru pak vypadá takto (je teď mnohem elegantnejší): Class Archiver { Archivable[] list; // mame jen 1 seznam (už ne 3) void doarchive() { // zpracuj archivable objekty for (all items a in list) { string = a.getarchiveimage(); // a je typu Archivable soubor.println(string) // zapis do souboru retezce vraceneho metodou getarchiveimage() void addarchivable(archivable newitem) { list.add(newitem); A instanciační kód je: Archiver archiver = new Archiver(); // create archiver Archivable c1 = new Customer(); // create customer objects... Archivable cn = new Customer(); Archivable b1... // create branch objects Archivable a1... // create account objects archiver.add(c1);... // load customer objects archiver.add(b1);... // load branch objects archiver.add(a1);... // load account objects archiver.doarchive(); // trigger archiver Archivable je opravdu a doslova definice "rozhraní", která se třídy Customer, Branch a Account zavazují splnit (chcete-li poskytovat). Archiver se tak může spolehnout, že když zavolá metody getarchiveimage() dostane relevantní data (tedy nejaky smysluplny String). Říkáme, že rozhraní Archivable je (tj. funguje jako) smlouva (contract) mezi Archiver a třídami Customer, Branch a Account (odtud slavné "design by contract"). Všiměte se, jak se nám aplikace zlepšila z hlediska její údržby. Když nyní (po letech bezproblémového provozu) potřebujeme vyměnit část aplikace Customer za novější a lepší verzi (řekněmě NobleCustomer), nemusíme nijak editovat, překládat a testovat třídu Archiver. To je velká úspora času programátorů. Příklad z praxe: interface Observable (http://java.sun.com/j2se/1.4.2/docs/api/java/util/observable.html [<?xml version="1.0"?> <code >http://java.sun.com/j2se/1.4.2/docs/api/java/util/observable.html</code > ]) je pro vývoj grafického uživatelského rozhraní naprosto nepostradatelná; probereme jej na třetí přednášce. 12
1.2.7. Další čtení http://java.sun.com/docs/books/tutorial/java/concepts/object.html http://java.sun.com/docs/books/tutorial/java/concepts/class.html http://java.sun.com/docs/books/tutorial/java/javaoo/accesscontrol.html http://java.sun.com/docs/books/tutorial/java/iandi/subclasses.html http://java.sun.com/docs/books/tutorial/java/iandi/createinterface.html 13