Série 8 – Itérateurs : corrigé

Introduction

Le code du corrigé est disponible sous la forme d'une archive Zip. Les solutions aux différents exercices sont brièvement discutées ci-dessous.

Exercice 1 – itérateur en lecture seule

La définition de la classe ReadOnlyIterator ne pose aucun problème, sa seule méthode étant triviale.

public abstract class ReadOnlyIterator<E> implements Iterator<E> {
    @Override
    final public void remove() {
        throw new UnsupportedOperationException();
    }
}

Exercice 2 – itérateur infini

La définition de la classe InfiniteIterator ne pose pas plus de problème que celle de la classe ReadOnlyIterator.

public abstract class InfiniteIterator<E>
        extends ReadOnlyIterator<E> {
    @Override
    final public boolean hasNext() {
        return true;
    }
}

Exercice 3 – itérateur entier infini

La classe IntIterator doit mémoriser deux informations : la valeur courante de l'itérateur (champ curr) et le pas (champ step). Ces deux valeurs sont initialisées dans le constructeur, et la méthode next calcule la prochaine valeur, la stocke dans le champ curr et retourne celui-ci.

public final class IntIterator extends InfiniteIterator<Integer> {
    private final int step;
    private int curr;

    public IntIterator(int start, int step) {
        this.curr = start - step;
        this.step = step;
    }

    @Override
    public Integer next() {
        curr += step;
        return curr;
    }
}

Bien entendu, il est également possible de mémoriser la prochaine valeur que next doit retourner, mais cela rend le code de cette méthode légèrement plus complexe, raison pour laquelle la solution de stocker la valeur courante (donc la valeur qui précède celle que next doit retourner) a été choisie.

Exercice 4 – itérateur filtrant

L'itérateur filtrant est plus délicat à définir que les précédents. L'idée principale consiste à conserver le prochain élément que la méthode next doit retourner (champ next), obtenu de l'itérateur sous-jacent, ainsi qu'un booléen indiquant la validité de ce champ (champ isNextValid).

La méthode hasNext se charge d'obtenir de l'itérateur sous-jacent la prochaine valeur à retourner, s'il y en a une. Pour ce faire, elle obtient des valeurs de l'itérateur sous-jacent jusqu'à ce que l'une d'elles soit acceptée par shouldInclude. Si une telle valeur est trouvée, alors elle est gardée dans next et le booléen isNextValid est vrai. Si l'itérateur sous-jacent est épuisé avant qu'une telle valeur ne soit trouvée, le champ isNextValid est faux.

La méthode next quant à elle ne fait rien d'autre que retourner la valeur du champ next en remettant le champ isNextValid à faux, afin d'indiquer que celle contenue dans next est maintenant invalide (puisqu'elle a été fournie) et que la prochaine valeur doit donc être obtenue.

Notez bien que, étant donné que next commence par appeler hasNext, cette dernière peut être laissée seule responsable de l'obtention de la prochaine valeur.

public abstract class FilteringIterator<E>
        extends ReadOnlyIterator<E> {
    private final Iterator<E> underlyingIt;
    private E next;
    private boolean isNextValid;

    public FilteringIterator(Iterator<E> underlyingIt) {
        this.underlyingIt = underlyingIt;
        this.next = null;
        this.isNextValid = false;
    }

    public abstract boolean shouldInclude(E elem);

    @Override
    public boolean hasNext() {
        while (!isNextValid && underlyingIt.hasNext()) {
            next = underlyingIt.next();
            isNextValid = shouldInclude(next);
        }
        return isNextValid;
    }

    @Override
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        isNextValid = false;
        return next;
    }
}

Exercice 5 – itérateur filtrant les multiples

La classe MultiplesFilteringIterator doit mémoriser la totalité des entiers qu'elle a déjà produits afin de ne jamais produire de multiple de l'un d'entre eux.

Les entiers produits sont stockés dans une collection (champ produced), ici une liste de type ArrayList. La méthode next est redéfinie afin d'ajouter chaque élément produit à cette collection avant de le retourner.

La méthode shouldInclude quant à elle ne retourne vrai que si l'élément candidat qu'elle reçoit n'est multiple d'aucun des éléments déjà fournis.

public class MultiplesFilteringIterator
        extends FilteringIterator<Integer> {
    private final List<Integer> produced = new ArrayList<>();

    public MultiplesFilteringIterator(Iterator<Integer> underIt) {
        super(underIt);
    }

    @Override
    public Integer next() {
        Integer n = super.next();
        produced.add(n);
        return n;
    }

    @Override
    public boolean shouldInclude(Integer elem) {
        for (int p: produced) {
            if (elem % p == 0)
                return false;
        }
        return true;
    }
}

Lorsqu'on applique cet itérateur filtrant à l'itérateur produisant les entiers à partir de 2, on obtient un itérateur sur les nombres premiers ! En effet, les seuls nombres acceptés par shouldInclude sont ceux qui ne sont multiple d'aucun entier supérieur ou égal à deux, ce qui est la définition d'un nombre premier.

L'idée utilisée par cet itérateur est similaire à celle du crible d'Eratosthène, mais moins efficace pour les raisons expliquées dans l'article The Genuine Sieve of Eratosthenes que les personnes intéressées (et très motivées) pourront lire.