Introducción: ¿por qué for-each se convirtió en el "rey" de los bucles en Java?
\n\nSi has escrito en Java al menos una semana, seguro que te has encontrado con la construcción for (Type var : collection). Esta sintaxis concisa se llama bucle for-each (o enhanced for loop). Apareció en Java 5 (2004) y rápidamente se convirtió en la favorita de los desarrolladores.
Pero, ¿sabes qué sucede bajo el capó cuando la JVM encuentra esta construcción? ¿Por qué for-each puede ser más lento que el for normal para arrays? ¿Y por qué a veces lanza ConcurrentModificationException, incluso si no estás eliminando nada?
En este artículo analizaremos cómo funciona for-each en Java a nivel de bytecode, lo compararemos con los bucles clásicos y aprenderemos a evitar las trampas típicas. ¡Allá vamos!
\n\nSintaxis y primer contacto
\n\nLa sintaxis básica es la siguiente:
\n\nfor (TipoElemento variable : colección_o_array) {\n // cuerpo del bucle\n}\n\nEjemplo con ArrayList:
List<String> names = Arrays.asList("Ana", "Boris", "Víctor");\n\nfor (String name : names) {\n System.out.println("¡Hola, " + name + "!");\n}\n\nSalida:
\n\n¡Hola, Ana!\n¡Hola, Boris!\n¡Hola, Víctor!\n\n¿Sencillo? Sí. Pero detrás de esta simplicidad se esconden dos mecanismos completamente diferentes: uno para arrays y otro para colecciones.
\n\n¿Cómo funciona for-each con arrays?
\n\nCuando escribes for-each para un array, el compilador de Java lo convierte en un for normal con índice. Sin magia.
Tu código:
\n\nint[] numbers = {10, 20, 30};\n\nfor (int num : numbers) {\n System.out.println(num);\n}\n\nDespués de la compilación (descompila el bytecode para comprobarlo):
\n\nint[] numbers = new int[]{10, 20, 30};\nfor (int i = 0; i < numbers.length; i++) {\n int num = numbers[i];\n System.out.println(num);\n}\n\nConclusión importante: para arrays, for-each y el for normal son lo mismo en cuanto a rendimiento. Sin gastos generales de iteradores.
\n\n¿Cómo funciona for-each con colecciones (List, Set)?
\n\nAquí es donde se pone interesante. Para las colecciones, for-each utiliza un iterador.
\n\nTu código:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (String lang : list) {\n System.out.println(lang);\n}\n\nEl compilador lo transforma en:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (Iterator<String> it = list.iterator(); it.hasNext(); ) {\n String lang = it.next();\n System.out.println(lang);\n}\n\nEs decir:
\n- \n
- Se llama a
list.iterator()— obtenemos un iterador. \n - En la condición —
it.hasNext(). \n - En el cuerpo —
it.next(). \n - La variable
langse inicializa con el resultado denext(). \n
¿Qué significa esto en la práctica?
\n- \n
- No se pueden eliminar elementos dentro de for-each mediante
list.remove()— obtendrás unConcurrentModificationException. Usait.remove()o un for normal. \n - No se puede modificar la colección (añadir/eliminar) durante la iteración — el mismo error. \n
- Rendimiento: para
ArrayList, la diferencia con el for normal es mínima (creación del objeto iterador). ParaLinkedList, for-each es mucho más rápido quefor (int i = 0; i < list.size(); i++), porqueget(i)en LinkedList es O(n) cada vez. \n
¿Y qué pasa con Map? Iteración sobre pares clave-valor
\n\nMap no implementa Iterable directamente, por lo que for-each