Obsah
základní zdroj informací
https://portal.zcu.cz/wps/myportal/predmety/kiv/uur
a odkaz na KIV/UUR
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 PPA1 proš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
1 kus + report + test GUI
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
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)
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
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; } }
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");
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
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á
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)
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() { 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
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()
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í
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 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
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):
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 } // 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í
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(); }
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.addArchivable(c1); . . . // load customer objects archiver.addArchivable(cn); . . . // load customer objects archiver.addArchivable(b1); . . . // load branch objects archiver.addArchivable(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
)
je pro vývoj grafického uživatelského rozhraní naprosto
nepostradatelná; probereme jej na třetí přednášce.
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