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
Iteratores de un solo uso. Si agotas un iterador, obtén uno nuevo coniterable.iterator(). for-eaches 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
Iterableda compatibilidad automática confor-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
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.