Kapitola 1. Administrativa & Jemný úvod do OOP

Obsah

1.1. Organizační informace
1.2. Jemný technologický úvod do OOP, dědičnosti a rozhraní
1.2.1. Třída a Objekt
1.2.2. Zapouzdření
1.2.3. Dědičnost
1.2.4. Polymorfismus, virtální metoda
1.2.5. Abstraktní metoda
1.2.6. Rozhraní / Interface
1.2.7. Další čtení

1.1. Organizační informace

  • základní zdroj informací

  • 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)

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; }
        }
    • 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á

  • 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() { 
        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()

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í

  • Třída GrObj2Dabstraktní 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

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):

  • 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.