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:

 

www.informatika-programozas.hu

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:

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

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:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 


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:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 


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.

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

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:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 


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