Interfaz iterable de Java: Iterator, ListIterator y Spliterator

    Introducción

    Si bien podemos usar un for o while bucle para atravesar una colección de elementos, un Iterator nos permite hacerlo sin preocuparnos por las posiciones del índice e incluso nos permite no solo revisar una colección, sino también alterarla al mismo tiempo, lo que no siempre es posible con for bucles si está eliminando elementos en el bucle, por ejemplo.

    Combine eso con la capacidad de implementar nuestro Iterador personalizado para iterar a través de objetos mucho más complejos, así como avanzar y retroceder, y las ventajas de saber cómo usarlo se vuelven bastante claras.

    Este artículo profundizará bastante en cómo Iterator y Iterable Se pueden utilizar interfaces.

    Iterador ()

    los Iterator La interfaz se utiliza para iterar sobre los elementos de una colección (List, Seto Map). Se utiliza para recuperar los elementos uno por uno y realizar operaciones sobre cada uno si es necesario.

    Estos son los métodos que se utilizan para recorrer colecciones y realizar operaciones:

    • .hasNext(): Devoluciones true si no hemos llegado al final de una colección, devuelve false de otra manera
    • .next(): Devuelve el siguiente elemento de una colección.
    • .remove(): Elimina el último elemento devuelto por el iterador de la colección
    • .forEachRemaining(): Realiza la acción dada para cada elemento restante en una colección, en orden secuencial

    En primer lugar, dado que los iteradores están diseñados para usarse con colecciones, hagamos un simple ArrayList con algunos elementos:

    List<String> avengers = new ArrayList<>();
    
    // Now lets add some Avengers to the list
    avengers.add("Ant-Man");
    avengers.add("Black Widow");
    avengers.add("Captain America");
    avengers.add("Doctor Strange");
    

    Podemos iterar a través de esta lista usando un ciclo simple:

    System.out.println("Simple loop example:n");
    for (int i = 0; i < avengers.size(); i++) {
        System.out.println(avengers.get(i));
    }
    

    Sin embargo, queremos explorar los iteradores:

    Te puede interesar:Métodos de objetos de Java: hashCode ()
    System.out.println("nIterator Example:n");
    
    // First we make an Iterator by calling 
    // the .iterator() method on the collection
    Iterator<String> avengersIterator = avengers.iterator();
    
    // And now we use .hasNext() and .next() to go through it
    while (avengersIterator.hasNext()) {
        System.out.println(avengersIterator.next());
    }
    

    ¿Qué sucede si queremos eliminar un elemento de este ArrayList? Intentemos hacerlo usando el for lazo:

    System.out.println("Simple loop example:n");
    for (int i = 0; i < avengers.size(); i++) {
        if (avengers.get(i).equals("Doctor Strange")) {
            avengers.remove(i);
        }
        System.out.println(avengers.get(i));
    }
    

    Seríamos recibidos con un desagradable IndexOutOfBoundsException:

    Simple loop example:
    
    Ant-Man
    Black Widow
    Captain America
    Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
    

    Esto tiene sentido ya que estamos modificando el tamaño de la colección a medida que la recorremos. Lo mismo ocurre con los avanzados for lazo:

    System.out.println("Simple loop example:n");
    for (String avenger : avengers) {
        if (avenger.equals("Doctor Strange")) {
            avengers.remove(avenger);
        }
        System.out.println(avenger);
    }
    

    Nuevamente nos saludan con otra excepción:

    Simple loop example:
    
    Ant-Man
    Black Widow
    Captain America
    Doctor Strange
    Exception in thread "main" java.util.ConcurrentModificationException
    

    Aquí es donde los iteradores son útiles, actuando como intermediarios para eliminar el elemento de la colección, pero también para garantizar que el recorrido continúe según lo planeado:

    Iterator<String> avengersIterator = avengers.iterator();
    while (avengersIterator.hasNext()) {
        String avenger = avengersIterator.next();
    
        // First we must find the element we wish to remove
        if (avenger.equals("Ant-Man")) {
            // This will remove "Ant-Man" from the original
            // collection, in this case a List
            avengersIterator.remove();
        }
    }
    

    Este es un método seguro garantizado para eliminar elementos mientras atraviesa colecciones.

    Y para validar si el artículo ha sido eliminado:

    // We can also use the helper method .forEachRemaining()
    System.out.println("For Each Remaining Example:n");
    Iterator<String> avengersIteratorForEach = avengers.iterator();
    
    // This will apply System.out::println to all elements in the collection
    avengersIteratorForEach.forEachRemaining(System.out::println);     
    

    Y la salida es:

    Te puede interesar:Control de flujo de Java: la declaración del cambio
    For Each Remaining Example:
    
    Black Widow
    Captain America
    Doctor Strange
    

    Como puede ver, «Ant-Man» se ha eliminado del avengers lista.

    ListIterator ()

    ListIterator extiende el Iterator interfaz. Solo se usa en Listsy puede iterar bidireccionalmente, lo que significa que puede iterar de adelante hacia atrás o de atrás hacia adelante. Tampoco tiene un elemento actual porque el cursor siempre se coloca entre 2 elementos en un List, entonces debemos usar .previous() o .next() para acceder a un elemento.

    ¿Cuál es la diferencia entre un Iterator y un ListIterator?

    Primero el Iterator se puede aplicar a cualquier colección – Lists, Maps, Queues, Sets, etc.

    los ListIterator solo se puede aplicar a listas. Al agregar esta restricción, el ListIterator puede ser mucho más específico cuando se trata de métodos, por lo que nos presentan muchos métodos nuevos que nos ayudan a modificar listas mientras recorremos.

    Si se trata de un List implementación (ArrayList, LinkedList, etc.), siempre es preferible utilizar el ListIterator.

    Estos son los métodos que probablemente utilizará:

    • .add(E e): Inserta el elemento en la lista.
    • .remove(): Elimina el último elemento devuelto por .next() o .previous() de la lista.
    • .set(E e): Reemplaza el último elemento devuelto por .next() o .previous() con el elemento especificado
    • .hasNext(): Devoluciones true si no hemos llegado al final de una lista, devuelve false de otra manera.
    • .next(): Devuelve el siguiente elemento de una lista.
    • .nextIndex(): Devuelve el índice del siguiente elemento.
    • .hasPrevious(): Devoluciones true si no hemos llegado al principio de una lista, devuelve false de otra manera.
    • .previous(): Devuelve el elemento anterior en una lista.
    • .previousIndex(): Devuelve el índice del elemento anterior.

    Nuevamente, poblaremos un ArrayList con algunos elementos:

    Te puede interesar:Control de flujo de Java: declaraciones while y do-while
    ArrayList<String> defenders = new ArrayList<>();
    
    defenders.add("Daredevil");
    defenders.add("Luke Cage");
    defenders.add("Jessica Jones");
    defenders.add("Iron Fist");
    

    Usemos un ListIterator para recorrer una lista e imprimir los elementos:

    ListIterator listIterator = defenders.listIterator(); 
      
    System.out.println("Original contents of our List:n");
    while (listIterator.hasNext()) 
        System.out.print(listIterator.next() + System.lineSeparator()); 
    

    Obviamente, funciona de la misma manera que el clásico. Iterator. La salida es:

    Original contents of our List: 
    
    Daredevil
    Luke Cage
    Jessica Jones
    Iron Fist
    

    Ahora, intentemos modificar algunos elementos:

    System.out.println("Modified contents of our List:n");
    
    // Now let's make a ListIterator and modify the elements
    ListIterator defendersListIterator = defenders.listIterator();
    
    while (defendersListIterator.hasNext()) {
        Object element = defendersListIterator.next();
        defendersListIterator.set("The Mighty Defender: " + element);
    }
    

    Imprimir la lista ahora produciría:

    Modified contents of our List:
    
    The Mighty Defender: Daredevil
    The Mighty Defender: Luke Cage
    The Mighty Defender: Jessica Jones
    The Mighty Defender: Iron Fist
    

    Ahora, sigamos adelante y recorramos la lista hacia atrás, como algo que podemos hacer con el ListIterator:

    System.out.println("Modified List backwards:n");
    while (defendersListIterator.hasPrevious()) {
        System.out.println(defendersListIterator.previous());
    }
    

    Y la salida es:

    Modified List backwards:
    
    The Mighty Defender: Iron Fist
    The Mighty Defender: Jessica Jones
    The Mighty Defender: Luke Cage
    The Mighty Defender: Daredevil
    

    Separador ()

    los Spliterator La interfaz es funcionalmente igual que una Iterator. Puede que nunca necesite usar Spliterator directamente, pero repasemos algunos casos de uso.

    Sin embargo, primero debe familiarizarse un poco con Java Streams y Lambda Expressions en Java.

    Te puede interesar:Control de flujo de Java: declaraciones if y if-else

    Si bien enumeraremos todos los métodos Spliterator tiene, el funcionamiento completo de la Spliterator interfaz están fuera del alcance de este artículo. Una cosa que cubriremos con un ejemplo es cómo Spliterator puede utilizar la paralelización para atravesar un Stream que podemos romper.

    Los métodos que usaremos al tratar con Spliterator son:

    • .characteristics(): Devuelve las características que tiene este Spliterator como int valor. Éstas incluyen:
      • ORDERED
      • DISTINCT
      • SORTED
      • SIZED
      • CONCURRENT
      • IMMUTABLE
      • NONNULL
      • SUBSIZED
    • .estimateSize(): Devuelve una estimación del número de elementos que se encontrarían con un recorrido como un long valor o devoluciones long.MAX_VALUE si no puede calcular.
    • .forEachRemaining(E e): Realiza la acción dada para cada elemento restante en una colección, en orden secuencial.
    • .getComparator(): Si esto SpliteratorLa fuente está ordenada por Comparator, devuelve eso Comparator.
    • .getExactSizeIfKnown(): Devoluciones .estimateSize() si se conoce el tamaño, de lo contrario devuelve -1
    • .hasCharacteristics(int characteristics): Devoluciones true si esto Spliteratores .characteristics() contienen todas las características dadas.
    • .tryAdvance(E e): Si existe un elemento restante, realiza la acción dada en él, devolviendo true, si no vuelve false.
    • .trySplit(): Si esto Spliterator se puede particionar, devuelve un Spliterator elementos de cobertura, que, al regresar de este método, no estarán cubiertos por este Spliterator.

    Como de costumbre, comencemos con un simple ArrayList:

    List<String> mutants = new ArrayList<>();
    
    mutants.add("Professor X");
    mutants.add("Magneto");
    mutants.add("Storm");
    mutants.add("Jean Grey");
    mutants.add("Wolverine");
    mutants.add("Mystique");
    

    Ahora, necesitamos aplicar el Spliterator a un Stream. Afortunadamente, es fácil convertir entre un ArrayList y un Stream debido al marco de Cobranzas:

    // Obtain a Stream to the mutants List.
    Stream<String> mutantStream = mutants.stream();
    
    // Getting Spliterator object on mutantStream.
    Spliterator<String> mutantList = mutantStream.spliterator();
    

    Y para mostrar algunos de estos métodos, ejecutemos cada uno:

    // .estimateSize() method
    System.out.println("Estimate size: " + mutantList.estimateSize());
    
    // .getExactSizeIfKnown() method
    System.out.println("nExact size: " + mutantList.getExactSizeIfKnown());
    
    System.out.println("nContent of List:");
    // .forEachRemaining() method
    mutantList.forEachRemaining((n) -> System.out.println(n));
    
    // Obtaining another Stream to the mutant List.
    Spliterator<String> splitList1 = mutantStream.spliterator();
    
    // .trySplit() method
    Spliterator<String> splitList2 = splitList1.trySplit();
    
    // If splitList1 could be split, use splitList2 first.
    if (splitList2 != null) {
        System.out.println("nOutput from splitList2:");
        splitList2.forEachRemaining((n) -> System.out.println(n));
    }
    
    // Now, use the splitList1
    System.out.println("nOutput from splitList1:");
    splitList1.forEachRemaining((n) -> System.out.println(n));
    

    Y obtenemos esto como salida:

    Estimate size: 6
    
    Exact size: 6
    
    Content of List: 
    Professor X
    Magneto
    Storm
    Jean Grey
    Wolverine
    Mystique
    
    Output from splitList2: 
    Professor X
    Magneto
    Storm
    
    Output from splitList1: 
    Jean Grey
    Wolverine
    Mystique
    

    Iterable ()

    ¿Y si por alguna razón quisiéramos hacer un personalizado? Iterator interfaz. Lo primero que debe conocer es este gráfico:

    Para hacer nuestra costumbre Iterator necesitaríamos escribir métodos personalizados para .hasNext(), .next()y .remove() .

    Te puede interesar:Control de flujo de Java: bucles para y para cada

    Dentro de Iterable interfaz, tenemos un método que devuelve un iterador para elementos en una colección, que es el .iterator() método, y un método que realiza una acción para cada elemento en un iterador, el .forEach() método.

    Por ejemplo, imaginemos que somos Tony Stark, y necesitamos escribir un iterador personalizado para enumerar todos los trajes de Iron Man que tienes actualmente en tu arsenal.

    Primero, creemos una clase para obtener y configurar los datos del traje:

    public class Suit {
    
        private String codename;
        private int mark;
    
        public Suit(String codename, int mark) {
            this.codename = codename;
            this.mark = mark;
        }
    
        public String getCodename() { return codename; }
    
        public int getMark() { return mark; }
    
        public void setCodename (String codename) {this.codename=codename;}
    
        public void setMark (int mark) {this.mark=mark;}
    
        public String toString() {
            return "mark: " + mark + ", codename: " + codename;
        }
    }
    

    A continuación, escribamos nuestro iterador personalizado:

    // Our custom Iterator must implement the Iterable interface
    public class Armoury implements Iterable<Suit> {
        
        // Notice that we are using our own class as a data type
        private List<Suit> list = null;
    
        public Armoury() {
            // Fill the List with data
            list = new LinkedList<Suit>();
            list.add(new Suit("HOTROD", 22));
            list.add(new Suit("SILVER CENTURION", 33));
            list.add(new Suit("SOUTHPAW", 34));
            list.add(new Suit("HULKBUSTER 2.0", 48));
        }
        
        public Iterator<Suit> iterator() {
            return new CustomIterator<Suit>(list);
        }
    
        // Here we are writing our custom Iterator
        // Notice the generic class E since we do not need to specify an exact class
        public class CustomIterator<E> implements Iterator<E> {
        
            // We need an index to know if we have reached the end of the collection
            int indexPosition = 0;
            
            // We will iterate through the collection as a List
            List<E> internalList;
            public CustomIterator(List<E> internalList) {
                this.internalList = internalList;
            }
    
            // Since java indexes elements from 0, we need to check against indexPosition +1
            // to see if we have reached the end of the collection
            public boolean hasNext() {
                if (internalList.size() >= indexPosition +1) {
                    return true;
                }
                return false;
            }
    
            // This is our custom .next() method
            public E next() {
                E val = internalList.get(indexPosition);
    
                // If for example, we were to put here "indexPosition +=2" we would skip every 
                // second element in a collection. This is a simple example but we could
                // write very complex code here to filter precisely which elements are
                // returned. 
                // Something which would be much more tedious to do with a for or while loop
                indexPosition += 1;
                return val;
            }
            // In this example we do not need a .remove() method, but it can also be 
            // written if required
        }
    }
    

    Y finalmente la clase principal:

    public class IronMan {
    
        public static void main(String[] args) {
    
            Armoury armoury = new Armoury();
    
            // Instead of manually writing .hasNext() and .next() methods to iterate through 
            // our collection we can simply use the advanced forloop
            for (Suit s : armoury) {
                System.out.println(s);
            }
        }
    }
    

    La salida es:

    mark: 22, codename: HOTROD
    mark: 33, codename: SILVER CENTURION
    mark: 34, codename: SOUTHPAW
    mark: 48, codename: HULKBUSTER 2.0
    

    Conclusión

    En este artículo, cubrimos en detalle cómo trabajar con iteradores en Java e incluso escribimos uno personalizado para explorar todas las nuevas posibilidades del Iterable interfaz.

    También mencionamos cómo Java aprovecha la paralelización de flujos para optimizar internamente el recorrido a través de una colección utilizando el Spliterator interfaz.

    Te puede interesar:Control de flujo de Java: romper y continuar declaraciones

     

    Rate this post

    Etiquetas: