Jerarquía de Collection Framework
flowchart TB
Iter[Iterable] --> Coll[Collection]
Coll --> List[List]
Coll --> Set[Set]
Coll --> Queue[Queue Deque]
List --> AL[ArrayList]
List --> LL[LinkedList]
Set --> HS[HashSet]
Set --> TS[TreeSet]
Set --> LHS[LinkedHashSet]
Queue --> Pri[PriorityQueue]
Queue --> ArrD[ArrayDeque]
Coll --> Map[Map separado]
Map --> HM[HashMap TreeMap]
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.
Caso B2B: streaming de datos en banca y telco
En sistemas bancarios donde vuestro equipo procesa ficheros SEPA o cuadernos AEB que llegan a millones de registros, implementar Iterable<Movimiento> sobre un lector de fichero permite recorrer el contenido sin cargarlo entero en memoria. Cada llamada a next() lee la siguiente línea del fichero. Este patrón aparece en pasarelas de cobros que ingieren ficheros nocturnos sin llegar nunca a tener más de unos KB en heap, fundamental cuando el contenedor en Kubernetes tiene límite de 512 MB.
En telco, los pipelines de procesamiento de CDR (Call Detail Records) usan iteradores sobre cursores JDBC con fetchSize ajustado para evitar cargar la tabla completa. Combinado con try-with-resources sobre el ResultSet, el patrón mantiene el consumo de memoria constante incluso cuando la consulta devuelve decenas de millones de filas.
En administraciones públicas, sistemas de notificación masiva implementan iteradores que paginan llamadas REST a un servicio externo (/notificaciones?page=N); cada next() dispara la siguiente página de forma transparente. La organización gana porque el código consumidor usa for-each sin enterarse de la paginación.
Versiones y disponibilidad
Iterable<E> e Iterator<E> están en java.lang y java.util desde Java 5 (2004). El método forEach(Consumer) por defecto se añadió en Java 8. forEachRemaining también en Java 8. Collection.removeIf(Predicate) desde Java 8. Java 21 LTS y Java 25 LTS los mantienen sin cambios. La interfaz Spliterator (Java 8) abrió la puerta a streams paralelos sobre cualquier Iterable.
Anti-patrones y pitfalls
Llamar a next() sin comprobar hasNext(). Provoca NoSuchElementException y suele ocurrir en código que asume que la colección no está vacía. En lectores de fichero que pueden devolver cero filas en producción, este bug aparece solo bajo condiciones reales.
Reusar un iterador agotado. Tras consumir todos los elementos, llamar a next() lanza excepción. Para volver a recorrer, pedid un nuevo iterador con iterable.iterator().
Modificar la colección dentro del for-each sin pasar por el iterador. La excepción ConcurrentModificationException es engañosa: muchos equipos creen que es solo concurrencia entre hilos. Es fail-fast incluso en un solo hilo cuando la colección detecta cambios estructurales no autorizados.
Iterators no thread-safe en ConcurrentHashMap. Sus iteradores son débilmente consistentes: ven un snapshot lógico que no lanza CME, pero no garantizan ver todas las modificaciones concurrentes. En sistemas de auditoría B2B esto puede provocar que un evento añadido durante la iteración no aparezca en el reporte.
Iterador infinito sin límite. Generadores perezosos sin limit provocan OutOfMemoryError o bucles infinitos al hacer collect(toList()). Usad Stream.limit(n) antes de materializar.
Comparativa con alternativas
Frente a arrays y bucles indexados, los iteradores abstraen el origen de los datos: misma sintaxis para ArrayList, LinkedList, Set o un cursor JDBC envuelto.
Frente a Streams, los iteradores son síncronos y de un solo uso, pero más simples para flujos donde no se necesita paralelismo ni operaciones intermedias. Para recorridos planos con efecto lateral mínimo, for-each es más legible que stream().forEach().
Frente a Reactor / RxJava, los iteradores son pull (el consumidor pide), mientras que los Flux/Observable son push (la fuente empuja). Para back-pressure y eventos asíncronos, los reactivos son la opción correcta; para recorrer datos en memoria o ficheros locales, el iterador es más simple y más rápido.
Documentación oficial
La especificación está en la Javadoc de java.lang.Iterable, java.util.Iterator, java.util.ListIterator y java.util.Spliterator para Java 21+. La JLS §14.14.2 detalla la traducción del for-each al bucle clásico. El libro "Effective Java" 3.ª edición, ítem 58, recomienda for-each salvo casos específicos.
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 útiles 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.