Programování

Iterace nad kolekcemi v Javě

Kdykoli máte sbírku věcí, budete potřebovat nějaký mechanismus k systematickému procházení položek v této kolekci. Jako každodenní příklad zvažte televizní dálkové ovládání, které nám umožňuje iterovat různými televizními kanály. Podobně ve světě programování potřebujeme mechanismus, který bude systematicky iterovat prostřednictvím kolekce softwarových objektů. Java zahrnuje různé mechanismy pro iteraci, včetně index (pro iteraci přes pole), kurzor (pro iteraci nad výsledky databázového dotazu), výčet (v dřívějších verzích Java) a iterátor (v novějších verzích Java).

Iterátorský vzor

An iterátor je mechanismus, který umožňuje sekvenční přístup ke všem prvkům kolekce, přičemž u každého prvku se provádí určitá operace. V zásadě poskytuje iterátor prostředek „smyčky“ nad zapouzdřenou kolekcí objektů. Mezi příklady použití iterátorů patří

  • Navštivte každý soubor v adresáři (aka složka) a zobrazit její název.
  • Navštivte každý uzel v grafu a určete, zda je dosažitelný z daného uzlu.
  • Navštivte každého zákazníka ve frontě (například simulace linky v bance) a zjistěte, jak dlouho čeká.
  • Navštivte každý uzel v abstraktním stromu syntaxe kompilátoru (který produkuje analyzátor) a proveďte sémantickou kontrolu nebo generování kódu. (V tomto kontextu můžete také použít vzor návštěvníka.)

Pro použití iterátorů platí určité zásady: Obecně byste měli mít možnost mít současně spuštěno několik přechodů; to znamená, že iterátor by měl umožnit koncept vnořené smyčky. Iterátor by měl být také nedestruktivní v tom smyslu, že proces iterace by sám o sobě neměl měnit kolekci. Samozřejmě operace prováděná s prvky v kolekci by mohla případně změnit některé z prvků. Může být také možné, aby iterátor podporoval odebrání prvku z kolekce nebo vložení nového prvku v určitém bodě kolekce, ale takové změny by měly být v rámci programu explicitní a nikoli vedlejším produktem iterace. V některých případech budete také muset mít iterátory s různými metodami procházení; například předobjednávka a poobjednávka prochází stromem nebo prochází hloubkou a šířkou prvního grafu.

Iterace složitých datových struktur

Poprvé jsem se naučil programovat v rané verzi FORTRANU, kde jedinou schopností strukturování dat bylo pole. Rychle jsem se naučil, jak iterovat přes pole pomocí indexu a smyčky DO. Odtamtud už byl jen krátký mentální skok k myšlence použití společného indexu do více polí k simulaci řady záznamů. Většina programovacích jazyků má funkce podobné polím a podporuje přímou smyčku přes pole. Ale moderní programovací jazyky také podporují složitější datové struktury, jako jsou seznamy, sady, mapy a stromy, kde jsou funkce zpřístupněny prostřednictvím veřejných metod, ale vnitřní podrobnosti jsou skryty v soukromých částech třídy. Programátoři musí být schopni procházet prvky těchto datových struktur, aniž by odhalili jejich vnitřní strukturu, což je účel iterátorů.

Iterátory a gang čtyř návrhových vzorů

Podle Gangu čtyř (viz níže) se Iterátor návrhový vzor je vzor chování, jehož klíčovou myšlenkou je „převzít odpovědnost za přístup a procházení ze seznamu [vyd. think collection] objekt a vložte jej do iteračního objektu. "Tento článek není tak o iterátorském vzoru, jako o tom, jak se iterátory používají v praxi. Úplné pokrytí vzoru by vyžadovalo diskusi o tom, jak by byl iterátor navržen, účastníci ( objekty a třídy) v návrhu, možné alternativní návrhy a kompromisy různých alternativ designu. Raději bych se zaměřil na to, jak se v praxi používají iterátory, ale ukážu vám několik zdrojů pro prozkoumání Iteratorova vzoru a návrhových vzorů obvykle:

  • Návrhové vzory: Prvky opakovaně použitelného objektově orientovaného softwaru (Addison-Wesley Professional, 1994) od Ericha Gammy, Richarda Helma, Ralpha Johnsona a Johna Vlissidesa (také známého jako Gang of Four nebo jednoduše GoF) je definitivním zdrojem pro poznávání návrhových vzorů. Ačkoli byla kniha poprvé vydána v roce 1994, zůstává klasickou, o čemž svědčí skutečnost, že zde bylo více než 40 tisků.
  • Bob Tarr, přednášející na University of Maryland v Baltimore County, má vynikající sadu snímků pro svůj kurz designových vzorů, včetně jeho uvedení do Iteratorova vzoru.
  • Série JavaWorld Davida Gearyho Java designové vzory představuje mnoho návrhových vzorů Gang of Four, včetně vzorů Singleton, Observer a Composite. Jeff Friesen také v JavaWorld obsahuje třídílný přehled návrhových vzorů průvodce po vzorech GoF.

Aktivní iterátory vs pasivní iterátory

Existují dva obecné přístupy k implementaci iterátoru v závislosti na tom, kdo řídí iteraci. Pro aktivní iterátor (také známý jako explicitní iterátor nebo externí iterátor), klient řídí iteraci v tom smyslu, že klient vytvoří iterátor, řekne mu, kdy přejít na další prvek, otestuje, zda byl každý prvek navštíven atd. Tento přístup je běžný v jazycích, jako je C ++, a právě tomuto přístupu se v knize GoF dostává největší pozornosti. Ačkoli iterátory v Javě získaly různé formy, použití aktivního iterátoru bylo v podstatě jedinou schůdnou možností před verzí Java 8.

Pro pasivní iterátor (také známý jako implicitní iterátor, interní iterátornebo iterátor zpětného volání), iterátor sám řídí iteraci. Klient v podstatě říká iterátoru: „proveďte tuto operaci s prvky v kolekci.“ Tento přístup je běžný v jazycích, jako je LISP, které poskytují anonymní funkce nebo uzávěrky. S vydáním Java 8 je nyní tento přístup k iteraci rozumnou alternativou pro programátory Java.

Jména 8 schémat pojmenování

I když to není tak špatné jako Windows (NT, 2000, XP, VISTA, 7, 8, ...), historie verzí Java obsahuje několik schémat pojmenování. Začneme tím, že bychom měli standardní verzi Java označovat jako „JDK“, „J2SE“ nebo „Java SE“? Čísla verzí Javy začínají docela jednoduše - 1,0, 1,1 atd. - ale všechno se změnilo s verzí 1.5, která byla označena jako Java (nebo JDK) 5. Když mluvím o dřívějších verzích Javy, používám fráze jako „Java 1.0“ nebo „Java 1.1, „ale po páté verzi Java používám fráze jako„ Java 5 “nebo„ Java 8. “

Pro ilustraci různých přístupů k iteraci v Javě potřebuji příklad kolekce a něco, co je třeba udělat s jejími prvky. Pro úvodní část tohoto článku použiji kolekci řetězců představujících názvy věcí. Pro každé jméno v kolekci jednoduše vytisknu jeho hodnotu na standardní výstup. Tyto základní myšlenky lze snadno rozšířit na sbírky složitějších objektů (například zaměstnanců) a tam, kde je zpracování každého objektu trochu více zapojeno (jako dát každému vysoce hodnocenému zaměstnanci zvýšení o 4,5 procenta).

Jiné formy iterace v Javě 8

Zaměřuji se na iteraci sbírek, ale v Javě existují i ​​jiné, specializovanější formy iterace. Můžete například použít JDBC ResultSet iterovat přes řádky vrácené z dotazu SELECT do relační databáze, nebo použít a Skener iterovat přes vstupní zdroj.

Iterace s Enumeration třídou

V prostředí Java 1.0 a 1.1 byly dvě primární třídy kolekce Vektor a Hashtablea návrhový vzor Iterator byl implementován ve třídě s názvem Výčet. Při zpětném pohledu to bylo pro třídu špatné jméno. Nezaměňujte třídu Výčet s konceptem typy výčtu, který se objevil až v Javě 5. Dnes oba Vektor a Hashtable jsou obecné třídy, ale tehdy generika nebyla součástí jazyka Java. Kód pro zpracování vektoru řetězců pomocí Výčet bude vypadat něco jako výpis 1.

Výpis 1. Použití výčtu k iteraci přes vektor řetězců

 Názvy vektorů = new Vector (); // ... přidat několik jmen do kolekce Enumeration e = names.elements (); while (e.hasMoreElements ()) {Název řetězce = (Řetězec) e.nextElement (); System.out.println (název); } 

Iterace s třídou Iterator

Java 1.2 představila třídy kolekce, které všichni známe a milujeme, a návrhový vzor Iterator byl implementován ve třídě s příslušným názvem Iterátor. Protože jsme ještě v Java 1.2 neměli generika, casting objektu vráceného z Iterátor bylo stále nutné. U verzí Java 1.2 až 1.4 může iterace přes seznam řetězců vypadat jako výpis 2.

Výpis 2. Použití iterátoru k iteraci seznamu řetězců

 Názvy seznamu = new LinkedList (); // ... přidat několik jmen do kolekce Iterator i = names.iterator (); while (i.hasNext ()) {Název řetězce = (Řetězec) i.next (); System.out.println (název); } 

Iterace s generikami a vylepšená smyčka for

Java 5 nám dala generika, rozhraní Iterablea vylepšená smyčka for. Vylepšená smyčka for-loop je jedním z mých nejoblíbenějších malých přírůstků do Javy. Vytvoření iterátoru a volání jeho hasNext () a další() metody nejsou v kódu výslovně vyjádřeny, ale stále se odehrávají v zákulisí. I když je tedy kód kompaktnější, stále používáme aktivní iterátor. V prostředí Java 5 by náš příklad vypadal podobně jako v seznamu 3.

Výpis 3. Použití generik a vylepšené smyčky pro opakování seznamu řetězců

 Názvy seznamu = new LinkedList (); // ... přidat několik jmen do kolekce pro (Název řetězce: jména) System.out.println (jméno); 

Java 7 nám dala diamantového operátora, který snižuje výřečnost generik. Pryč byly dny, kdy bylo nutné opakovat typ použitý k vytvoření instance obecné třídy po vyvolání Nový operátor! V prostředí Java 7 bychom mohli zjednodušit první řádek v seznamu 3 výše na následující:

 Názvy seznamu = new LinkedList (); 

Mírný vztek proti generikům

Návrh programovacího jazyka zahrnuje kompromisy mezi výhodami jazykových funkcí oproti složitosti, kterou ukládají syntaxi a sémantice jazyka. U generik nejsem přesvědčen, že přínosy převažují nad složitostí. Generics vyřešil problém, který jsem s Javou neměl. Obecně souhlasím s názorem Kena Arnolda, když prohlašuje: "Generika jsou chybou. Toto není problém založený na technických neshodách. Je to zásadní problém jazykového designu [...] Složitost Javy byla přeplňována tím, co se mi zdá relativně malá výhoda. “

Naštěstí, zatímco navrhování a implementace generických tříd může být někdy příliš komplikované, zjistil jsem, že používání generických tříd v praxi je obvykle jednoduché.

Iterace metodou forEach ()

Než se pustíme do iteračních funkcí Java 8, pojďme se zamyslet nad tím, co se děje s kódem zobrazeným v předchozích seznamech - což je vlastně nic. V aktuálně nasazených aplikacích, které používají aktivní iterátory podobné těm, které jsou uvedeny v mých seznamech, existují miliony řádků kódu Java. Java 8 jednoduše poskytuje další funkce a nové způsoby provádění iterace. U některých scénářů mohou být nové způsoby lepší.

Hlavní nové funkce v prostředí Java 8 se soustředí na výrazy lambda spolu se souvisejícími funkcemi, jako jsou proudy, odkazy na metody a funkční rozhraní. Tyto nové funkce v prostředí Java 8 nám umožňují vážně zvážit použití pasivních iterátorů namísto běžnějších aktivních iterátorů. Zejména Iterable interface poskytuje pasivní iterátor ve formě výchozí metody zvané pro každého().

A výchozí metoda, další nová funkce v prostředí Java 8, je metoda v rozhraní s výchozí implementací. V tomto případě pro každého() metoda je ve skutečnosti implementována pomocí aktivního iterátoru podobným způsobem, jaký jste viděli ve výpisu 3.

Třídy kolekce, které se implementují Iterable (například všechny třídy seznamu a tříd) nyní mají pro každého() metoda. Tato metoda trvá jeden parametr, který je funkčním rozhraním. Proto je skutečný parametr předán do pro každého() metoda je kandidátem na výraz lambda. Pomocí funkcí prostředí Java 8 by se náš běžící příklad vyvinul do podoby zobrazené v seznamu 4.

Výpis 4. Iterace v prostředí Java 8 pomocí metody forEach ()

 Názvy seznamu = new LinkedList (); // ... přidejte některá jména do sbírky names.forEach (name -> System.out.println (name)); 

Všimněte si rozdílu mezi pasivním iterátorem v seznamu 4 a aktivním iterátorem v předchozích třech seznamech. V prvních třech seznamech řídí struktura smyčky iteraci a během každého průchodu smyčkou je objekt načten ze seznamu a poté vytištěn. V seznamu 4 neexistuje žádná explicitní smyčka. Jednoduše řekneme pro každého() metoda co dělat s objekty v seznamu - v tomto případě jednoduše vytiskneme objekt. Řízení iterace spočívá v pro každého() metoda.

Iterace s proudy Java

Nyní uvažujme o tom, že uděláme něco trochu více zapojeného než jen tisk jmen v našem seznamu. Předpokládejme například, že chceme spočítat počet jmen, která začínají písmenem A. Složitější logiku bychom mohli implementovat jako součást výrazu lambda, nebo bychom mohli použít nové Stream API Java 8. Pojďme použít druhý přístup.