Java (7) - Podmínky

V minulých dílech jsme se věnovali objektům. V tomto dílu si ukážeme, jak vypadají konstrukce ovlivňující větvení programu – podmínky. Představíme si operátory porovnání, jejichž výstupem jsou pravdivostní hodnoty a řekneme si, jaká úskalí plynou z jejich použití. Pravdivostní hodnoty poté využijeme při rozhodování v podmínkách, ternárních operátorech a konstrukci switch.

Od tohoto dílu dále silně doporučuji, abyste si kód nejen přečetli, ale také abyste ho přepsali a zkoušeli poupravovat. Jednou věcí je kód chápat a druhou jej umět reprodukovat. A bohužel jinak, než neustálým zkoušením a chybováním, se programovat ještě nikdo nenaučil.

Operátory porovnání

Ve čvrtém dílu jsme si ukázali některé základní operátory, nyní tento výběr doplníme o operátory porovnání, které mají jako návratou hodnotu pravdivostní typ boolean.

Operátory == a !=

Operátory rovná se (==) a nerovná se (!=) slouží k porovnání hodnot na své levé a pravé straně. Operátor rovnosti vrací true, pokud jsou tyto hodnoty totožné, anologicky operátor nerovnosti vrací v tomto případě false. U primitivních datových typů je porovnání zřejmé – závisí čistě na uložené hodnotě.

Nebezpečné porovnávání objektů

Nebezpečí těchto operátorů nastává u porovnávání ukazatelů na objekty. Pokud si vzpomenete, tak jsme si říkali, že operátor new vrací referenci, která ukazuje do paměti. Čili v proměnné objektového typu není objekt, ale nějaká adresa. Z tohoto důvodu porovnání dvou různých ukazatelů ukazujících na objekty s totožným obsahem skončí jako false, protože obě reference ukazují jinam. Pro porovnání objektů se proto používá metoda equals (dokumentace), již má každý objekt, a kterou můžeme ve svých třídách redefinovat (znovu deklarovat a implementovat).

Velmi nebezpečnou záležitostí se stává neopatrné použití operátorů == a != u řetězců, protože u nich může kompilátor nebo virtuální stroj přijít na to, že řetězec již v paměti existuje a nevytvořit jej znovu (ukázat na místo již existujícího exempláře), což je samozřejmě možné, protože řetězce jsou neměnné a totožné objekty se proto mohou libovolně zastupovat.

Autoboxing

Další a možná ještě nebezpečnější situací je porovnávání objektových wrapperů primitivních datových typů (Java obsahuje pro každý primitivní typ jeho objektový ekvivalent). S těmito typy se váže tzv. autoboxing, což je konstrukce, při které se přiřazením primitivní hodnoty do reference na odpovídající objektový wrapper vytvoří automaticky obalující objekt s patřičnou hodnotou.

Toto nebezpečí spočívá v tom, že si právě při autoboxingu Java ukládá některé často používané objekty bokem a při jejich opětovném autoboxovacím využití vrátí tyto původní instance. U typu Integer (obalující třída int) se jedná o proměnné v rozsahu -128 → 127.

        System.out.println(5 == 5); //true
        System.out.println(5 != 5); //false
        System.out.println('a' != 'b'); //true
        System.out.println('a' == 'b'); //false

        String s = "pes"; //String je objekt
        System.out.println(s == null); //false, s neukazuje na null
        
        System.out.println(null == null); //true


        /*****************************************************
         * Ukazka nebezpecnosti porovnavani objektovych typu *
         *****************************************************/
        Integer i = 5; //Integer je objektovy wrapper na int
        Integer i2 = 5;
        System.out.println(i == i2); //true, cacheovane hodnoty

        Integer i3 = 128; //tohle Java necacheuje
        Integer i4 = 128;
        System.out.println(i3 == i4); //false

        Integer i5 = new Integer(5); //puvodni priklad bez autoboxingu
        Integer i6 = new Integer(5);
        System.out.println(i5 == i6); //false, cache se vztahuje jenom k autoboxingu

        /******************************
         * Jak by to melo byt spravne *
         ******************************/
        System.out.println(i.equals(i2)); //true
        System.out.println(i3.equals(i4)); //true
        System.out.println(i5.equals(i6)); //true

Z tohoto kódu plyne jediné poučení. Operátory rovno a nerovno používejte pouze pro primitivní datové typy. Pokud u objektů vysloveně nechcete porovnávat reference, tak vždy používejte metodu equals, jinak se Váš program bude chovat maximálně nevyzpytatelně.

Operátory >, <, >=, <=

Pro porovnávání číselných typů máme k dispozici operátory porovnání větší, menší, větší nebo rovno, menší nebo rovno (>, <, >=, <=).

        System.out.println(5.5 > 5); //true
        System.out.println(5.5 < 5); //false
        System.out.println(5 <= 5); //true
        System.out.println(5.5 >= 6); //false

Operátor instanceof

Pro úplnost si ještě uvedeme nástin toho, co umí operátor instanceof, ke kterému se v jednom z následujících dílů vrátíme a vysvětlíme si, proč je jeho časté využívání ukazatelem špatného návrhu aplikace.

Operátor instanceof vrací true, pokud je daný objekt instancí uvedené třídy, případně instancí libovolného podtypu této třídy (odvozování podtypů nás v rámci tohoto tutoriálu teprve čeká).

System.out.println("" instanceof String); //true  

Logické operátory

Jednotlivé podmínky můžeme vrstvit pomocí logických operátorů konjunkce (a), disjunkce (nebo) a negace.

Negace

Nejjednodušším operátorem je operátor negace (!). Tento umisťujeme vždy před výraz, jehož logickou hodnotu chceme negovat (otočit).

        boolean b = true;
        System.out.println(b); //true
        b = !b;
        System.out.println(b); //false
        System.out.println(!(5 < 6)); //false

Konjunkce a disjunkce

Pro konjunci a disjunkci existuje dokonce po dvou operátorech. Rozdíl mezi nimi je v jejich vyhodnocování. Při použití logických operátorů konjukce a disjunkce (& a |) budou vyhodnoceny vždy obě strany operátoru. V případě podmíněných oeprátorů (&& a ||) bude vyhodnocování výrazu ukončeno v okamžik, kdy se již výsledek nemůže změnit.

To znamená, že pokud je levá strana operátoru && false, tak operátor vrátí okamžitě false (libovolná hodnota na pravé straně již výsledek nemůže zvrátit). Obdobně pokud je pravdivá levá strana operátoru ||, tak ten vrátí bez dalšího vyhodnocování pravé strany true.

    /**
     * Demonstrace vyhodnocovani vyrazu, pouzivame metody, abychom si 
     * nase zavery mohli overit, v praxi bychom samozrejme pouzivali pouze @true a @false
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        /*******************************
         * Vyhodnocovani &, &&, | a || *
         *******************************/
        System.out.println(returnFalse() & returnTrue()); //vyhodnoti se oboji
        System.out.println(returnFalse() && returnTrue()); //pouze prvni cast

        System.out.println(returnTrue() | returnFalse()); //vyhodnoti se oboji
        System.out.println(returnTrue() || returnFalse()); //pouze prvni cast
        //slozeny vyraz, vyhodnoti se vse, vypise true
        System.out.println((returnTrue() && returnFalse()) | !returnFalse());
    }

    /**
     * Vypise retezec "returnFalse" na konzoli a vrati false
     * Metoda je staticka, protoze nevytvarime zadnou instanci, volame tuto
     * metodu ze statickeho kontextu metody main (vstupni bod aplikace)
     * Pokud bychom tuto metudu chteli mit instancni, pak bychom odstranili slovo
     * static a v metode main vytvorili instanci tohoto objektu (zkuste)
     * @return vzdy @false
     */
    public static boolean returnFalse(){
        System.out.println("returnFalse");
        return false;
    }
    /**
     * Vypise retezec "returnTrue" na konzoli a vrati true
     * @return vzdy @true
     */
    public static boolean returnTrue(){
        System.out.println("returnTrue");
        return true;
    }

Při programování se setkáme v drtivé většině případů s podmíněnými příkazy (&&, ||), jelikož jednak budou lépe popisovat to, co budeme chtít udělat a druhak jsou šetrnější ke zdrojům aplikace (nemusí se v daných případech ověřovat ona druhá část výrazu).

Podmínky

Základním konstruktem je samotná podmínka – příkaz if – za kterým následuje booleovský výraz v kulatých závorkách a blok (nebo jeden příkaz), který se vykoná, je-li podmínka splněna.

        if(i % 3 == 0){ //pokud je cislo delitelne tremi
            System.out.println("Cislo je delitelne tremi!");
        }

Druhým konstruktem je else, za kterým následuje buď jednotlivý příkaz nebo blok. Větví else se vydá zpracování programu tehdy, není-li splněna podmínka u nejbližšího if.

        if(i % 3 == 0){ //pokud je cislo delitelne tremi
            System.out.println("Cislo je delitelne tremi!");
        }else{
            System.out.println("Cislo neni delitelne tremi");
        }

Pokud chceme postupně ověřit více podmínek stejné úrovně, tak tyto konstrukty složíme za sebe a vznikne složenina else if(podmínka). Všimněme si, že if je zde v roli onoho jednoho příkazu, který můžeme vepsat přímo za else.

    /**
     * Ukazkova metoda pro podminky (staticka, abychom nemuseli vytvaret instanci obalujiciho objektu)
     */
    public static void testCondition(){
        int i = (int) (Math.random()*10); //nahodne cislo 0 az 9
        if(i % 3 == 0){ //pokud je cislo delitelne tremi
            System.out.println("Cislo je delitelne tremi!");
        } else if(i % 2 == 0) {
            System.out.println("Cislo je sude");
        } else {
            System.out.println("Cislo neni delitelne tremi, ani neni sude");
        }

        //za if a else muzeme napsat jeden prikaz bez vyuziti bloku.
        //Obcas to udela kod prehlednejsim, ale obecne bych to (krome pripadu else if) nedoporucil,
        //protoze to pri nevhodnem formatovani kodu muze vest k chybam.
        if(i == 7) System.out.println("Cislo je 7");
        else if(i == 6) System.out.println("Cislo je 6");
        else System.out.println("Cislo neni ani 7 ani 6");
    }

Ternární operátor

Ternární operátor, který zapisujeme jako

podmínka ? odpověď1 : odpověď2;

slouží ke zjednodušení (zkrácení) přiřazovací konstrukce if-then-else. Jeho interpretace je následující. Pokud platí podmínka před otazníkem, pak operátor vrátí první z odpovědí (oddělené dvojtečkou). Pokud podmínka není uspokojena, pak vrátí druhou odpověď.

Technicky je sice možné skládat řadu ternárních operátorů do sebe a zajistit tak komplexnější rozhodování, ale jedná se o praktiku, jež velmi znepřehledňuje kód, a kterou je z tohoto důvodu zdraví nebezpečné používat (případný čtenář tohoto kódu by Vás mohl insultovat).

    /**
     * Metoda demonstrujici ternarni operator, ktera vraci osloveni osoby v anglictine
     * na zaklade toho, zda-li oslovujeme zenu nebo muze a jejich rodinneho stavu
     * (pan, slecna, pani)
     * @param male priznak toho, zda-li se jedna o muze (@true), nebo zenu (@false)
     * @param married male priznak rodinneho stavu - zenaty/vdana (@true), svobodny/svobodna (@false)
     * @return osloveni
     */
    public static String createGreeting(boolean male, boolean married){
        String base = "Dear ";
        if(male){ // je to muz
            return base + "Mr.";
        }else{ //je to zena
            return base + (married ? "Mrs." : "Ms.");
        }
        /*
         * takto by to take slo zapsat, ale uz je to ponekud neprehledne
         * pripade pozuti slozitejsich logickych podminek a vice slozenych
         * ternarnich operatoru by to byl konec sveta
         */
        //return base + (male ? "Mr." : married ? "Mrs." : "Ms.");
    }
 

Switch

Méně častou možností, jak řídit tok, je příkaz switch (přepínač). Switch, jak již název napovídá, umožňuje na základě hodnoty proměnné přepínat mezi několika větvemi programu. Proměnná, která složí jako přepínač může být následujících typů: char, byte, short, int. Tento seznam je platný pro Javu 1.6, ve verzi 1.7 by se měl rozšířit o řetězec.

Samotný zápis switche se od podmínek v některých aspektech liší. S podmínkami analogicky definujeme blok nejprve příkazem switch, za kterým následuje v kulatých závorkách proměnná, podle které přepínáme, a samotný blok příkazu.

V tomto bloku ovšem nalezneme vždy nejprve návěští case, za ním hodnotu proměnné, která odpovídá danému stavu přepínače a dvojtečku. Za dvojtečkou následují příkazy odpovídající tomuto stavu a příkaz break, který slouží k opuštění bloku switch. Pokud bychom jej z jakéhokoliv důvodu neuvedli nebo na něj zapomněli, tak se provede i následující case blok.

        int day = (int) (Math.random() * 7); //cisla 0-6
        switch (day) {
            case 0:
                System.out.println("Je pondeli");
                break;
            case 1:
                System.out.println("Je utery");
                break;
            case 2:
                System.out.println("Je streda");
                break;
            case 3:
                System.out.println("Je ctvrtek");
                break;
            case 4:
                System.out.println("Je patek");
                break;
            case 5:
                System.out.println("Je sobota");
                break;
            case 6:
                System.out.println("Je nedele");
                break;
        }

        //pokazeny switch
        int switcher = 0;
        switch(switcher){
            case 0:
                System.out.println("Toto bude vypsano, ale zapomeneme na break");
            case 1:
                System.out.println("A toto se vypise taky");
        }

Metody equals a hashCode

Aby nebyl dnešní díl pouze přehlídkou aha-příkladů, tak si ukážeme, jakým způsobem můžeme předefinovat (překrýt) metodu equals u třídy zvířátka z minulých dílů.

S redefinicí metody equals se bohužel váže i redefinice metody hashCode (dokumentace), která slouží při ukládání objektů do rozptýlených tabulek (hash table). Na vysvětlení této datové struktury a všech souvislostí bohužel zatím nemáme dostatek znalostí. Proto se spokojíme s metodou hashCode, kterou nám na základě naší úpravy equals vygeneruje vývojové prostředí.

/**
 * Zviratko, ktere uz ma i nejake operace
 * @author Pavel Micka
 */
public class Animal {
    /**
     * Druh zviratka
     */
    private String kind;
    /**
     * Zvuk, ktery vydava
     */
    private String sound;
    .
    .
    .
    @Override //kontrola toho, ze skutecne prekryvame (slouzi k prevenci chyb z nepozornosti)
    public boolean equals(Object obj) {
        if(obj instanceof Animal){ //je stejneho typu
            Animal animal = (Animal) obj; //muzeme tedy pretypovat
            if(this.kind.equals(animal.kind) && this.sound.equals(animal.sound)){
                return true; //je stejneho druhu a vydava stejny zvuk
            }
        }
        return false;
    }
    /*
     * Tuto metodu nam nageneruje vyvojove prostredi na zaklade nasi upravy
     * metody equals
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 79 * hash + (this.kind != null ? this.kind.hashCode() : 0);
        hash = 79 * hash + (this.sound != null ? this.sound.hashCode() : 0);
        return hash;
    }
}

Poznámka: kód není kompletní, ony tři tečky značí, že je tam reálně zbylý kód z minulých dílů, který ovšem nyní není podstatný a pouze by překážel.








Doporučujeme

Internet pro vaši firmu na míru