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

Skúšali ste niekedy spočítať počet MP3jok v adresári? Alebo, ak máte iTunes, M4Ačiek? V Jave je to, ehm, jednoduché:

File folder = new File("c:/Users/rn/Music/iTunes/iTunes Media/Music/David Carbonara/Mad Men_ After Hours (Music from the Ori");
int m4aCount = 0;
for (File file : folder.listFiles()) {
    if(file.getName().endsWith(".m4a")) {
        m4aCount++;
    }
}
System.out.println("Found .m4a files: " + m4aCount);

(Áno, áno, toto nikto nikdy neurobí: v Bashi/Powershelli sú na to jednoriadkové skripty.)

Trieda pre súbory java.io.File však ponúka aj alternatívnu možnosť: metódu list() s parametrom FileFilter. Vráti nám pole súborov File[], čo je očividné (listujeme súbory, nie?), ale čo ten parameter?

Kuk do dokumentácie a aha: je to interfejs s jedinou metódou:

public interface FileFilter {
    boolean accept(File pathname);
}

Objekt tohto typu rozhodne, či sa konkrétny súbor akceptuje, teda zahrnie do výsledného zoznamu, alebo nie. Inak povedané, filter povie pre každý súbor, či ho berie alebo zamieta. Ak chceme začať filtrovať, potrebujeme dve veci:

  • Naklepať vlastnú triedu, ktorá implementuje interfejs FileFilter,
  • Vytvoriť inštanciu tejto triedy.

Potrebujeme triediť M4Ačka? Toť trieda:

public class M4AFileFilter implements FileFilter {
    public boolean accept(File pathname) {
        return pathname.getName().endsWith(".m4a");
    }
}

A toť jej vytváranie a použitie:

FileFilter m4aFilter = new M4AFileFilter();
System.out.println(m4aFilter.accept("01 A Beautiful Mine (Jazz Interpreta.m4a"));
System.out.println(m4aFilter.accept("Digital Booklet - Mad Men, After Hou.pdf"));

Keď máme inštanciu filtra, môžeme ho narvať do metódy list():

File folder = new File("c:/Users/rn/Music/iTunes/iTunes Media/Music/David Carbonara/Mad Men_ After Hours (Music from the Ori");
FileFilter m4aFilter = new M4AFileFilter();
int m4aCount = folder.list(m4aFilter).length;

System.out.println("Found .m4a files: " + m4aCount);

Vo vnútri sa stane toto:

Cpt. File: Vitajte, vážené súbory. Je vás tu šestnásť, zoraďte sa a ideme. Hej ty, prvý! 01 A Beautiful Mine (Jazz Interpreta.m4a! Strč sa do metódy accept()!

Súbor A Beautiful Mine: Áno, pane!

Cpt. File: FileFilter, čo si vrátil?

FileFilter: true, pane!

Cpt. File:: Výborne, Beautiful Mine, marš doľava do výsledného radu! Ďalší! Kto je ďalší?! "Digital Booklet - Mad Men, After Hou.pdf", strč sa do metódyaccept()`!

Súbor A Beautiful Mine: Áno, pane!

Cpt. File: FileFilter, čo si vrátil?

FileFilter: false, pane!

Cpt. File:: Marš preč, Digital Booklet atď, marš doprava, nech ťa už nevidím! Ďalší!

anonymous3

Chceme pre zmenu počítať MP3ky? Opäť: nová trieda, nová inštancia a jej použitie:

public class MP3FileFilter implements FileFilter {
    public boolean accept(File pathname) {
        return pathname.getName().endsWith(".mp3");
    }
}

A potom:

FileFilter mp3Filter = new MP3FileFilter();
int mp3Count = folder.list(mp3Filter).length;

System.out.println("Found .mp3 files: " + mp3Count);

Takýchto filtrov môžeme navyrábať koľko chceme a používať podľa nálady, stavu programu a úloh v TODO zozname.

Anonymné vnútorné triedy

Čo bolo napísané vyššie o filtroch? Dva kroky: 1) vytvoriť triedu implementujúcu interfejs 2) vytvoriť jej inštanciu.

V Jave je možné oba kroky vybaviť naraz. Nejedno API totiž spolieha na návrhový vzor, ktorý bolo vidieť v metóde list(): teda parameter typu interfejs. V prípade jednorazových použití by sa v kóde len kopili kratučké triedy, a čo je horšie, v prípadoch, keď si metóda a interfejsová trieda potrebujú vymieňať údaje, by vznikala kopa zbytočného kódu (čo bude vidieť nižšie).

Najprv si ukážme anonymnú vnútornú triedu filtra MP3jok:

FileFilter mp3Filter = new FileFilter() {
    public boolean accept(File pathname) {          
        return pathname.getName().endsWith(".mp3");
    }       
};
int count = folder.list(mp3Filter).length;

Čo všetko sa stalo na prvých štyroch riadkoch?

Predovšetkým, vytvárame novú inštanciu FileFiltera. To je ale interfejs a vieme, že vytvárať objekty interfejsov nie je možné — lebo interfejs je len zoznam hlavičiek metód, bez akéhokoľvek kódu. Keby bolo možné vytvoriť pomocou new inštanciu interfejsu, úbohý objekt by nevedel, ako sa má správať.

Možno keby sme nejakým hackom vedeli dodať kód k jednotlivým metódam…

Ale to sa deje! Všimnime si kučeravé zátvorky:

new FileFilter() {
    ...
};

Medzi nimi je kód, ktorý plní rovnakú úlohu ako v deklarácii triedy implementujúcej interfejs! Je tam presne taká istá metóda ako vyššie v ukážke triedy MP3FileFilter!

Hra “Nájdi dva rozdiely!”:

public class MP3FileFilter implements FileFilter {
    public boolean accept(File pathname) {
        return pathname.getName().endsWith(".mp3");
    }
}

a

... = new MP3FileFilter();

vs:

... = new FileFilter() {
    public boolean accept(File pathname) {          
        return pathname.getName().endsWith(".mp3");
    }       
};

Táto konštrukcia vytvorila inštanciu anonymnej vnútornej triedy.

Veľký pozor! pozor na to, že za poslednou kučeravou zátvorkou musí byť bodkočiarka! Tá totiž hovorí, že končí riadok, na ktorom sa vytvára nová inštancia. Je to presne ako v prípade

new NIEČONIEČONIEČO();

akurát, že NIEČONIEČONIEČO je zložité vyjadrenie triedy. Dá sa to predstaviť aj takto:

anonymous2

Anonymná…

anonymous5

Naša trieda je anonymná, lebo nemá meno. Konštrukcia new FileFilter() hovorí:

  1. Vytvor triedu:

    public class ??? implements FileFilter {
        public boolean accept(File pathname) {          
            return pathname.getName().endsWith(".mp3");
        }               
    }
    
  2. A hneď na to vytvor jej inštanciu:

    FileFilter mp3Filter = new ???();
    

Oba kroky sa uskutočnia naraz. Otázniky ??? hovoria, že “neviem síce ako sa moja trieda volá, ale viem, že implementuje FileFilter a chcem podľa nej vytvoriť objekt.

…vnútorná…

Trieda je vnútorná z jednoduchého dôvodu: deklarujeme ju vo vnútri inej triedy. Keby sme mali kompletný kód…

public class FileFilters {
    public static void main(String[] args) {
        FileFilter mp3Filter = new FileFilter() {
            public boolean accept(File pathname) {          
                return pathname.getName().endsWith(".mp3");
            }       
        };
        int count = folder.list(mp3Filter).length;
        System.out.println(count);
    }
}

… videli by sme, že anonymná trieda je vo vnútri triedy FileFilters.

Anonymná nie je anonymná

Inak, i anonymná vnútorná trieda nejaké meno. Kompilátor ho priradí na základe mena vonkajšej triedy: napr. po skompilovaní tohto súboru FileFilters.java vzniknú dva .class súbory:

  • FileFilters.class
  • a FileFilters$1.class: čo znamená „prvá anonymná trieda v triede FileFilters.

anonymous4

Ešte „zjednodušená“ syntax

Syntax sa dá ohnúť ešte viac! Deklaráciu premennej môžeme vynechať a všetko rovno napchať do vnútra metódy!

File[] files = folder.list(new FileFilter() {
                                public boolean accept(File pathname) {          
                                    return pathname.getName().endsWith(".mp3");
                                }       
                           };
    );
int count = files.length;

Na hromade to vyzerá katastroficky, ale môžeme to vymaľovať:

anonymous-inner-classes-1

Toto rozhodne nie je nejaký hack: používa sa to úplne bežne, uvidíte príklady. Že z toho ide hlava dokola? Táto syntax je druhý najtemnejší kút zápisu v kóde. Permanentná vicemiss Zlokódu od roku 199x. (Prvá sú možno generiká.)

Ďalšie príklady anonymných vnútorných tried

Vytvorenie vlákien

Chcete vytvoriť nové vlákno, teda úlohu bežiacu na pozadí? Tá istá finta!

public class ThreadTest {
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            public void run() {
                for(int i = 0; i < 20000; i++) {
                    System.out.println("Budem študovať Blocha!");
                }
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}

Samozrejme, dalo by sa to aj klasickým spôsobom:

public class HomeworkRunnable implements Runnable {
    public void run() {
        for(int i = 0; i < 20000; i++) {
            System.out.println("Budem študovať Blocha!");
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Runnable runnable = new HomeworkRunnable()
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Poslucháči/listenery v swingáckych aplikáciách

Anonymná vnútorná trieda je denný chlieb v Swingu. Aha, jedno tlačidlo a jeho obsluha pomocou ActionListenera:

public class TestFrame extends JFrame {
    public TestFrame() {
        JButton button = new JButton("Klik!");
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("Klik!");
            }
        });
        add(button);
        pack();
    }
}

Interfejs ActionListener má jedinú metódu, kde je kód obsluhy kliknutia na tlačidlo. Na jeden krok vytvoríme jeho inštanciu, prekryjeme metódu actionPerformed() a dodáme demo kód.

A keď už hovoríme o Swingu…

Spúšťanie swingáckych aplikácií

Áno, je to divné, ale aj swingácka aplikácia sa musí spustiť v inom vlákne než v hlavnom. Trieda SwingUtilities má metódu invokeLater(), ktorá nainicializuje aplikáciu v tom správnom vlákne, ibaže to všetko musíme dodať v … anonymnej vnútornej triede.

Musíme totiž deklarovať objekt typu Runnable, ktorý pošleme ako parameter:

public class TestFrame extends JFrame {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            public void run() {
                TestFrame frame = new TestFrame();
                frame.setVisible(true)
            }
        };
    }       
}

Opäť ten istý vzor.

Nabudúce

Nabudúce si ukážeme niekoľko trikov s anonymnými vnútornými triedami: ako možno pristupovať k premenným z vonkajšej triedy; finta, kde možno vytvárať podtriedy i z bežných tried a porovnáme to so syntaxou iných jazykov (lebo … vicemiss Zlokód.)

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

Napísať odpoveď pre Róbert Novotný Zrušiť odpoveď

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