Interfaz iterable de Java: Iterator, ListIterator y Spliterator

Si bien podemos usar un ciclo for o while para recorrer una colección de elementos, un iterador nos permite hacerlo sin preocuparnos por las posiciones del índice y todo lo demás.

Introducción

Si bien podemos usar un bucle for o while para recorrer una colección de elementos, un Iterator nos permite hacerlo sin preocuparnos por las posiciones del índice e incluso nos permite no solo recorrer una colección, sino también modificar al mismo tiempo, lo que no siempre es posible con bucles for si está eliminando elementos en el bucle, por ejemplo.

Combine eso con la capacidad de implementar nuestro iterador personalizado para iterar a través de objetos mucho más complejos, así como avanzar y retroceder, y las ventajas de saber cómo usarlo se vuelven bastante claras.

Este artículo profundizará bastante en cómo se pueden usar las interfaces Iterator e Iterable.

Iterador()

La interfaz Iterator se utiliza para iterar sobre los elementos de una colección (List, Set o Map). Se utiliza para recuperar los elementos uno por uno y realizar operaciones sobre cada uno si es necesario.

Estos son los métodos utilizados para recorrer colecciones y realizar operaciones:

  • .hasNext(): Devuelve verdadero si no hemos llegado al final de una colección, devuelve falso en caso contrario
  • .next(): Devuelve el siguiente elemento de una colección
  • .remove(): elimina el último elemento devuelto por el iterador de la colección
  • .forEachRemaining(): realiza la acción dada para cada elemento restante en una colección, en orden secuencial

En primer lugar, dado que los iteradores están destinados a usarse con colecciones, hagamos una ArrayList simple con algunos elementos:

1
2
3
4
5
6
7
List<String> avengers = new ArrayList<>();

// Now lets add some Avengers to the list
avengers.add("Ant-Man");
avengers.add("Black Widow");
avengers.add("Captain America");
avengers.add("Doctor Strange");

Podemos iterar a través de esta lista usando un ciclo simple:

1
2
3
4
System.out.println("Simple loop example:\n");
for (int i = 0; i < avengers.size(); i++) {
    System.out.println(avengers.get(i));
}

Sin embargo, queremos explorar los iteradores:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
System.out.println("\nIterator Example:\n");

// First we make an Iterator by calling 
// the .iterator() method on the collection
Iterator<String> avengersIterator = avengers.iterator();

// And now we use .hasNext() and .next() to go through it
while (avengersIterator.hasNext()) {
    System.out.println(avengersIterator.next());
}

¿Qué sucede si queremos eliminar un elemento de este ArrayList? Tratemos de hacerlo usando el bucle normal for:

1
2
3
4
5
6
7
System.out.println("Simple loop example:\n");
for (int i = 0; i < avengers.size(); i++) {
    if (avengers.get(i).equals("Doctor Strange")) {
        avengers.remove(i);
    }
    System.out.println(avengers.get(i));
}

Seríamos recibidos con una IndexOutOfBoundsException desagradable:

1
2
3
4
5
6
Simple loop example:

Ant-Man
Black Widow
Captain America
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3

Esto tiene sentido ya que estamos alterando el tamaño de la colección a medida que la recorremos. Lo mismo ocurre con el bucle for avanzado:

1
2
3
4
5
6
7
System.out.println("Simple loop example:\n");
for (String avenger : avengers) {
    if (avenger.equals("Doctor Strange")) {
        avengers.remove(avenger);
    }
    System.out.println(avenger);
}

Nuevamente, somos recibidos con otra excepción:

1
2
3
4
5
6
7
Simple loop example:

Ant-Man
Black Widow
Captain America
Doctor Strange
Exception in thread "main" java.util.ConcurrentModificationException

Aquí es donde los iteradores resultan útiles, ya que actúan como intermediarios para eliminar el elemento de la colección, pero también para garantizar que el recorrido continúe según lo planeado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Iterator<String> avengersIterator = avengers.iterator();
while (avengersIterator.hasNext()) {
    String avenger = avengersIterator.next();

    // First we must find the element we wish to remove
    if (avenger.equals("Ant-Man")) {
        // This will remove "Ant-Man" from the original
        // collection, in this case a List
        avengersIterator.remove();
    }
}

Este es un método seguro garantizado para eliminar elementos mientras se recorren colecciones.

Y para validar si el artículo ha sido eliminado:

1
2
3
4
5
6
// We can also use the helper method .forEachRemaining()
System.out.println("For Each Remaining Example:\n");
Iterator<String> avengersIteratorForEach = avengers.iterator();

// This will apply System.out::println to all elements in the collection
avengersIteratorForEach.forEachRemaining(System.out::println);     

Y la salida es:

1
2
3
4
5
For Each Remaining Example:

Black Widow
Captain America
Doctor Strange

Como puedes ver, "Ant-Man" ha sido eliminado de la lista de vengadores.

ListIterator()

ListIterator amplía la interfaz Iterator. Solo se usa en Lists y puede iterar bidireccionalmente, lo que significa que puede iterar de adelante hacia atrás o de atrás hacia adelante. Tampoco tiene un elemento actual porque el cursor siempre se coloca entre 2 elementos en una Lista, por lo que debemos usar .previous() o .next() para acceder a un elemento.

¿Cuál es la diferencia entre un Iterator y un ListIterator?

Primero, el ‘Iterador’ se puede aplicar a cualquier colección - ‘Listas’, ‘Mapas’, ‘Colas’, ‘Conjuntos’, etc.

El ListIterator solo se puede aplicar a listas. Al agregar esta restricción, el ListIterator puede ser mucho más específico cuando se trata de métodos, por lo que se nos presenta una gran cantidad de métodos nuevos que nos ayudan a modificar las listas mientras recorremos.

Si está tratando con una implementación List (ArrayList, LinkedList, etc.), siempre es preferible usar ListIterator.

Estos son los métodos que probablemente usará:

  • .add(E e): Inserta un elemento en la Lista.
  • .remove(): elimina el último elemento devuelto por .next() o .previous() de la lista.
  • .set(E e): Reemplaza el último elemento devuelto por .next() o .previous() con el elemento especificado
  • .hasNext(): Devuelve verdadero si no hemos llegado al final de una Lista, devuelve falso en caso contrario.
  • .next(): Devuelve el siguiente elemento de una Lista.
  • .nextIndex(): Devuelve el índice del siguiente elemento.
  • .hasPrevious(): Devuelve verdadero si no hemos llegado al principio de una Lista, devuelve falso en caso contrario.
  • .previous(): Devuelve el elemento anterior en una Lista.
  • .previousIndex(): Devuelve el índice del elemento anterior.

Nuevamente, completemos una ArrayList con algunos elementos:

1
2
3
4
5
6
ArrayList<String> defenders = new ArrayList<>();

defenders.add("Daredevil");
defenders.add("Luke Cage");
defenders.add("Jessica Jones");
defenders.add("Iron Fist");

Usemos un ListIterator para recorrer una lista e imprimir los elementos:

1
2
3
4
5
ListIterator listIterator = defenders.listIterator(); 
  
System.out.println("Original contents of our List:\n");
while (listIterator.hasNext()) 
    System.out.print(listIterator.next() + System.lineSeparator()); 

Obviamente, funciona de la misma manera que el clásico Iterator. La salida es:

1
2
3
4
5
6
Original contents of our List: 

Daredevil
Luke Cage
Jessica Jones
Iron Fist

Ahora, intentemos modificar algunos elementos:

1
2
3
4
5
6
7
8
9
System.out.println("Modified contents of our List:\n");

// Now let's make a ListIterator and modify the elements
ListIterator defendersListIterator = defenders.listIterator();

while (defendersListIterator.hasNext()) {
    Object element = defendersListIterator.next();
    defendersListIterator.set("The Mighty Defender: " + element);
}

Imprimir la lista ahora produciría:

1
2
3
4
5
6
Modified contents of our List:

The Mighty Defender: Daredevil
The Mighty Defender: Luke Cage
The Mighty Defender: Jessica Jones
The Mighty Defender: Iron Fist

Ahora, avancemos y recorramos la lista hacia atrás, como algo que podemos hacer con ListIterator:

1
2
3
4
System.out.println("Modified List backwards:\n");
while (defendersListIterator.hasPrevious()) {
    System.out.println(defendersListIterator.previous());
}

Y la salida es:

1
2
3
4
5
6
Modified List backwards:

The Mighty Defender: Iron Fist
The Mighty Defender: Jessica Jones
The Mighty Defender: Luke Cage
The Mighty Defender: Daredevil

Separador()

La interfaz Spliterator es funcionalmente igual que un Iterator. Es posible que nunca necesite usar ‘Spliterator’ directamente, pero aún repasemos algunos casos de uso.

You should, however, first be somewhat familiar with Flujos de Java and Expresiones Lambda en Java.

Si bien enumeraremos todos los métodos que tiene Spliterator, el funcionamiento completo de la interfaz Spliterator está fuera del alcance de este artículo. Una cosa que cubriremos con un ejemplo es cómo ‘Spliterator’ puede usar la paralelización para atravesar de manera más eficiente un ‘Stream’ que podemos dividir.

Los métodos que usaremos cuando tratemos con el Spliterator son:

  • .characteristics(): Devuelve las características que tiene este Spliterator como un valor int. Éstos incluyen:
    • ORDERED
    • DISTINCT
    • SORTED
    • SIZED
    • CONCURRENT
    • IMMUTABLE
    • NONNULL
    • SUBSIZED
  • .estimateSize(): Devuelve una estimación del número de elementos que encontraría un recorrido como un valor largo, o devuelve long.MAX_VALUE si no puede calcular.
  • .forEachRemaining(E e): Realiza la acción dada para cada elemento restante en una colección, en orden secuencial.
  • .getComparator(): Si la fuente de este Spliterator está ordenada por un Comparator, devuelve ese Comparator.
  • .getExactSizeIfKnown(): Devuelve .estimateSize() si se conoce el tamaño, de lo contrario devuelve -1
  • .hasCharacteristics(características int): Devuelve true si .characteristics() de este Spliterator contiene todas las características dadas.
  • .tryAdvance(E e): si existe un elemento restante, realiza la acción dada en él, devolviendo true, de lo contrario devuelve false.
  • .trySplit(): si este Spliterator se puede particionar, devuelve un Spliterator que cubre elementos que, al regresar de este método, no estarán cubiertos por este Spliterator.

Como de costumbre, comencemos con un ArrayList simple:

1
2
3
4
5
6
7
8
List<String> mutants = new ArrayList<>();

mutants.add("Professor X");
mutants.add("Magneto");
mutants.add("Storm");
mutants.add("Jean Grey");
mutants.add("Wolverine");
mutants.add("Mystique");

Ahora, necesitamos aplicar el ‘Spliterator’ a un ‘Stream’. Afortunadamente, es fácil convertir entre un ArrayList y un Stream debido al marco de Collections:

1
2
3
4
5
// Obtain a Stream to the mutants List.
Stream<String> mutantStream = mutants.stream();

// Getting Spliterator object on mutantStream.
Spliterator<String> mutantList = mutantStream.spliterator();

Y para mostrar algunos de estos métodos, ejecutemos cada uno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// .estimateSize() method
System.out.println("Estimate size: " + mutantList.estimateSize());

// .getExactSizeIfKnown() method
System.out.println("\nExact size: " + mutantList.getExactSizeIfKnown());

System.out.println("\nContent of List:");
// .forEachRemaining() method
mutantList.forEachRemaining((n) -> System.out.println(n));

// Obtaining another Stream to the mutant List.
Spliterator<String> splitList1 = mutantStream.spliterator();

// .trySplit() method
Spliterator<String> splitList2 = splitList1.trySplit();

// If splitList1 could be split, use splitList2 first.
if (splitList2 != null) {
    System.out.println("\nOutput from splitList2:");
    splitList2.forEachRemaining((n) -> System.out.println(n));
}

// Now, use the splitList1
System.out.println("\nOutput from splitList1:");
splitList1.forEachRemaining((n) -> System.out.println(n));

Y obtenemos esto como salida:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Estimate size: 6

Exact size: 6

Content of List: 
Professor X
Magneto
Storm
Jean Grey
Wolverine
Mystique

Output from splitList2: 
Professor X
Magneto
Storm

Output from splitList1: 
Jean Grey
Wolverine
Mystique

Iterable()

¿Qué pasa si por alguna razón nos gustaría hacer una interfaz Iterator personalizada? Lo primero que debe conocer es este gráfico:

Para hacer nuestro Iterator personalizado necesitaríamos escribir métodos personalizados para .hasNext(), .next() y .remove() .

Dentro de la interfaz Iterable, tenemos un método que devuelve un iterador para elementos en una colección, ese es el método .iterator(), y un método que realiza una acción para cada elemento en un iterador, .forEach () método.

Por ejemplo, imaginemos que somos Tony Stark y necesitamos escribir un iterador personalizado para enumerar todos los trajes de Iron Man que tienes actualmente en tu arsenal.

Primero, hagamos una clase para obtener y configurar los datos del traje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Suit {

    private String codename;
    private int mark;

    public Suit(String codename, int mark) {
        this.codename = codename;
        this.mark = mark;
    }

    public String getCodename() { return codename; }

    public int getMark() { return mark; }

    public void setCodename (String codename) {this.codename=codename;}

    public void setMark (int mark) {this.mark=mark;}

    public String toString() {
        return "mark: " + mark + ", codename: " + codename;
    }
}

A continuación, escribamos nuestro iterador personalizado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Our custom Iterator must implement the Iterable interface
public class Armoury implements Iterable<Suit> {
    
    // Notice that we are using our own class as a data type
    private List<Suit> list = null;

    public Armoury() {
        // Fill the List with data
        list = new LinkedList<Suit>();
        list.add(new Suit("HOTROD", 22));
        list.add(new Suit("SILVER CENTURION", 33));
        list.add(new Suit("SOUTHPAW", 34));
        list.add(new Suit("HULKBUSTER 2.0", 48));
    }
    
    public Iterator<Suit> iterator() {
        return new CustomIterator<Suit>(list);
    }

    // Here we are writing our custom Iterator
    // Notice the generic class E since we do not need to specify an exact class
    public class CustomIterator<E> implements Iterator<E> {
    
        // We need an index to know if we have reached the end of the collection
        int indexPosition = 0;
        
        // We will iterate through the collection as a List
        List<E> internalList;
        public CustomIterator(List<E> internalList) {
            this.internalList = internalList;
        }

        // Since java indexes elements from 0, we need to check against indexPosition +1
        // to see if we have reached the end of the collection
        public boolean hasNext() {
            if (internalList.size() >= indexPosition +1) {
                return true;
            }
            return false;
        }

        // This is our custom .next() method
        public E next() {
            E val = internalList.get(indexPosition);

            // If for example, we were to put here "indexPosition +=2" we would skip every 
            // second element in a collection. This is a simple example but we could
            // write very complex code here to filter precisely which elements are
            // returned. 
            // Something which would be much more tedious to do with a for or while loop
            indexPosition += 1;
            return val;
        }
        // In this example we do not need a .remove() method, but it can also be 
        // written if required
    }
}

Y finalmente la clase principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class IronMan {

    public static void main(String[] args) {

        Armoury armoury = new Armoury();

        // Instead of manually writing .hasNext() and .next() methods to iterate through 
        // our collection we can simply use the advanced forloop
        for (Suit s : armoury) {
            System.out.println(s);
        }
    }
}

La salida es:

1
2
3
4
mark: 22, codename: HOTROD
mark: 33, codename: SILVER CENTURION
mark: 34, codename: SOUTHPAW
mark: 48, codename: HULKBUSTER 2.0

Conclusión

En este artículo, cubrimos en detalle cómo trabajar con iteradores en Java e incluso escribimos uno personalizado para explorar todas las nuevas posibilidades de la interfaz Iterable.

También mencionamos cómo Java aprovecha la paralelización de flujos para optimizar internamente el recorrido a través de una colección utilizando la interfaz Spliterator. r`.

Licensed under CC BY-NC-SA 4.0