Silná káva pro objektového programátora

Když jsem si v sedmém čísle přečtl článek pana Viriuse "Jak jsem potkal Javu", měl jsem nejprve pocit, odpovídající titulku: vždyť to tak není?!? Po druhém přečtení  jsem si musel názor opravit: ono to tak většinou je, pan Virius je příliš dobrý profesionál, než aby – až na drobnosti – napsal něco, co není pravda. Jenže... jenže jako programátor v neobjektovém a špatně navrženém C++ prostě neví, v čem jsou silné a slabé stránky skutečně objektového programovacího jazyka, takže sice skvěle popíše drobné syntaktické a sémantické odlišnosti, ale vůbec se nezmíní o podstatných rozdílech.

Rád bych proto tímto článkem jeho seriálek ne opravil – protože na víceméně bezchybném textu není co opravovat – ale doplnil tak, aby čtenář získal celkový přehled o tom, co mu Java nabízí oproti C++ nového,  a naopak, co starého není nadále k dispozici. Většinou budu reagovat na konkrétní údaje a informace z článků v sedmém a v osmém čísle; někdy však se rozepíši samostatně o věcech, jimž pan Virius nevěnoval ani zmínku.

Interpretovaný? Ano, ale...

Java skutečně je standardně interpretovaná, což zvyšuje její přenositelnost a snižuje efektivitu. Ovšem, není to jediné řešení! Pro případy, kdy přenositelnost není tak zásadní, ale efektivita ano, se ještě pořád Javy nemusíme vzdávat: skvělý systém překladačů EGCS (nástupce známého GNU C) Javu dokáže překládat do strojového kódu téměř libovolného mikroprocesoru! Navíc, jako všechny GNU produkty, je k dispozici pro skoro jakékoli prostředí, a zcela zdarma.

Čistě objektová znamená něco jiného!

"Čistá objektovost" Javy neznamená, že by "úplně všechno byly objekty" – což je sice hezká idea, ale v praxi trochu problém, ale to, že její objekty jsou skutečnými objekty, tj. uzavřenými černými skříňkami, se kterými můžeme podle potřeby komunikovat, ale které teprve samy naše pokyny interpretují – a pokud to rozumně nejde, vyvolají dobře definovanou chybu. V C++ je tomu právě naopak: zde je "objekt" vlastně pořád proměnná typu struct s řadou speciálních služeb navíc, a její nekorektní použití vede k nepředpověditelnému zhroucení programu, či ještě hůř k jeho nesprávné funkci... Nejlépe je to vidět na typické situaci, kdy prostě voláme metodu:

    NejakaTrida *oc=new NejakaTrida;
    oc->nejakaMetoda(); // C++

    NejakaTrida oj=new NejakaTrida();
    oj.nejakaMetoda(); // Java

Tyto zápisy v C++ i Javě jsou si syntakticky velmi podobné, a sémanticky jsou – na první pohled – totožné: v obou případech se vytvoří nová instance třídy NejakaTrida, odkaz na ni se uloží do proměnné, a pak se prostřednictvím proměnné vyvolá její metoda nejakaMetoda. Zásadní rozdíl ovšem spočívá v tom, že v Javě každý objekt ví, co je zač a podle toho sám interpretuje to, co po něm někdo jiný chce. Ukažme si konkrétní ilustrace těchto rozdílů, opět na – na první pohled – prakticky totožném kódu:

    // pokračování minulého příkladu
    UplneJinaTrida *qc=(UplneJinaTrida*)oc;
    qc->uplneJinaMetoda(); // C++

    UplneJinaTrida qj=(UplneJinaTrida)(Object)oj;
    qj.uplneJinaMetoda(); // Java

Příklad v C++ je zřejmý: prostě zkusíme vyvolat metodu z jiné třídy, přičemž program zcela nepředpověditelným způsobem spadne, nebo dokonce poběží dál, ale po provedení naprosto neočekávané a nesmyslné akce!!! Jistě, lze namítnout, že přetypování ('typecast') samo o sobě je "nedoporučeníhodná" věc, která si říká o problémy, jenže v praxi bez něj programovat prostě nelze. Můžeme se v extrémním případě vyhnout explicitním přetypováním, ovšem stále nám zůstanou implicitní: např. všechny metody všech knihovních tříd jsou přece implicitně otypovány podle toho, jaké headery použijeme – pokud jsou třeba náhodou novější, s novými či prostě jinými virtuálními metodami oproti headerům, jež byly použity při tvorbě knihoven, máme v C++ závažný problém.

V Javě nic podobného nehrozí. Především, už samotný překladač by poznal, že instance třídy NejakaTrida a UplneJinaTrida navzájem přetypovat nelze, a program by vůbec nepřeložil; proto musíme hodnotu nejprve přetypovat na universální třídu Object. Pak se program korektně přeloží, ovšem při spuštění nedojde k žádné nepředpověditelné chybě: namísto toho se vyvolá jasně definovaná výjimka; v Javě už ve chvíli, kdy nekorektní přetypování provádíme. Ukažme si zcela konkrétní příklad:

    29 ~/Library/Java\> cat Test.java
    class Ttt {
        public void metoda() {
                System.out.println("metoda");
        }
    }
    class Sss {
    }

    class Test {
        public static void main(String[] args) {
                Sss s=new Sss();
                System.out.println("aaaa");
                Ttt t=(Ttt)(Object)s;
                System.out.println("bbbb");
                t.metoda();
        }
    }
    30 ~/Library/Java\> javac Test.java
    31 ~/Library/Java\> java Test      
    aaaa
    java.lang.ClassCastException: Sss
        at Test.main(Test.java:13)
    32 ~/Library/Java\>

Možná stojí za další ukázku to, že nejde o nějaké specifikum Javy – takto korektní chování je dáno rozumnou podporou objektů. Např. obdobný program v Objective C – až na oddělená rozhraní a implementace tříd je téměř přesně stejný, jako minulý příklad v Javě -- také ohlásí chybu (v tomto případě při pokusu volat metodu metoda):

    50 /tmp\> cat test.m
    #import <Foundation/Foundation.h>
    @interface Ttt:NSObject
    -(void)metoda;
    @end
    @interface Sss:NSObject @end

    void main()
    {
        Sss *s=[Sss new];
        Ttt *t;
        printf("pretypovani je v ObjC bez problemu...\n");
        t=(Ttt*)s;
        printf("...ale pokus o volani neexistujici metody ohlasi slusne chybu!\n");
        [t metoda];
    }

    @implementation Ttt
    -(void)metoda {
        printf("metoda");
    }
    @end
    @implementation Sss @end
    51 /tmp\> cc -Wall -framework Foundation test.m ; ./a.out
    pretypovani je v ObjC bez problemu...
    ...ale pokus o volani neexistujici metody ohlasi slusne chybu!
    Aug 07 14:54:53 a.out[3693] *** Uncaught exception: <NSInvalidArgumentException> *** -[Sss metoda]: selector not recognized
    52 /tmp\>

Můžeme jít ještě dál: v Javě vůbec není zapotřebí znát třídu objektu k tomu, abychom mohli korektně volat jeho metodu (nebo, protože objektová terminologie skutečně je pro Javu dalsko vhodnější, abychom mu mohli zaslat zprávu, na základě níž sám objekt vhodnou metodu provede). Podobně jako u metod tříd, zmíněných níže, zde narážíme na umělé omezení – samotný jazyk Java to prostě neumí (překladač dělá podobnou hloupost jako C++, že korektní, byť podezřelé akce hlásí jako chyby, ačkoli by mělo jít o warningy). Proto musíme využít knihovní služby; zde je využit package apple.com.foundation, který nabízí velmi pohodlnou třídu NSSelector. Pokud bychom tento package neměli k dispozici, museli bychom použít standardní package java.lang.reflect, který nabízí v zásadě tytéž služby, ale s daleko méně šikovným API. Program je ve třech zdrojových souborech jen proto, aby třídy Ttt a Sss mohly být public:

    279 ~/Library/Java\> cat Ttt.java Sss.java Test.java
    public class Ttt {
        public void metoda() {
            System.out.println("metoda ttt");
        }
    }
    public class Sss {
        public void jenAbyByloVidetZeNaPoradiMetodToNezalezi() {}
        public void metoda() {
            System.out.println("metoda sss");
        }
    }
    import com.apple.yellow.foundation.*;
    class Test {
        static void check(Object o) throws java.lang.NoSuchMethodException, java.lang.reflect.InvocationTargetException, java.lang.IllegalAccessException {
            // Java to neumí přímo, musíme využít NSSelektor:
            // - jméno metody je zřejmé,
            // - prázdné pole udává, že metoda nemá žádné argumenty,
            // - o je objekt, jehož metodu chceme volat (jeho třídu neznáme!),
            // - null na konci jsou "žádné argumenty" pro volání metody
            NSSelector.invoke("metoda",new Class[]{},o,null);
        }
        public static void main(String[] args) throws java.lang.NoSuchMethodException, java.lang.reflect.InvocationTargetException, java.lang.IllegalAccessException {
            Ttt t=new Ttt();
            Sss s=new Sss();
            check(t);
            check(s);
        }
    }
    280 ~/Library/Java\> javac Ttt.java ; javac Sss.java ; javac Test.java
    281 ~/Library/Java\> java Test                                        
    metoda ttt
    metoda sss
    282 ~/Library/Java\>

Zásadní rozdíl spočívá v tom, že C++ svůj "objekt" používá zvenku: překladač vychází ze znalosti vnitřní struktury objektu, a podle toho konstruuje volání metody. Pokud skutečná vnitřní struktura objektu představám překladače neodpovídá – a to se ve skutečném, rozsáhlém, distribuovaném a často upgradovaném systému stává s železnou pravidelností –  může se stát cokoli: zavolá se kód na nesmyslné adrese, nebo se dokonce volají data jako kód, případně se zavolá úplně jiná metoda... Java naopak využívá skutečné vnitřní struktury existujícího objektu; proto v ní k podobným problémům z principu nikdy a nijak nemůže dojít – ať již jsou objekty získány jakkoli, s přetypováním či bez něj, z knihoven nebo třeba z jiného procesu. Mimochodem, to je také jeden z důvodů, proč je v Javě každá třída vždy dědicem základní třídy Object.

Funkce tam být nemohou...

...protože prostě tvůrci Javy nechtěli poměrně přehledný jazyk komplikovat další syntaktickou kategorií, nadto kategorií prakticky nepotřebnou. V žádném případě tedy nejde o "snahu po objektovosti na úkor zdravého rozumu", ale právě naopak: zdravý rozum tvůrcům Javy správně napověděl, že jazyk, v němž jsou k dispozici statické metody tříd, už žádné funkce nepotřebuje: darmo by komplikovaly jeho syntaxi i sémantiku, a nepřinesly by naprosto nic nového.

Co se týká nutnosti jména "funkcí" kvalifikovat jménem třídy... nu, v 99.99% případů je to jen dobře, a naopak v klasických jazycích tato možnost vždy nepříjemně chyběla. Matematicky náročné algoritmy ostatně nikdo – alespoň nikdo, kdo má onen zmíněný zdravý rozum – nebude psát v Javě, ale použije native rutinu v plain C nebo ve Fortranu, a z Javy ji jen zavolá. A konečně, dejme tomu, že jednou za dlouhý čas je opravdu zapotřebí psát v Javě "sin(PI)" – pak stačí použít universální preprocesor z jazyka C, a vše funguje jak má:

    115 ~/Library/Java\> cat Test.java
    #define sin Math.sin
    #define cos Math.cos
    #define PI Math.PI
    class Test {
        public static void main(String[] args) {
                System.out.println("V pohode, sin pi + cos pi="+ (sin(PI)+cos(PI)));
        }
    }
    116 ~/Library/Java\> cc -E -P Test.java > /tmp/javacIsStupid.java ; javac /tmp/javacIsStupid.java -d .
    117 ~/Library/Java\> java Test
    V pohode, sin pi + cos pi=-0.9999999999999999
    118 ~/Library/Java\>

Jednoduchoučké a zcela funkční... a to je ještě příkazový řádek zbytečně komplikovaný tím, že můj překladač javac se neumí chovat slušně, a nedostane-li vstupní soubor, překládat standardní vstup – jinak by stačilo pouhé "cc -E -P Test.java | javac".

Vlk nemá hlad, a koza je v pořádku

Možnost kvalifikovaných bloků pro násilné ukončení příkazy break a continue namísto příkazu goto opět není nějaká z nouze ctnost, jak naznačuje původní článek; naopak, jde o perfektní řešení problémů, které příkaz goto přináší. Protože to nemá s objekty nic společného, jen velmi stručně: téměř vždy je možné použití goto přepsat lépe a elegantněji – s jedinou výjimkou, a tou je právě výskok z vnořených bloků, asi takto:

    for (...) {
        while (...) {
            switch (...) {
                if (...) goto Out;
            }
        }
    } Out:;

Přesně tuto situaci Java dokonale (a přehledněji a s menším risikem chyb) řeší – můžeme zde napsat ekvivalentní, ale mnohem elegantnější

    for (...) Hlavni: { // nebo Hlavni: for..., podle libosti
        while (...) {
            switch (...) {
                if (...) break Hlavni;
            }
        }
    }

Chyba příkazu goto totiž není v tom, že jde o příkaz skoku jako takový; konečně, skok je nutně skryt i v příkazech cyklu nebo v příkazu if. Jde především o nepřehledný kód, kdy se skáče sem a tam (v dobách BASICu jsme takový zdroják nazývávali "špagetovým", protože linie provádění kódu byla díky příkazům goto propletená jako špagety na talíři).

Třídy: ale tohle je opravdu jinak!

Především, v Javě jsou třídy. Už to je pro programátora v C++ novinkou – C++ totiž třídy nemělo, deklarace class v něm vytvořila pouze nový typ, který však za běhu neměl žádnou skutečnou representaci. V Javě je naproti tomu třída objekt jako každý jiný. Velmi typické využití této možnosti je objekt, který sám vytváří podle potřeby jiné objekty; přitom třídu, jejíž instance bude vytvářet, se dozví až za běhu:

    152 ~/Library/Java\> cat Test.java
    class Xxx {}
    class Test {
        Class theClass=null;
        void setClass(Class cls) {
                theClass=cls;
        }
        Object objectNeeded() throws InstantiationException, IllegalAccessException {
                if (theClass==null) return this;
                return theClass.newInstance();
        }
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
                Test me=new Test();
                System.out.println(me.objectNeeded());
                me.setClass(Xxx.class);
                System.out.println(me.objectNeeded());
                me.setClass(null);
                System.out.println(me.objectNeeded());
        }
    }
    153 ~/Library/Java\> javac Test.java
    154 ~/Library/Java\> java Test
    Test@65ed8
    Xxx@65f15
    Test@65ed8
    155 ~/Library/Java\>

Tento jednoduchý příklad samozřejmě vypadá uměle; v praxi je však u dynamických systémů možnost uchovat třídu často velmi šikovná. Např. právě nyní pracuji na systému konverzních filtrů, v němž objekt "filtr" podle potřeby a podle typu vstupních dat vytváří jiný objekt, který provede konverzi dat. Filtry i konverze jsou samozřejmě dynamicky zaveditelné z nezávisle vytvářených DLL knihoven. V jazyce typu C++ by bylo nutné pro každou, i tu nejtriviálnější konverzi vždy vytvářet novou podtřídu filtru; v Javě (resp. jiném objektovém jazyce – zmíněný projekt píši v Objective C) tento problém není: pro jednoduché případy si prostě jediný, standardní filtr zapamatuje třídu, od níž bude nové konverze odvozovat, a je hotovo...

Polymorfismus

Co se týká metod: je jistě pravda, že pan Virius je z C++ zvyklý, že metody standardně nejsou polymorfní; tuto příšernou a nesmyslnou zrůdnost jazyka C++ však naštěstí Java odstranila. Nic takového jako nevirtuální metoda jazyka C++ v Javě není, a díky bohu za to! Klíčové slovo super, o kterém se pan Virius v tomto kontextu zmiňuje, docela určitě neslouží k potlačení pozdní vazby; jeho význam je jiný, a dostaneme se k němu později.

Java ovšem umožňuje v opodstatněných případech dosáhnout stejné efektivity, již v C++ nabízejí nevirtuální metody, ale bez jejich strašlivých nevýhod: umožňuje to klíčové slovo final. Deklarujeme-li metodu jako final, říkáme překladači, že ji již nikdo v případných podtřídách nesmí reimplementovat (chcete-li, "přetížit" – podle mého názoru je tento kalk z angličtiny dost zrůdný, asi jako kdybychom chtěli rozhraní říkat "meziobličej"). Překladač proto může optimalizovat až do extrémů (např. může takovouto metodu přeložit jako inline). Přitom zůstává zcela korektně zachováno základní paradigma objektového systému, tj. zpracování požadavku stále závisí na objektu – není tedy naštěstí již možná zrůdnost jazyka C++, kdy zcela korektní přetypování na obecnější třídu mohlo vést k volání zcela odlišných metod:

    // v Javě se objekty chovají rozumně, takže...
    NejakaTrida o=new NejakaTrida();
    o.nejakaMetoda(); // tato metoda...
    JinaTrida o1=(JinaTrida)o; // ...pokud toto vůbec projde...
    o1.nejakaMetoda(); // ...je ZARUČENĚ volána podruhé!!!!

Jelikož objekt sám se nikterak nezměnil, nemůže se změnit v objektovém prostředí ani volání jeho metod – jako se to děje v C++:

    // v C++ se mohou dít divné věci, takže...
    NejakaTrida *o=new NejakaTrida;
    o.nejakaMetoda(); // tato metoda...
    JinaTrida *o1=(JinaTrida*)o; // ...i když je toto přetypování zcela korektní,
                                 // tj. NejakaTrida je dědicem JinaTrida...
    o1.nejakaMetoda(); // ...se MǓŽE LIŠIT od této metody!!!!

To je samozřejmě obecně špatně – voláme-li jednu a tu samou metodu jednoho a toho samého objektu, nemůže se nám zavolat něco jiného podle toho, víme-li zrovna náhodou o objektu více nebo méně (tj. známe-li jeho skutečnou třídu NejakaTrida, nebo jen některou z jejích nadtříd JinaTrida)!

Vícenásobná dědičnost a rozhraní

Je možné diskutovat o tom, nakolik je nebo není vícenásobná dědičnost šikovná; mé zkušenosti říkají, že vždy přináší více problémů než výhod, ovšem zkušenosti jiných programátorů mohou být odlišné.

Faktem ale je, že nejčastější a určitě nejrozumnější využití vícenásobné dědičnosti vždy bylo právě definování rozhraní pomocí plně abstraktních tříd. V C++ to však vedlo k problémům s identifikací objektů, a k nepříjemným a obtížně odstranitelným chybám, jež pak z toho vyplývaly – v následujícím příkladu se zavolá úplně jiná metoda, než jsme chtěli!!!

    180 ~/Library/Java\> cat smazat.cpp
    #include <stdio.h>
    class Interface { public:
        virtual void metoda()=0;
    };
    class Object { public: // C++ nemá standardní třídu Object, musíme si ji proto dodělat sami
        virtual void xxx() {
                printf("Nejaka standardni metoda vsech objektu...\n");
        };
    };
    class Xxx: public Object, public Interface { public:
        virtual void metoda() {
                printf("metoda\n");
        };
    };

    void use(Interface *o)
    {
        ((Object*)o)->xxx();
        o->metoda();
    }
    void main()
    {
        Xxx *x=new Xxx;
        x->xxx();
        x->metoda();
        printf("jedem...\n");
        use(x);
        printf("hotovo\n");
    181 ~/Library/Java\> cc smazat.cpp
    182 ~/Library/Java\> ./a.out
    Nejaka standardni metoda vsech objektu...
    metoda
    jedem...
    metoda
    metoda
    hotovo
    183 ~/Library/Java\>

V Javě právě díky tomu, že pro definici rozhraní využívá jinou syntaxi (a pro jeho implementaci samozřejmě i jinou sémantiku) než pro normální "třídní" dědičnost tento problém nemůže nastat:

    188 ~/Library/Java\> cat Test.java
    interface Interface {
        void metoda();
    }
    class Xxx implements Interface {
        public void metoda() {
                System.out.println("metoda");
        }
    }
    class Test {
        static void use(Interface o) {
                System.out.println("Standardni metoda 'toString':"+ o.toString());
                o.metoda();
        }
        public static void main(String[] args) {
                Xxx x=new Xxx();
                System.out.println("Standardni metoda 'toString':"+ x.toString());
                x.metoda();
                System.out.println("jedem...");
                use(x);
                System.out.println("hotovo");
        }
    }
    189 ~/Library/Java\> javac Test.java
    190 ~/Library/Java\> java Test
    Standardni metoda 'toString':Xxx@65ed6
    metoda
    jedem...
    Standardni metoda 'toString':Xxx@65ed6
    metoda
    hotovo
    191 ~/Library/Java\>

Ono je používání odlišné syntaktické kategorie i čistší, protože se jedná o odlišné věci: dědičnost tříd postihuje strukturální podobnost objektů, zatímco rozhraní postihují jejich podobnost funkční; zatímco jeden směr implikace plati (jsou-li si dva objekty podobné strukturálně, budou mít alespoň nějakou podobu i funkčně), opačný nikoli: z funkční podobnosti ještě nemusí plynout podobnost struktury. To by však bylo téma pro rozsáhlou diskusi o základních principech objektového programování; pokud si vzpomínám, před lety jsme ji s panem Viriusem i vedli na stránkách Softwarových novin, avšak nevyřešila nic... doufejme, že tato polemika bude plodnější.

Na co je dobré super?

Výraz super naprosto neslouží k potlačení polymorfismu, ale umožňuje programově volat implementaci z nadtřídy. Představme si hypotetickou třídu Complex, která bude implementovat komplexní čísla, a sama bude dědicem třídy Real tak, že zděděna bude reálná složka, zatímco imaginární bude v nové proměnné im; implementace metody setZero, jež číslo vynuluje, by pak mohla vypadat např. takto:

    public void setZero() {
      im=0.0; // vynulování imaginární složky
      super.setZero(); // metoda předka--vynulování reálné složky
    }

Nejde tedy o potlačení polymorfismu, protože neustále platí, že metoda setZero je zcela polymorfní: volá se metoda právě toho objektu, se kterým pracujeme, bez ohledu na případné přetypování nebo jiné vnější triky. Klíčové slovo super pouze umožňuje v rámci volání metody definované v určité třídě využít implemetaci z předka jako součást nové implementace.

V C++ bylo v zásadě možné totéž, museli jsme však nadtřídu explicitně kvalifikovat jejím jménem (v našem příkladu bychom museli napsat Real::setZero()), což bylo nepohodlné, a při změnách hierarchie tříd to vedlo k nepříjemným a obtížně odhalitelným chybám.

Metody tříd jsou omezeny jako v C++

Přeci jen pár hloupostí Java ale z C++ naneštěstí přebrala; patří mezi ně např. to, že v metodách tříd (tj. metodách, deklarovaných jako static) není k dispozici ani výraz this (který by zde samozřejmě měl representovat třídu, jejíž metodu právě provádíme), ani výraz super (jenž by měl volat metodu nadtřídy).

Podívejme se nejprve na ukázku jak to má správně vypadat; protože Java to neumí, bude ukázka v Objective C, s detailními komentáři pro lepší porozumění:

    54 /tmp> cat test.m
    @interface Xxx:NSObject // zhruba odpovídá "class Xxx extends Object"
    +(Class)metoda; // zhruba odpovídá "static Class metoda()"
    @end
    @interface Yyy:Xxx // reimplementace metody v podtřídě, i to Java umí
    +(Class)metoda;
    @end

    // implementace metod, v Javě je spolu s rozhraním v jediném bloku
    @implementation Xxx
    +(Class)metoda {
       printf("implementace Xxx\n");
       return self; // odpovídá "return this"
    }
    @end
    @implementation Yyy
    +(Class)metoda {
       printf("implementace Yyy\n");
       // (zde by mohly být specifické akce pro třídu Yyy)
       return [super metoda]; // odpovídá "super.metoda()"
    }
    @end

    void main() {
      Class class;
      printf("pro Xxx:\n");
      class=[Xxx metoda]; // odpovídá "Xxx.metoda()" v Javě, "Xxx::metoda()" v C++
      // standardní služby description a cString zjistí jméno třídy
      printf("=%s\n",[[class description] cString]);
      printf("pro Yyy:\n");
      class=[Yyy metoda];
      printf("=%s\n",[[class description] cString]);
    }
    55 /tmp\> cc -Wall -framework Foundation test.m ; ./a.out
    pro Xxx:
    implementace Xxx
    =Xxx
    pro Yyy:
    implementace Yyy
    implementace Xxx
    =Yyy
    56 /tmp\>

Uvědomme si, co se děje: používáme zde metody tříd přesně stejně, jako (virtuální) metody objektů: obě třídy mají nějak implementovánu metodu metoda; třída Xxx prostě vrátí "this" (které se v Objective C jmenuje self), zatímco třída Yyy využije implementaci ze své nadtřídy.

Stojí za to zdůraznit, že vše je naprosto logické:

  • je-li možné metody tříd dědit (a to možné je), měl by být k dispozici i aparát pro využití metody nadtřídy v rámci implementace – standardní super;
  • jestliže statická metoda patří nějaké třídě (jako že vždy patří), mělo by v její implementaci být možné zjistit, která třída to je. Použít rovnou jméno třídy nelze právě kvůli dědičnosti: pokud bychom napsali namísto return self ("return this" v Javě) třeba return [Xxx class] ("return Xxx.class" v Javě), vrátilo by volání [Yyy metoda] špatnou třídu (Xxx namísto správné Yyy).

Bohužel, do Javy tento program přepsat nelze, ačkoli všechna potřebná primitiva v ní jsou k dispozici, sémantika by byla zřejmá a bezproblémová, a syntaxe se nabízí:

    // toto bohužel NENÍ korektní program v Javě!!!
    class Xxx {
        static Class metoda {
            System.out.println("implementace Xxx");
            return this; // nelze
        }
    }
    class Yyy extends Xxx {
        static Class metoda {
            System.out.println("implementace Yyy");
            ...
            return super.metoda(); // nelze
        }
    }
    ... // main je zbytečné uvádět

Stejně je nesmyslné, že rozhraní nemohou obsahovat metody tříd. Je to škoda, protože na rozdíl od C++ kompletní podpoře metod tříd nebrání nic než zcela umělé syntaktické omezení. Můžeme jen doufat, že vyšší verse Javy tuto možnost, samozřejmou v objektových jazycích, přinesou.

Reference a garbage collector

Je vhodné si uvědomit, že to, že objektové typy jsou ve skutečnosti reference, spolu s garbage collectorem především umožňuje cosi, co bylo v C++ krajně problematické: sdílení objektů. Mají-li dva různé moduly sdíleně pracovat s jedním společným objektem, je zřejmé, že alespoň jeden z nich musí používat referenci; v klasickém prostředí typu C++ navíc vznikají téměř neřešitelné problémy s tím, kdo a kdy má objekt uvolnit. Má to snad být ten modul, který jej vytvořil? V dynamickém systému je ale velmi dobře možné, že ten, kdo objekt vytvořil, sám zanikne dávno předtím, než ostatní moduly, jež s objektem pracují, skončí...

Abych ušetřil prostor v tomto článku, odkážu čtenáře na minulý díl seriálu o programování v Cocoa, který tuto problematiku podrobně rozebírá a vysvětluje i její řešení (na příkladu poloautomatického garbage collectoru, který nabízí Cocoa pro Objective C; plně automatický garbage collector v Javě je samozřejmě ještě daleko pohodlnější).

"Přetížení" operátorů není nebezpečné, nekázeň ale ano!

Především, obecně určitě platí, že je-li nějaká služba k dispozici, je to lepší, než naopak: mohu si přece vybrat, jestli ji použiji nebo ne... Jenže, toto obecně zcela rozumné a nevyvratitelné stanovisko naráží na praxi programátorů: přiznejme si to na rovinu, většinou jsme my programátoři pěkná prasata.

Příklad srovnávající "a*b+c" s "Plus(Krat(a,b),c)" je proto hezký, ale ve skutečném životě poněkud k ničemu: jaké objekty – kromě čísel, a ta jsou v Javě representována neobjektovými typy – má smysl násobit a sčítat? Jistě, pár příkladů se najde – matice, posloupnosti, s přimhouřenýma očima množiny. Algebry. Jak často jste ale právě tyto objekty implementovali?

Musel jsem upravovat a portovat řadu cizích programů v C++; s maticemi nebo posloupnostmi čísel jsem se dosud nikdy nesetkal, zato jsem v praxi zažil, mimo jiné:

- operátor '*', vyhledávající substringy (takže, pokud s1 a s2 byly stringy, s1*s2 byl index začátku s2 v s1);
- operátor '+', nastavující titulek okna (takže pro okno w a string s příkaz w+s nastavil nový titulek);
- operátor '&', použitý pro zaokrouhlování (nadto s dost podivnou sémantikou, takže např. 3.14159&0.4 bylo 3.1416, a 12345&2 bylo 12300).

Jistěže to jsou ukázky nekorektního programování, ale zřejmě mám nějakou divnou smůlu, že jsem se až dosud setkával téměř výhradně s podobnými případy. Přesto vcelku souhlasím s tím, že by overloading v Javě byl příjemný – sám skřípu zuby, kdykoli v Objective C musím psát něco jako [string1 stringByAppendingString:string2] – avšak naprosto chápu, proč se jej tvůrci Javy rozhodli nepodporovat.

Funkční parametry

Zde je asi trochu zapotřebí uvést na pravou míru tvrzení, že "legrace nastane ve chvíli, kdy chceme předat jako parametr funkci". Především, jak dobře víme, v Javě žádné funkce nejsou – máme zde jen metody objektů (a tříd, jež jsou samy také objekty). Díky tomu ovšem také nikde žádné funkce předávat nepotřebujeme, a příklad, který pan Virius uvádí, je proto trochu umělý – zřejmě jej navrhoval nějaký programátor v C++, který s objektovým programováním neměl moc zkušeností (nebyl to pan Virius, byl to nějaký nešťastník přímo ve firmě Sun).

Vyřešit situace, jež v C++ vyžadují callbacky nebo předávání funkcí, je totiž v objektovém systému nesmírně jednoduché: stačí předat objekt, který má být upozorněn když ta či ona událost nastane; nic jiného není zapotřebí. Ukažme si nejprve nejjednodušší variantu s využitím specifického rozhraní; v praxi se to dělá lépe a pohodlněji s využitím selektorů, avšak tento příklad je na první pohled srozumitelnější, a proto jím začneme:

    interface ButtonObserver {
        void buttonWasPressed(Button whichOne);
    }
    ...
    class Button {
        ButtonObserver currentObserver=null; // stejně dobře by to mohlo být i pole!
        ...
        void setObserver(ButtonObserver o) {
            currentObserver=o;
        }
        ...
        void getEvent(...) { // stisknutí tlačítka myši apod....
            ...
            if (...) { // button byl skutečně aktivován
                ...
                if (currentObserver!=null) currentObserver.buttonWasPressed(this);
                ...
            }
        }
        ...
    }

Kterákoli instance třídy, jež implementuje rozhraní ButtonObserver, pak může být bez nejmenších problémů použita jako "callback objekt" – tj. ten, kdo je automaticky informován o události, a může na ni nějak zareagovat. Odpovídající implementace by mohla vypadat například takto:

    class MyController implements ButtonObserver {
        ...
        public void buttonWasPressed(Button button) {
            // dejme tomu, že button má metodu "title", jež vrátí jeho popis
            System.out.println ("Uživatel stiskl tlačítko "+button.title());
        }
        ...
    }

To je vše. Naprosto žádný automaticky generovaný kód není zapotřebí – dokonce ani pro vytvoření instance třídy MyController a vyvolání metody setObserver odpovídajícího tlačítka s touto instancí jako argumentem. O to se totiž v rozumném prostředí (jakým je např. Cocoa, ale patrně nikoli JBuilder, a zřejmě ani standardní rozhraní Swing) postarají standardní knihovní funkce při načítání objektové sítě uživatelského rozhraní, vytvořené prostředky visuálního programování (v systému Cocoa je to universální InterfaceBuilder, schopný připravit objektovou síť pro libovolný objektový programovací jazyk).

(Ostatně, co se JBuilderu týká: se zájmem jsem si přečtl i druhý článek, věnovaný tomuto prostředí. Rozsah tohoto článku mi bohužel neumožňuje reagovat; v příštím čísle Chipu se však seriál "Programování v Cocoa" dostane k popisu jeho standardního vývojového prostředí ProjectBuilder; rád bych proto tento článek všem čtenářům doporučil. ProjectBuilder nejenže umožnuje programovat v Javě s větším luxusem, než jaký je k dispozici v JBuilderu; navíc ale podporuje i další programovací jazyky, takže každou část projektu můžeme snadno psát v jazyce, který se pro ni ideálně hodí.)

Nepříjemným omezením implementace podle minulého příkladu by byla (a) nutnost mít pro každou akci, která v UI může nastat, samostatné rozhraní, (b) nemožnost navázat více akcí na jednu společnou metodu, (c) navázat akci na již existující metodu již hotové třídy, aniž bychom kvůli tomu museli psát další kód. (Je zřejmé proč? Ne-li, pročtěte si implementaci znovu!)

Skutečně používané řešení proto využívá selektory; implementace třídy Button by pak v praxi mohla vypadat nějak takto:

    class Button {
        Object target=null; // stejně dobře by to mohlo být i pole!
        NSSelector action=null; // dtto
        ...
        void setTarget(Object o) { // libovolný objekt!!!
            target=o;
        }
        void setAction(String methodName) { // libovolná metoda!!!
            action=new NSSelector(methodName,Class[]{Object});
        }
        ...
        void performClick(Object sender) { // opravdu se "kliklo"
                if (target!=null && action!=null) try {
                    action.invoke(target,this);
                } catch... // ignore if target can't perform action
        }
        void getEvent(...) { // stisknutí tlačítka myši apod....
            ...
            if (...) { // button byl skutečně aktivován
                ...
                performClick(this);
                ...
            }
        }
        ...
    }

Zde patrně bude potřebné podrobnější vysvětlení: proměnná target a odpovídající metoda setTarget pro její nastavení jsou snad zřejmé, můžeme takto prostě určit libovolný objekt, který má být informován když došlo ke stisknutí tlačítka.

Zajímavější je proměnná action a odpovídající metoda setAction. Zde je opět využit package apple.com.foundation, který nabízí velmi pohodlnou třídu NSSelector; pokud bychom tento package neměli k dispozici, museli bychom použít standardní package java.lang.reflect, který nabízí v zásadě tytéž služby, ale s daleko méně šikovným API.

V každém případě, proměnná action obsahuje signaturu metody; přesný ekvivalent v C++ neexistuje, nejblíž je patrně ukazatel na metodu. V Objective C nebo SmallTalku je přesným ekvivalentem selektor. Zde se znovu dostáváme k tomu, že v Javě – stejně jako v jiných objektových jazycích – každý objekt ví co je zač a sám interpretuje metody (namísto toho, aby to dělal překladač "zvenku" jako v C++). Díky tomu je snadno možné (a) do proměnné action uložit informaci "Jde o metodu se jménem XYZ a jedním argumentem typu Object" – což je přesně to, co v implementaci setAction děláme; a (b) vyvolat takto specifikovanou metodu libovolného objektu s tím, že pokud objekt žádnou takovou metodu nemá, dojde k výjimce. To je zase jádrem implementace getEvent (služba action.invoke).

Je tedy zřejmé, že takto připravená třída Button dokáže informovat libovolný objekt o tom, že tlačítko representované její instancí bylo stisknuto, a že mu tuto informaci dokáže předat prostřednictvím libovolné metody (jež má jediný argument typu Object; z příkladu však snad je vidět, jak by bylo možné obejít i toto omezení, kdyby to bylo zapotřebí).

Praktické použití pak je triviální: stačí korektně nastavit "target" a "action" – což za normálních okolností není třeba dělat programově, ale postará se o to systém visuálního programování, v rozumném systému aniž by bylo třeba generovat jakýkoli kód (pak je totiž takový systém nezávislý na programovacím jazyce!). My si zde ukážeme několik příkladů s programovým nastavením, jež lze snadno ukázat v textu:

    class MyController {
        ...
        public void performSomeAction(Object sender) {
            System.out.println ("Uživatel vyvolal akci, použil k tomu "+sender.getName());
            try {
                Button button=(Button)sender;
                System.out.println ("...což je tlačítko "+button.title());
            } catch... // ignorovat nekorektní přetypování
        }
        ...
    }
    // následující kód není zapotřebí používáme-li visuální programování
    MyController ctrl=new MyController();
    ...
    Button someButton=...; // vazba na uživatelské rozhraní
    MenuItem someMenuItem=...; // ditto
    Window someWindow=...; // ditto
    someButton.setTarget(ctrl);
    someButton.setAction("performSomeAction");
    someMenuItem.setTarget(ctrl);
    someMenuItem.setAction("performSomeAction");
    someWindow.setDelegate(ctrl);
    someWindow.setActionOnClose("performSomeAction");
    ...

Vidíme, že jedna konkrétní akce performSomeAction bude vyvolána kdykoli uživatel stiskne dané tlačítko, nebo kděkoli zvolí danou položku z menu, nebo kdykoli zavře jisté okno. Jak by vypadala možná implementace tříd MenuItem a Window a odpovídajících metod je snad již zřejmé.

Za samostatnou ukázku stojí ještě to, že stejně dobře můžeme jako "target" využít instanci některé knihovní třídy; není zde žádné omezení na třídy, jež sami píšeme. Dejme tomu, že funkce tlačítka z minulého příkladu se může dynamicky měnit. Jistě, bylo by možné napsat si na to extra metodu, ve které by se vždy nová akce přiřadila všem třem objektům (tlačítku, položce menu a oknu)... tak by se to asi dělalo v C++. Jde to ale mnohem pohodlněji: stačí přeci nastavit

    ...
    someMenuItem.setTarget(someButton);
    someMenuItem.setAction("performClick");
    someWindow.setDelegate(someButton);
    someWindow.setActionOnClose("performClick");
    ...

a vše funguje automaticky: tlačítku můžeme přiřadit kdykoli jakkoli libovolnou akci, a položka menu a zavření okna ji vždy korektně vyvolají – protože jejich skutečnou akcí je vlastně "říci tlačítku, že má udělat přesně to samé, jako by bylo stisknuto".

Tento systém navíc umožňuje řadu snadných, logických, a přitom nesmírně luxusních rozšíření. Uveďme pouze jeden příklad: v metodě performClick je podmínka "target!=null && action!=null" vlastně zbytečně redundantní: pro chování "žádná akce" stačí nastavit na null jen "action".

Toho lze využít – a např. Cocoa toho také využívá – tak, že pro danou metodu ("action"), ale neexistující "target" se metoda automaticky posílá tomu objektu uživatelského rozhraní, který je v rámci aplikace právě aktivní (s trochou zjednodušení můžeme říci, že je to ten, ve kterém momentálně je kursor). Jestliže pak – ať již programově jako zde, nebo pomocí prostředků visuálního programování – nastavíme např.

    ...
    Button someButton=...;
    someButton.setTarget(null);
    someButton.setAction("copy");
    ...

zajistili jsme, že tlačítko vyvolá kopírování do schránky v kterémkoli objektu uživatelského rozhraní (v textovém okně, nebo v textovém políčku dialogu, nebo prostě kdekoli), ve kterém je právě kursor! Jistě, aby to mohlo fungovat, vyžaduje to polymorfismus knihovních objektů (tj. aby kterýkoli objekt UI, který může uložit svůj označený obsah do schránky, to udělal když dostane zprávu copy) – to je však v rozumně navržených knihovnách samozřejmostí.

Opravdu úplně "jiný kafe"

V závěru se opět s panem Viriusem dokonale shodnu: ano, Javu je opravdu třeba se naučit, a zvlášť programátorům, kteří dosud neznali nic než C++, to dá dost práce. Ne kvůli nevýznamným syntaktickým a nepříliš zásadním sémantickým rozdílům, ale pro zásadní rozdíl ve filosofii: Java je objektový jazyk, zatímco C++ ne.

Copyright © O.Čada <ocs@ocs.cz>, Chip 2000