Gyakorlati alapok III.
A JCF Iterable és Iterator interfészei
Amint azt már említettük, az
interfész
kötelezően kidolgozandó metódusfej, ebben a vonatkozásban pedig az adattípusok
“elvi szintű” absztrakciói, az adattípus lehető legáltalánosabb
megfogalmazása. Így bár szinte “filozófiai” szintre emelkedik, de mégis
viszonylag könnyen alakítható futtatható kóddá. Ez a fajta általánosítás már
önmagában is hasznos és megállja a helyét, azaz megszolgálja önnön
funkcionalitását, azonban további lehetőségként az interfészek éppúgy
alkothatnak származtatási hierarchiát, mint az osztályok (Interfész + öröklődés
című fejezet). Ekkor a
legáltalánosabb deklarációból még szintén elvi szinten lehetőségünk van az
interfészeket tovább specializálni, tehát egyre specifikusabb kollekciókat
származtatni.
Bevezetésképpen nézzük meg a JCF-ben meghatározott interfész-hierarchiát:
Forrás - Source: www.myshared.ru
Látható azonban, hogy a csúcson mégsem a Collection, hanem az
Iterable interfész áll. Az iterable szót bár meglehetősen lazán, de
lefordíthatjuk úgy, hogy adatszerkezetileg bejárható, iterálható, azaz a
kollekció minden egyes eleméhez szekvenciálisan hozzáférhetünk. Ez az
interfész semmi mást, csakis a kollekciók egyik legfontosabb tulajdonságát,
egyúttal követelményét deklarálja:
a kollekció minden egyes eleme legyen mindig elérhető és szerkezetileg bejárható (másképpen nincs sok értelme kollekciókat felépíteni). Ehhez pusztán egyetlen metódus szükséges: egy foreach ciklus vagy ezt helyettesítő metódus(ok).
Nézzük is meg gyorsan az Iterable interfész szabványos Java-kódját:
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Az interfész implementálása biztosítja, hogy az osztály hozzá fog tud férni az
iterálási szolgáltatáshoz, sőt ezen tulajdonságát
öröklődés
esetén az osztály leszármazottjai is megöröklik. A kód legfontosabb metódusa
az iterator (Iterator<T> iterator(),
amelyik voltaképpen a tényleges iterálást fogja elvégezni. Érdemes
áttanulmányoznunk szabványos Java-kódját:
package java.util;
import java.util.function. Consumer;
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining (Consumer< ? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
A sok elmélet után pedig nézzünk meg egy működő Java-kódot is. A kód
generikus (általános) típusból
indul ki és természetesen implementálja a Iterable interfészt. Az
allatkert nevű
String
tömb ABC-sorrendben tartalmaz állatneveket. Ez lesz a bemeneti
adattömbje a
Main nevű osztály konstruktorában beállított
kollekciónak, amelynek elemeit a
Collections.shuffle() metódus véletlenszerűen
keveri össze, mindezt pedig egy precízen működő
foreach
ciklus nyomtatja ki nekünk a konzolra:
import java.util.*;
public class Main<T> implements Iterable<T> {
private List<T> lista;
public Main(T [] t) {
lista = Arrays.asList(t);
Collections.shuffle(lista);
}
@Override
public Iterator<T> iterator() {
return lista.iterator();
}
public static void main(String [] args) {
String [] allatkert = {"Elefant", "Krokodil", "Majom",
"Oroszlan",
"Strucc"};
Main<String> allatkertiLista = new Main<>(allatkert);
for(String i : allatkertiLista) {
System.out.println(i);
}
}
}
Végeredmény (például):
Majom
Strucc
Elefant
Krokodil
Oroszlan
Egy másik metódus (Collections.swap()) képes
elemeket egymással felcserélni. Próbáljuk ki ezt is egy
Integer típusú tömbből átvezetett lista-kollekción, ekkor egyúttal kihasználjuk az osztály már
említett generikus tulajdonságát, nevezetesen: az osztálydefinícióban csakis
általános típusú elemeket (T) deklaráltunk, így
csak a példányosítás során szükséges konkrét típusokat megadnunk:
import java.util.*;
public class Main<T> implements Iterable<T> {
private List<T> lista;
public Main(T [] t) {
lista = Arrays.asList(t);
Collections.swap(lista, 0, 1);
}
@Override
public Iterator<T> iterator() {
return lista.iterator();
}
public static void main(String [] args) {
Integer [] szamok = {1, 2, 3, 4, 5};
Main<Integer> szamLista = new Main<>(szamok);
for(Integer i : szamLista) {
System.out.println(i);
}
}
}
Végeredmény:
2
1
3
4
5
Láthatóan az iterálás belső részleteivel nem kellett sokat törődnünk, azt a
felelős kódok automatikusan elvégezték, noha különböző adattípusokkal
dolgoztunk.
Gondoljunk csak bele, hogy milyen előny ez, hiszen már felfedezhettük, hogy
az adatszerkezetek valójában mennyire különbözőek!
Azonban szükséges észrevennünk, hogy az iterálás néha nem megy automatikusan.
Ilyen esetekben felül kell írnunk a származtatott metódusokat (override
-
A metódus felülírásának szabályai (overriding)
című fejezet), azaz a rendszer számára saját szabályokat szükséges
meghatározni.
Miért is van erre szükség? Mert a dolgok sokszor nem egyértelműek. A honlapon
erre már láttunk néhány példát, az egyik legelső az equals() függvény
körüli anomália volt (Mikor egyenlő az egyenlő avagy az equals() függvény
című fejezet), nevezetesen: mi alapján döntjük el 2 objektum egyenlőségét?
Kollekciók esetében ez azt a kérdést veti fel, hogy milyen szempontok szerint
iterálja a rendszer a számára ismeretlen (vagy előre nem definiált) szerkezetű
objektumot? Ilyenkor az iterálási szabályokat “kézi” vezérléssel kell
beállítanunk, amely legtöbbször az iterator() objektum teljes vagy részleges
felülírását, legfőképpen annak next() és
hasNext() metódusának
felüldefiniálását vonja maga után (a kódban ezt a @Override jelek alatt
tanulmányozhatjuk). Erre láthatunk egy izgalmas példát az alábbiakban. A
végeredmények a fenti kódokkal lényegében azonosak, de az iterálást kézi
vezérléssel állítottuk be:
import java.util.*;
public class Main<T> implements Iterable<T> {
private T[] tombLista;
private int aktualisTombMeret;
private T adat;
public Main(T[] tomb) {
this.tombLista = tomb;
this.aktualisTombMeret = tombLista.length;
}
@Override
public Iterator<T> iterator() {
Iterator<T> iterator = new Iterator<T>() {
private int i = 0;
@Override
public boolean hasNext() {
return i < aktualisTombMeret && tombLista[i] != null;
}
@Override
public T next() {
return tombLista[i++];
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
return iterator;
}
public static void main(String[] args) {
Integer[] szamok = new Integer[] {1, 2, 3, 4, 5};
Main<Integer> szamlista = new Main<Integer>(szamok);
for(Integer i : szamlista) {
System.out.println(i);
}
System.out.println("");
String[] allatkert = new String[]{"Elefant", "Krokodil", "Majom",
"Oroszlan",
"Strucc"};
Main<String> allatkertiLista = new Main<String>(allatkert);
for(String i : allatkertiLista) {
System.out.println(i);
}
}
}
Végeredmény:
1
2
3
4
5
Elefant
Krokodil
Majom
Oroszlan
Strucc