Iterable, Iterator y el for-each

Intermedio
Java
Java
Actualizado: 18/04/2026

Iterable: lo que se puede recorrer

java.lang.Iterable<E> es la interfaz raíz de cualquier colección recorrible en Java. Declarar una clase Iterable<E> permite usarla en un bucle for-each.

public interface Iterable<E> {
    Iterator<E> iterator();

    default void forEach(Consumer<? super E> action) { ... }
    default Spliterator<E> spliterator() { ... }
}

La única operación obligatoria es iterator(): devolver un nuevo iterador cada vez que se llame. Las otras dos vienen con implementación por defecto.

Iterator: protocolo de recorrido

Iterator<E> representa el estado de una iteración en curso. Sus métodos:

public interface Iterator<E> {
    boolean hasNext(); // ¿queda algún elemento?
    E next(); // obtener el siguiente (y avanzar)
    default void remove(); // eliminar el último elemento retornado por next()
    default void forEachRemaining(Consumer<? super E> action);
}

Uso clásico con iterador explícito:

List<String> lista = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = lista.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

For-each: azúcar sintáctico

El bucle for-each (o enhanced for) es equivalente al código anterior:

for (String s : lista) {
    System.out.println(s);
}

Es azúcar sintáctico: el compilador lo traduce internamente a:

// Para Iterable (colecciones)
for (Iterator<String> it = lista.iterator(); it.hasNext(); ) {
    String s = it.next();
    System.out.println(s);
}

// Para arrays (sin iteradores)
for (int i = 0; i < array.length; i++) {
    String s = array[i];
    System.out.println(s);
}

Por eso funciona tanto sobre Iterable como sobre arrays. Cualquier clase que implementes con Iterable será compatible con for-each.

Modificar durante iteración: iterator.remove()

Nunca modifiques una colección directamente durante un for-each:

// MAL: lanza ConcurrentModificationException
for (String s : lista) {
    if (s.startsWith("x")) {
        lista.remove(s); // bum
    }
}

La forma correcta es usar el método remove() del iterador, que elimina el último elemento devuelto por next():

Iterator<String> it = lista.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.startsWith("x")) {
        it.remove(); // seguro
    }
}

Alternativa moderna: Collection.removeIf(Predicate):

lista.removeIf(s -> s.startsWith("x"));

removeIf es más conciso y lo recomendado para filtrar in-place.

ConcurrentModificationException

El nombre despista: no significa necesariamente multi-hilo. Es el error que lanza un iterador cuando detecta que la colección ha cambiado desde que él se creó:

List<Integer> lista = new ArrayList<>(List.of(1, 2, 3));
for (int n : lista) {
    if (n == 2) lista.add(4); // ConcurrentModificationException
}

Las implementaciones clásicas (ArrayList, HashSet, etc.) son fail-fast: usan un contador interno (modCount) y lo comparan al iterar. Si no coincide, saltan excepción.

ListIterator: bidireccional y modificable

ListIterator<E> extiende Iterator<E> añadiendo:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void set(E e); // reemplaza el último elemento retornado
    void add(E e); // inserta en la posición actual
}

Solo funciona con List:

List<String> lista = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> it = lista.listIterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) {
        it.set("BETA"); // reemplaza "b" por "BETA"
        it.add("después"); // inserta después de "BETA"
    }
}
// Resultado: [a, BETA, después, c]

Implementar tu propio Iterable

Supón una clase Rango que representa enteros de inicio a fin:

public class Rango implements Iterable<Integer> {
    private final int inicio;
    private final int fin;

    public Rango(int inicio, int fin) {
        this.inicio = inicio;
        this.fin = fin;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int actual = inicio;

            @Override
            public boolean hasNext() {
                return actual < fin;
            }

            @Override
            public Integer next() {
                if (!hasNext()) throw new NoSuchElementException();
                return actual++;
            }
        };
    }
}

// Uso natural con for-each
Rango r = new Rango(1, 5);
for (int n : r) {
    System.out.println(n); // 1, 2, 3, 4
}

También funcionan forEach, spliterator(), y por tanto se puede convertir a stream:

// StreamSupport.stream(rango.spliterator(), false)

Patrones útiles

Iteración con índice (no directa en for-each)

List<String> lista = ...;
for (int i = 0; i < lista.size(); i++) {
    System.out.println(i + ": " + lista.get(i));
}

O con IntStream.range:

IntStream.range(0, lista.size())
.forEach(i -> System.out.println(i + ": " + lista.get(i)));

Iterar en orden inverso

Con ListIterator:

ListIterator<String> it = lista.listIterator(lista.size());
while (it.hasPrevious()) {
    System.out.println(it.previous());
}

Con Java 21+ y SequencedCollection:

for (String s : lista.reversed()) {
    System.out.println(s);
}

Combinar dos iteradores

Iterator<String> it1 = lista1.iterator();
Iterator<Integer> it2 = lista2.iterator();
while (it1.hasNext() && it2.hasNext()) {
    System.out.println(it1.next() + " -> " + it2.next());
}

Integración con Collectors y Streams

Cualquier Iterable se puede convertir a Stream:

Iterable<Integer> origen = ...;
Stream<Integer> stream = StreamSupport.stream(origen.spliterator(), false);

Si la clase implementa Collection, tiene stream() directo.

Limitaciones y buenas prácticas

  • Un Iterator es de un solo uso. Si agotas un iterador, obtén uno nuevo con iterable.iterator().
  • for-each es más limpio que índices manuales; úsalo salvo que necesites el índice.
  • Para eliminar elementos, prefiere removeIf(Predicate) a bucle + iterator.remove().
  • Al escribir una clase, implementar Iterable da compatibilidad automática con for-each, Stream, Collectors.

Resumen

Iterable y Iterator son las piezas fundamentales del recorrido en Java. El for-each los consume transparentemente. Conocer cómo funcionan por dentro permite:

  • Entender excepciones como ConcurrentModificationException.
  • Eliminar durante iteración con seguridad.
  • Implementar colecciones personalizadas compatibles con el lenguaje.
  • Construir iteradores perezosos, infinitos o derivados.

Es uno de los patrones más simples y utiles del JDK.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Java es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Java

Explora más contenido relacionado con Java y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender el contrato de Iterable<E> y Iterator<E>. Saber qué hace el compilador al traducir un for-each. Usar iterator.remove() para eliminar durante la iteración. Entender el fallo ConcurrentModificationException. Implementar tu propio iterador cuando creas una clase iterable. Usar ListIterator para bidireccionalidad y modificación.