Dizajnové čriepky: anonymné vnútorné triedy 2

Kto si spomenie na predošlé dielo spred dvoch dní o a) anonymných b) vnútorných c) triedach, ktoré nepochybne dočítal až do konca, mohol na konci vidieť niekoľko sľubov. Jedným bolo pojednanie o vzťahu ÁVéTé a lokálnych premenných.

Vzťah AVT a lokálnych premenných

Dajme si rovno príklad: máme tlačidlo JButton a multiriadkový textový chlievik JTextArea a chceme, aby sa po každom kliknutí na tlačidlo zjavil v MTCh nový riadok s oznamom Klik!.

Aha ho, konštruktora:

public TestFrame() {
    JTextArea textArea = new JTextArea();
    add(textArea, BorderLayout.CENTER);

    JButton button = new JButton("Klik!");
    button.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            textArea.setText(textArea.getText() + "Klik!\n");
        }
    });
    add(button, BorderLayout.SOUTH);
    pack();
}

Toto však fungovať nebude: V Eclipse to popodčiarkuje premennú textArea vo vnútri metódy actionPerformed() a ohlási:

Cannot refer to a non-final variable textArea inside an inner class defined in a different method

underline

Metóda AVT actionPerformed() chce pristupovať k lokálnej premennej v metóde obaľujúcej triedy. Ešte inak: trieda TestFrame má konštruktor, v ňom vytvoríme anonymnú vnútornú triedu s vlastnou metódou a chceme, aby vnútorná metóda videla lokálne premenné vonkajšej metódy.

Toto je možné, ale za jedinej podmienky: všetky lokálne premenné zvonku, ktoré majú byť vidieť vo vnútri musia byť označené ako final. Lebo čítanie z Java Language specification, stať 8.3.1:

Any local variable, formal parameter, or exception parameter used but not declared in an inner class must be declared final.

Čiže:

Každá lokálna premenná, formálny parameter či parameter výnimky, ktorý je používaný vo vnútornej triede a nie je v nej deklarovaný, musí byť deklarovaný ako final.

V onom príklade:

  • textArea je lokálna premenná,
  • je používaná vo vnútornej triede (v metóde actionPerformed()),
  • a samozrejme, nie je v nej deklarovaná.

…čiže: musí byť označená ako final.

final-countdown

Technické okienko (pre chrabré programátorky a -orov)

Toto všetko je kvôli technickým obmedzeniam: môže sa totiž stať, že vonkajšia metóda dobehne skôr než vnútorná. (Ako to? Jednoducho: stačí si poznačiť objekt v premennej button niekam bokom a neskôr s ním pracovať.) V tej chvíli sa obsah lokálnej premennej stratí…a máme problém.

Ak vnútorná trieda pristupuje k finalnej premennej, v skutočnosti sa na pozadí udejú špinavé triky. Asi takéto:

  • vo vnútornej triede sa vytvorí skrytá inštančná premenná textArea typu JTextArea
  • magickým spôsobom sa do nej nakopíruje hodnota lokálnej premennej textArea (je prakticky jedno akým: napr. cez skrytý a neprístupný konštruktor s parametrom typu JTextArea; skrytá inštančná premenná môže byť public a pod.

Ak sa vonkajšia metóda ukončí, nič sa nestane: AVT si drží svoju vlastnú kópiu lokálnej premennej.

Ako vieme (teda dúfam), finálna premenná je určená len na čítanie: a teraz je jasné prečo: kopírovanie z lokálnej premennej do inštančnej sa udeje len raz. Ak by bolo možné lokálnu premennú meniť podľa ľubovôle, bolo by nutné synchronizovať zmeny s AVT… čo je jednoducho príliš komplikované a preto to autori Javy zakázali.

Koniec technického okienka.

Problémy s finálnymi premennými

Ako vieme (teda dúfam), finálna premenná je určená len na čítanie, ehm. To je často zdrojom všakovakých obmedzení, ale existujú dve možnosti, ako to obísť.

Z lokálnej premennej zrobime inštančnú

Pamätáte na špecifikáciu? Zase sa budem opakovať:

Any local variable, formal parameter, or exception parameter used but not declared in an inner class must be declared final.

Ak chce vnútorná trieda pristupovať k inštančným premenným vonkajšej premennej, môže to robiť bez problémov. Lebo a) len lokálne b) len parametre c) len výnimky musia byť finálne.

Aha:

public class TestFrame {
    private JTextArea textArea;     

    public TestFrame() {
        this.textArea = new JTextArea();
        add(textArea, BorderLayout.CENTER);

        JButton button = new JButton("Klik!");
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                textArea.setText(textArea.getText() + "Klik!\n");
            }
        });
        add(button, BorderLayout.SOUTH);
        pack();
    }
}

Toto funguje bez problémov.

Jeden local variable wrap poprosím

Niekedy si ale nechceme/nemôžeme dovoliť inštančnú premennú. To však možno obísť ďalším syntaktickým hackom, ale ten je natoľko importantný, že si zaslúži samostatný nadpis <h2>.

Jeden local variable wrap poprosím

V príklade sa vráťme k súborom a prechádzaniu adresárov. Dajme si úlohu:

Spočítajte všetky MP3ky v adresári D:/MP3 a jeho podadresároch.

Linuxáci by si dali samozrejme:

find . | grep \.mp3 | wc-l

Javáci si môžu urobiť všeobecnú úlohu, kde budú rekurzívne preliezať zoznam podadresárov a podsúborov daného adresára a každý nájdený súbor/adresár pošlú do listenera, podobne ako to robí metóda list().

Najprv si vyrobme interfejs s jedinou metódou, ktorá dostane na vstup aktuálny súbor:

public interface FileVisitor {
    public void visit(File file);
}

Metóda pre nájdenie všetkých podsúborov/podadresárov potom bude:

public class FileTraverser {
    public void traverse(File file, FileVisitor visitor) {
        visitor.visit(file);
        if(file.isDirectory()) {
            for (File child : file.listFiles()) {
                traverse(child, visitor);
            }
        }
    }
}

Kód je (samozrejme) samovysvetľujúci: abstraktný súbor z parametra navštívime, teda zavoláme metódu na interfejsi FileVisitor. Ak je abstraktný súbor náhodou adresárom, prejdeme jeho deti a na každom zavoláme rekurzívne metódu traverse().

Keď sa na to pozrieme z pohľadu návrhových vzorov, nejde o nič iné než o starý známy vzor Visitor. V iných jazykoch by sa dal implementovať oveľa jednoduchšie.

A teraz zábava: popočítajme už MP3ky:

public static void main(String[] args) {
    int count = 0;
    FileTraverser traverser = new FileTraverser();
    traverser.traverse(new File("D:/MP3"), new FileVisitor() {
        public void visit(File file) {
            if(file.getName().endsWith(".mp3")) {
                count++;
            }
        }
    });
}

Of course, že to nebude fungovať. Premenná count musí byť samozrejme final, teda konštanta, teda sa nedá meniť. Ale ak ju obalíme do objektu… bude to fungovať. final totiž znamená len to, že samotná referencia sa nesmie meniť, ale keď budeme mať kontajner na číslo, jeho vnútro sa meniť bez problémov dá.

Kontajner na číslo, variant Á..tomik intéger

Na kontajner môžeme heknúť triedy z konkurentného balíčka java.util.concurrent.atomic. Máme tam triedu AtomicInteger, ktorá môžeme inkrementovať a získavať jej hodnotu.

final AtomicInteger count = new AtomicInteger();
File folder = new File("D:/mp3"); 
FileTraverser traverser = new FileTraverser();
traverser.traverse(folder, new FileVisitor() {
    public void visit(File file) {
        if (file.getName().endsWith(".mp3")) {
            count.incrementAndGet();
        }
    }
});
System.out.println(count);

Takisto môžeme použiť AtomicLong alebo AtomicBoolean.

Je to možno kanón na vrabce, ale rozhodne je to najčitateľnejšia možnosť.

Kontajner na číslo, variant B: poľo

Za kontajner poslúži jednoprvkové poľo. Opäť platí rovnaká finta: pole síce bude nemenné, ale jeho obsah možno radostne meniť.

final int[] countWrapper = new int[1];
File folder = new File("D:/mp3"); 
FileTraverser traverser = new FileTraverser();
traverser.traverse(folder, new FileVisitor() {
    public void visit(File file) {
        if (file.getName().endsWith(".mp3")) {
            countWrapper[0]++;
        }
    }
});
System.out.println(countWrapper[0]);

Poľo je efektívne (zaberá málo pamäte, niežeby to bolo v Jave problém), akurát si treba zvyknúť na hranatozátvorčie.

Alternatívne je možné použiť aj zoznam, ale úprimne: je to strašná syntaktická babračka (hlavne kvôli objektom a ich možným null hodnotám) a je to nielen nicht nur menej prehľadné, sonder auch menej efektívne.

Kontajner na číslo, variant C: class

Tvrdé jadro si môže vytvoriť pomocnú (vnútornú; lež nie anonymnú!) triedu s jedinou verejnou inštančnou premennou, teda vnútrom kontajnera. Vzápäť vytvoríme jej inštanciu, ktorá prirodzene musí byť final, ale od toho momentu ju používame ako keby sa nič nedialo.

public static void main(String[] args) {
    class Holder {
        int item;
    }
    final Holder holder = new Holder();

    File folder = new File("D:/mp3"); 
    FileTraverser traverser = new FileTraverser();
    traverser.traverse(folder, new FileVisitor() {
        public void visit(File file) {
            if (file.getName().endsWith(".mp3")) {
                holder.item++;
            }
        }
    });
    System.out.println(holder.item);
}

Dokonca si trúfame obetovať gettery a settery: je to už aj tak natoľko syntakticky ohnuté, že by takéto veci tomu nepomohli.

Dlhý zápis

Toto všetko by sme nemuseli robiť, keby sme nepoužívali anonymné vnútorné triedy. Dlhý regulárny zápis by vytvoril pomenovanú nevnútornú triedu:

public class Mp3CountingFileVisitor implements FileVisitor {

    private count;

    public void visit(File file) {
        if (file.getName().endsWith(".mp3")) {
            count++;
        }
    }

    public void getCount() {
        return this.count;
    }
}

A použili by sme ju:

File folder = new File("D:/mp3"); 
Mp3CountingFileVisitor visitor = new Mp3CountingFileVisitor();

FileTraverser traverser = new FileTraverser();
traverser.traverse(folder, visitor);
System.out.println(visitor.getCount());

Trik funguje pre akékoľvek iné triedy

Môžete to šíriť: ale finta s krátkym zápisom funguje pre akúkoľvek inú triedu. Vytvoriť inštanciu a oddediť v jednom kroku funguje vždy a všade.

Chcete vytvoriť vlastný model pre zoznamy JList? Môžete radostne oddediť od abstraktnej triedy AbstractListModel a rovno vytvoriť jeho inštanciu:

ListModel<Integer> model = new AbstractListModel<Integer>() {
    public int getSize() {
        return 20;
    }

    public Integer getElementAt(int index) {
        return index + 1;
    }
};

Následne ho rovno použite v komponente typu JList, teda v skrolovateľnom zozname:

JList<Integer> list = new JList<Integer>(model);

Toto funguje len pre prekrývanie/overriding metód, takýmto spôsobom nemôžete dodávať do vlastnej triedy nové metódy. Teda môžete, ale nepomôže vám to, lebo mimo triedy ich neuvidíte. Inak povedané, napísať môžete:

Object dog = new Object() {
    public void bark() {
        System.out.println("Bark!");
    }
};
dog.bark();

Volanie dog.bark() však bude vyhlásené za chybu: premenná dog je typu Object a nemá štekavú metódu.

Iné jazyky a súvislosti

V céčku sa interfejsy s jednou metódou dajú naemulovať pointrami na funkcie (stačí vytvoriť premennú typu “pointer na funkciu, ktorá berie reťazec s názvom súboru a vracia void).

V Groovy je to úplne skvelé. Jednometódové interfejsy sa dajú nahradiť tzv. closures a situácia sa tým dramaticky zjednoduší. To platí aj pre ostatné funkcionálnoidné jazyky: samotný interfejs totiž nie je nič iné než bežná (matematická) funkcia, ktorá berie súbor File a vracia nič/boolean a kód to extrémne zjednoduší.

3 thoughts on “Dizajnové čriepky: anonymné vnútorné triedy 2

  1. Wow, wonderfu blog layout! How long have you been blogging for? you make blogging look easy. Thee overall look of your web site is fantastic, as well as the content!

Napísať odpoveď pre Mabel Zrušiť odpoveď

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *