Iterando colecciones en Java

Cada vez que tenga una colección de cosas, necesitará algún mecanismo para recorrer sistemáticamente los elementos de esa colección. Como ejemplo cotidiano, considere el control remoto de la televisión, que nos permite recorrer varios canales de televisión. De manera similar, en el mundo de la programación, necesitamos un mecanismo para iterar sistemáticamente a través de una colección de objetos de software. Java incluye varios mecanismos de iteración, incluido el índice (para iterar sobre una matriz), el cursor (para iterar sobre los resultados de una consulta de base de datos), la enumeración (en las primeras versiones de Java) y el iterador (en las versiones más recientes de Java).

El patrón de iterador

Un iterador es un mecanismo que permite acceder secuencialmente a todos los elementos de una colección, realizando alguna operación en cada elemento. Esencialmente, un iterador proporciona un medio de "bucle" sobre una colección encapsulada de objetos. Ejemplos de uso de iteradores incluyen

  • Visite cada archivo en un directorio ( también conocido como carpeta) y muestre su nombre.
  • Visite cada nodo en un gráfico y determine si es accesible desde un nodo determinado.
  • Visite a cada cliente en una cola (por ejemplo, simulando una fila en un banco) y averigüe cuánto tiempo ha estado esperando.
  • Visite cada nodo en el árbol de sintaxis abstracta de un compilador (que es producido por el analizador) y realice la verificación semántica o la generación de código. (También puede utilizar el patrón Visitante en este contexto).

Ciertos principios son válidos para el uso de iteradores: en general, debería poder tener múltiples recorridos en progreso al mismo tiempo; es decir, un iterador debe permitir el concepto de bucle anidado. Un iterador también debería ser no destructivo en el sentido de que el acto de iteración no debería, por sí mismo, cambiar la colección. Por supuesto, la operación que se realiza en los elementos de una colección posiblemente podría cambiar algunos de los elementos. También es posible que un iterador admita la eliminación de un elemento de una colección o la inserción de un nuevo elemento en un punto particular de la colección, pero tales cambios deben ser explícitos dentro del programa y no un subproducto de la iteración. En algunos casos, también necesitará tener iteradores con diferentes métodos de recorrido; por ejemplo, recorrido por preorden y posorden de un árbol, o recorrido en profundidad y primero en anchura de un gráfico.

Iterando estructuras de datos complejas

Primero aprendí a programar en una versión anterior de FORTRAN, donde la única capacidad de estructuración de datos era una matriz. Aprendí rápidamente cómo iterar sobre una matriz usando un índice y un bucle DO. A partir de ahí, fue solo un pequeño salto mental a la idea de usar un índice común en múltiples matrices para simular una matriz de registros. La mayoría de los lenguajes de programación tienen características similares a las matrices y admiten un bucle directo sobre matrices. Pero los lenguajes de programación modernos también admiten estructuras de datos más complejas como listas, conjuntos, mapas y árboles, donde las capacidades están disponibles a través de métodos públicos, pero los detalles internos están ocultos en partes privadas de la clase. Los programadores deben poder atravesar los elementos de estas estructuras de datos sin exponer su estructura interna, que es el propósito de los iteradores.

Iteradores y patrones de diseño de la banda de los cuatro

De acuerdo con Gang of Four (ver más abajo), el patrón de diseño de Iterator es un patrón de comportamiento, cuya idea clave es "tomar la responsabilidad del acceso y el recorrido fuera del objeto de la lista [ ed. Think collection ] y ponerlo en un iterador objeto." Este artículo no trata tanto sobre el patrón Iterator como sobre cómo se utilizan los iteradores en la práctica. Cubrir completamente el patrón requeriría discutir cómo se diseñaría un iterador, los participantes (objetos y clases) en el diseño, los posibles diseños alternativos y las compensaciones de diferentes alternativas de diseño. Prefiero concentrarme en cómo se usan los iteradores en la práctica, pero le señalaré algunos recursos para investigar el patrón de iterador y los patrones de diseño en general:

  • Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley Professional, 1994) escrito por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (también conocido como Gang of Four o simplemente GoF) es el recurso definitivo para el aprendizaje sobre patrones de diseño. Aunque el libro se publicó por primera vez en 1994, sigue siendo un clásico, como lo demuestra el hecho de que ha habido más de 40 ediciones.
  • Bob Tarr, profesor de la Universidad de Maryland en el condado de Baltimore, tiene un excelente conjunto de diapositivas para su curso sobre patrones de diseño, incluida su introducción al patrón Iterator.
  • Los patrones de diseño Java de la serie JavaWorld de David Geary presentan muchos de los patrones de diseño Gang of Four, incluidos los patrones Singleton, Observer y Composite. También en JavaWorld, la descripción general de patrones de diseño más reciente de Jeff Friesen en tres partes incluye una guía para los patrones de GoF.

Iteradores activos vs iteradores pasivos

Hay dos enfoques generales para implementar un iterador dependiendo de quién controla la iteración. Para un iterador activo (también conocido como iterador explícito o iterador externo ), el cliente controla la iteración en el sentido de que crea el iterador, le dice cuándo avanzar al siguiente elemento, prueba para ver si cada elemento ha sido visitado, y así. Este enfoque es común en lenguajes como C ++, y es el enfoque que recibe más atención en el libro de GoF. Aunque los iteradores en Java han tomado diferentes formas, el uso de un iterador activo era esencialmente la única opción viable antes de Java 8.

Para un iterador pasivo (también conocido como iterador implícito , iterador interno o iterador de devolución de llamada ), el iterador mismo controla la iteración. Básicamente, el cliente le dice al iterador, "realice esta operación en los elementos de la colección". Este enfoque es común en lenguajes como LISP que proporcionan funciones o cierres anónimos. Con el lanzamiento de Java 8, este enfoque de iteración es ahora una alternativa razonable para los programadores de Java.

Esquemas de nombres de Java 8

Aunque no es tan malo como Windows (NT, 2000, XP, VISTA, 7, 8, ...), el historial de versiones de Java incluye varios esquemas de nombres. Para empezar, ¿deberíamos referirnos a la edición estándar de Java como "JDK", "J2SE" o "Java SE"? Los números de versión de Java comenzaron bastante sencillos (1.0, 1.1, etc.), pero todo cambió con la versión 1.5, que tenía la marca Java (o JDK) 5. Cuando me refiero a las primeras versiones de Java, utilizo frases como "Java 1.0" o "Java 1.1 ", pero después de la quinta versión de Java utilizo frases como" Java 5 "o" Java 8. "

Para ilustrar los diversos enfoques de la iteración en Java, necesito un ejemplo de una colección y algo que se debe hacer con sus elementos. Para la parte inicial de este artículo, usaré una colección de cadenas que representan nombres de cosas. Para cada nombre de la colección, simplemente imprimiré su valor en la salida estándar. Estas ideas básicas se extienden fácilmente a colecciones de objetos más complicados (como empleados), y donde el procesamiento de cada objeto es un poco más complicado (como darle a cada empleado altamente calificado un aumento del 4.5 por ciento).

Otras formas de iteración en Java 8

Me estoy enfocando en iterar sobre colecciones, pero hay otras formas más especializadas de iteración en Java. Por ejemplo, puede usar un JDBC ResultSetpara iterar sobre las filas devueltas de una consulta SELECT a una base de datos relacional, o usar un Scannerpara iterar sobre una fuente de entrada.

Iteración con la clase Enumeration

En Java 1.0 y 1.1, las dos clases de colección principales eran Vectory Hashtable, y el patrón de diseño de Iterator se implementó en una clase llamada Enumeration. En retrospectiva, este era un mal nombre para la clase. No confunda la clase Enumerationcon el concepto de tipos de enumeración , que no apareció hasta Java 5. Hoy en día ambos Vectory Hashtableson clases genéricas, pero en ese entonces los genéricos no formaban parte del lenguaje Java. El código para procesar un vector de cadenas Enumerationse parecería al Listado 1.

Listado 1. Uso de enumeración para iterar sobre un vector de cadenas

 Vector names = new Vector(); // ... add some names to the collection Enumeration e = names.elements(); while (e.hasMoreElements()) { String name = (String) e.nextElement(); System.out.println(name); } 

Iteración con la clase Iterator

Java 1.2 introduced the collection classes that we all know and love, and the Iterator design pattern was implemented in a class appropriately named Iterator. Because we didn't yet have generics in Java 1.2, casting an object returned from an Iterator was still necessary. For Java versions 1.2 through 1.4, iterating over a list of strings might resemble Listing 2.

Listing 2. Using an Iterator to iterate over a list of strings

 List names = new LinkedList(); // ... add some names to the collection Iterator i = names.iterator(); while (i.hasNext()) { String name = (String) i.next(); System.out.println(name); } 

Iteration with generics and the enhanced for-loop

Java 5 gave us generics, the interface Iterable, and the enhanced for-loop. The enhanced for-loop is one of my all-time-favorite small additions to Java. The creation of the iterator and calls to its hasNext() and next() methods are not expressed explicitly in the code, but they still take place behind the scenes. Thus, even though the code is more compact, we are still using an active iterator. Using Java 5, our example would look something like what you see in Listing 3.

Listing 3. Using generics and the enhanced for-loop to iterate over a list of strings

 List names = new LinkedList(); // ... add some names to the collection for (String name : names) System.out.println(name); 

Java 7 gave us the diamond operator, which reduces the verbosity of generics. Gone were the days of having to repeat the type used to instantiate the generic class after invoking the new operator! In Java 7 we could simplify the first line in Listing 3 above to the following:

 List names = new LinkedList(); 

A mild rant against generics

The design of a programming language involves tradeoffs between the benefits of language features versus the complexity they impose on the syntax and semantics of the language. For generics, I am not convinced that the benefits outweigh the complexity. Generics solved a problem that I did not have with Java. I generally agree with Ken Arnold's opinion when he states: "Generics are a mistake. This is not a problem based on technical disagreements. It's a fundamental language design problem [...] The complexity of Java has been turbocharged to what seems to me relatively small benefit."

Fortunately, while designing and implementing generic classes can sometimes be overly complicated, I have found that using generic classes in practice is usually straightforward.

Iteration with the forEach() method

Before delving into Java 8 iteration features, let's reflect on what's wrong with the code shown in the previous listings–which is, well, nothing really. There are millions of lines of Java code in currently deployed applications that use active iterators similar to those shown in my listings. Java 8 simply provides additional capabilities and new ways of performing iteration. For some scenarios, the new ways can be better.

The major new features in Java 8 center on lambda expressions, along with related features such as streams, method references, and functional interfaces. These new features in Java 8 allow us to seriously consider using passive iterators instead of the more conventional active iterators. In particular, the Iterable interface provides a passive iterator in the form of a default method called forEach().

A default method, another new feature in Java 8, is a method in an interface with a default implementation. In this case, the forEach() method is actually implemented using an active iterator in a manner similar to what you saw in Listing 3.

Collection classes that implement Iterable (for example, all list and set classes) now have a forEach() method. This method takes a single parameter that is a functional interface. Therefore the actual parameter passed to the forEach() method is a candidate for a lambda expression. Using the features of Java 8, our running example would evolve to the form shown in Listing 4.

Listing 4. Iteration in Java 8 using the forEach() method

 List names = new LinkedList(); // ... add some names to the collection names.forEach(name -> System.out.println(name)); 

Note the difference between the passive iterator in Listing 4 and the active iterator in the previous three listings. In the first three listings, the loop structure controls the iteration, and during each pass through the loop, an object is retrieved from the list and then printed. In Listing 4, there is no explicit loop. We simply tell the forEach() method what to do with the objects in the list — in this case we simply print the object. Control of the iteration resides within the forEach() method.

Iteration with Java streams

Ahora consideremos hacer algo un poco más complicado que simplemente imprimir los nombres en nuestra lista. Supongamos, por ejemplo, que queremos contar el número de nombres que comienzan con la letra A . Podríamos implementar la lógica más complicada como parte de la expresión lambda, o podríamos usar la nueva API Stream de Java 8. Tomemos el último enfoque.