¿Hay una manera concisa de iterar sobre una secuencia con índices en Java 8?


382

¿Hay una manera concisa de iterar sobre una secuencia mientras se tiene acceso al índice en la secuencia?

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = intRange(1, names.length).boxed();
nameList = zip(indices, stream(names), SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey())
        .map(Entry::getValue)
        .collect(toList());

lo que parece bastante decepcionante en comparación con el ejemplo LINQ dado allí

string[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
var nameList = names.Where((c, index) => c.Length <= index + 1).ToList();

¿Hay una manera más concisa?

Además, parece que la cremallera se ha movido o se ha eliminado ...


2
¿Qué es intRange()? No he encontrado este método en Java 8 hasta ahora.
Rohit Jain

@RohitJain Probablemente un IntStream.rangeClosed(x, y).
Assylias

2
Como comentario List<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
adicional

3
Sí, zipse eliminó, junto con secuencias experimentales de dos valores denominadas de forma diferente BiStreamo MapStream. El principal problema es que para hacer esto efectivamente, Java realmente necesita un tipo de par (o tupla) estructuralmente tipado. A falta de uno, es fácil crear una clase genérica de pares o tuplas, se ha hecho muchas veces, pero todas se borran del mismo tipo.
Stuart Marks

3
Otro problema con una clase genérica Pair o Tuple es que requiere que todas las primitivas estén encuadradas.
Stuart Marks

Respuestas:


435

La forma más limpia es comenzar desde una secuencia de índices:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
IntStream.range(0, names.length)
         .filter(i -> names[i].length() <= i)
         .mapToObj(i -> names[i])
         .collect(Collectors.toList());

La lista resultante contiene solo "Erik".


Una alternativa que parece más familiar cuando está acostumbrado a los bucles sería mantener un contador ad hoc utilizando un objeto mutable, por ejemplo un AtomicInteger:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
AtomicInteger index = new AtomicInteger();
List<String> list = Arrays.stream(names)
                          .filter(n -> n.length() <= index.incrementAndGet())
                          .collect(Collectors.toList());

Tenga en cuenta que el uso del último método en una secuencia paralela podría romperse ya que los elementos no se procesarían necesariamente "en orden" .


28
Usar atómica de esta manera es problemático con corrientes paralelas. Primero, el orden de procesamiento de los elementos no será necesariamente el mismo que el orden en que los elementos ocurren en la matriz inicial. Por lo tanto, el "índice" asignado usando el atómico probablemente no coincidirá con el índice de matriz real. En segundo lugar, si bien los atómicos son seguros para subprocesos, es posible que haya conflictos entre múltiples subprocesos que actualicen el atómico, degradando la cantidad de paralelismo.
Stuart Marks

1
Desarrollé una solución similar a la de @assylias. Para sortear la problemática con el flujo paralelo que se menciona en @StuartMarks, primero hago un flujo secuencial dado secuencial, realizo el mapeo y restauro el estado paralelo. public static <T> Stream<Tuple2<Integer, T>> zipWithIndex(Stream<T> stream) { final AtomicInteger index = new AtomicInteger(); final Function<T, Tuple2<Integer, T>> zipper = e -> Tuples.of(index.getAndIncrement(), e); if (stream.isParallel()) { return stream.sequential().map(zipper).parallel(); } else { return stream.map(zipper); } }
Daniel Dietrich

44
@DanielDietrich Si cree que resuelve la pregunta, debe publicarla como respuesta en lugar de como comentario (¡y el código también será más legible!).
Assylias

3
@DanielDietrich Lo siento, si estoy leyendo ese código correctamente, no funcionará. No puede tener diferentes segmentos de una tubería ejecutándose en paralelo versus secuencial. Solo se respeta el último parallelo el último sequentialcuando comienza la operación del terminal.
Stuart Marks

44
Por el bien de la justicia, "la forma más limpia" fue robada de la respuesta de @ Stuart.
Vadzim

70

La API de secuencias de Java 8 carece de las características para obtener el índice de un elemento de secuencia, así como la capacidad de agrupar secuencias. Esto es lamentable, ya que hace que ciertas aplicaciones (como los desafíos de LINQ) sean más difíciles de lo que serían de otra manera.

Sin embargo, a menudo hay soluciones alternativas. Por lo general, esto se puede hacer "conduciendo" la secuencia con un rango entero y aprovechando el hecho de que los elementos originales a menudo están en una matriz o en una colección accesible por índice. Por ejemplo, el problema del Desafío 2 se puede resolver de esta manera:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList =
    IntStream.range(0, names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(toList());

Como mencioné anteriormente, esto aprovecha el hecho de que la fuente de datos (la matriz de nombres) es directamente indexable. Si no fuera así, esta técnica no funcionaría.

Admito que esto no satisface la intención del Desafío 2. Sin embargo, resuelve el problema de manera razonablemente efectiva.

EDITAR

Mi ejemplo de código anterior solía flatMapfusionar las operaciones de filtro y mapa, pero esto era engorroso y no proporcionaba ninguna ventaja. He actualizado el ejemplo según el comentario de Holger.


77
¿Qué tal IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Funciona sin boxeo ...
Holger

1
Hm, sí, ¿por qué pensé que necesitaba usar de flatMaptodos modos?
Stuart Marks

2
Finalmente revisando esto ... probablemente lo utilicé flatMapporque fusiona una operación de filtrado y mapeo en una sola operación, pero esto realmente no proporciona ninguna ventaja. Editaré el ejemplo.
Stuart Marks

Stream.of (Array) creará una interfaz de transmisión para una matriz. Efectivamente convirtiéndolo en Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );Menos unboxing y menos asignación de memoria; ya que ya no estamos creando una secuencia de rango.
Código Eyez

44

Desde guayaba 21, puedes usar

Streams.mapWithIndex()

Ejemplo (del documento oficial ):

Streams.mapWithIndex(
    Stream.of("a", "b", "c"),
    (str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")

3
Además, la gente de Guava aún no ha implementado forEachWithIndex (tomando un consumidor en lugar de una función), pero es un problema asignado: github.com/google/guava/issues/2913 .
John Glassmyer

25

He usado la siguiente solución en mi proyecto. Creo que es mejor que usar objetos mutables o rangos de enteros.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Objects.requireNonNull;


public class CollectionUtils {
    private CollectionUtils() { }

    /**
     * Converts an {@link java.util.Iterator} to {@link java.util.stream.Stream}.
     */
    public static <T> Stream<T> iterate(Iterator<? extends T> iterator) {
        int characteristics = Spliterator.ORDERED | Spliterator.IMMUTABLE;
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, characteristics), false);
    }

    /**
     * Zips the specified stream with its indices.
     */
    public static <T> Stream<Map.Entry<Integer, T>> zipWithIndex(Stream<? extends T> stream) {
        return iterate(new Iterator<Map.Entry<Integer, T>>() {
            private final Iterator<? extends T> streamIterator = stream.iterator();
            private int index = 0;

            @Override
            public boolean hasNext() {
                return streamIterator.hasNext();
            }

            @Override
            public Map.Entry<Integer, T> next() {
                return new AbstractMap.SimpleImmutableEntry<>(index++, streamIterator.next());
            }
        });
    }

    /**
     * Returns a stream consisting of the results of applying the given two-arguments function to the elements of this stream.
     * The first argument of the function is the element index and the second one - the element value. 
     */
    public static <T, R> Stream<R> mapWithIndex(Stream<? extends T> stream, BiFunction<Integer, ? super T, ? extends R> mapper) {
        return zipWithIndex(stream).map(entry -> mapper.apply(entry.getKey(), entry.getValue()));
    }

    public static void main(String[] args) {
        String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

        System.out.println("Test zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Test mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index+"="+name).forEach((String s) -> System.out.println(s));
    }
}

+1: pudo implementar una función que "inserta" un elemento que cada N indexa StreamSupport.stream()y un iterador personalizado.
ach

13

Además de protonpack, la Seq de jOOλ proporciona esta funcionalidad (y por bibliotecas de extensión que se basan en ella como cyclops-react , yo soy el autor de esta biblioteca).

Seq.seq(Stream.of(names)).zipWithIndex()
                         .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                         .toList();

Seq también admite solo Seq.of (nombres) y creará una secuencia JDK debajo de las cubiertas.

El equivalente de reacción simple sería similar a

 LazyFutureStream.of(names)
                 .zipWithIndex()
                 .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                 .toList();

La versión de reacción simple está más adaptada para el procesamiento asíncrono / concurrente.


John, hoy vi tu biblioteca, estoy asombrado y confundido.
GOXR3PLUS

12

Para completar, aquí está la solución que involucra mi biblioteca StreamEx :

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
EntryStream.of(names)
    .filterKeyValue((idx, str) -> str.length() <= idx+1)
    .values().toList();

Aquí creamos un EntryStream<Integer, String>que extiende Stream<Entry<Integer, String>>y agrega algunas operaciones específicas como filterKeyValueo values. También toList()se usa el atajo.


buen trabajo; ¿hay un atajo para .forEach(entry -> {}) ?
Steve Oh

2
@SteveOh si entiendo tu pregunta correctamente, entonces sí, puedes escribir .forKeyValue((key, value) -> {}).
Tagir Valeev

8

Encontré las soluciones aquí cuando el Stream se crea de una lista o matriz (y sabes el tamaño). Pero, ¿qué pasa si Stream tiene un tamaño desconocido? En este caso, pruebe esta variante:

public class WithIndex<T> {
    private int index;
    private T value;

    WithIndex(int index, T value) {
        this.index = index;
        this.value = value;
    }

    public int index() {
        return index;
    }

    public T value() {
        return value;
    }

    @Override
    public String toString() {
        return value + "(" + index + ")";
    }

    public static <T> Function<T, WithIndex<T>> indexed() {
        return new Function<T, WithIndex<T>>() {
            int index = 0;
            @Override
            public WithIndex<T> apply(T t) {
                return new WithIndex<>(index++, t);
            }
        };
    }
}

Uso:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
    stream.map(WithIndex.indexed()).forEachOrdered(e -> {
        System.out.println(e.index() + " -> " + e.value());
    });
}

6

Con una Lista puedes probar

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // An example list of Strings
strings.stream() // Turn the list into a Stream
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Create a map of the index to the object
        .forEach((i, o) -> { // Now we can use a BiConsumer forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

Salida:

0 => First
1 => Second
2 => Third
3 => Fourth
4 => Fifth

1
En realidad es una buena idea, pero strings :: indexOf puede ser un poco caro. Mi sugerencia es usar en su lugar: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Simplemente puede usar el método size () para crear el índice.
gil.fernandes

@ gil.fernandes Gracias por la sugerencia. Haré las ediciones.
V0idst4r

3

No hay una forma de iterar durante un Streamtiempo teniendo acceso al índice porque a Streames diferente a cualquiera Collection. A Streames simplemente una tubería para transportar datos de un lugar a otro, como se indica en la documentación :

Sin almacenamiento. Una secuencia no es una estructura de datos que almacena elementos; en cambio, transportan valores de una fuente (que podría ser una estructura de datos, un generador, un canal IO, etc.) a través de una tubería de operaciones computacionales.

Por supuesto, como parece estar insinuando en su pregunta, siempre puede convertir su Stream<V>en un Collection<V>, como a List<V>, en el que tendrá acceso a los índices.


2
Está disponible en otros idiomas / herramientas. Es simplemente un valor incremental pasado a la función de mapa
Lee Campbell

Su enlace a la documentación está roto.
Usman Mutawakil

3

Con https://github.com/poetix/protonpack puedes hacer eso zip:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = IntStream.range(0, names.length).boxed(); 

nameList = StreamUtils.zip(indices, stream(names),SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey()).map(Entry::getValue).collect(toList());                   

System.out.println(nameList);

3

Si no le importa usar una biblioteca de terceros, Eclipse Collections tiene zipWithIndexy está forEachWithIndexdisponible para su uso en muchos tipos. Aquí hay un conjunto de soluciones a este desafío para los tipos JDK y Eclipse Collections zipWithIndex.

String[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
ImmutableList<String> expected = Lists.immutable.with("Erik");
Predicate<Pair<String, Integer>> predicate =
    pair -> pair.getOne().length() <= pair.getTwo() + 1;

// JDK Types
List<String> strings1 = ArrayIterate.zipWithIndex(names)
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings1);

List<String> list = Arrays.asList(names);
List<String> strings2 = ListAdapter.adapt(list)
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings2);

// Eclipse Collections types
MutableList<String> mutableNames = Lists.mutable.with(names);
MutableList<String> strings3 = mutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings3);

ImmutableList<String> immutableNames = Lists.immutable.with(names);
ImmutableList<String> strings4 = immutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings4);

MutableList<String> strings5 = mutableNames.asLazy()
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne, Lists.mutable.empty());
Assert.assertEquals(expected, strings5);

Aquí hay una solución usando en su forEachWithIndexlugar.

MutableList<String> mutableNames =
    Lists.mutable.with("Sam", "Pamela", "Dave", "Pascal", "Erik");
ImmutableList<String> expected = Lists.immutable.with("Erik");

List<String> actual = Lists.mutable.empty();
mutableNames.forEachWithIndex((name, index) -> {
        if (name.length() <= index + 1)
            actual.add(name);
    });
Assert.assertEquals(expected, actual);

Si cambia las lambdas a las clases internas anónimas anteriores, todos estos ejemplos de código funcionarán también en Java 5 - 7.

Nota: Soy un committer para Eclipse Collections


2

Si utiliza Vavr (anteriormente conocido como Javaslang), puede aprovechar el método dedicado:

Stream.of("A", "B", "C")
  .zipWithIndex();

Si imprimimos el contenido, veremos algo interesante:

Stream((A, 0), ?)

Esto se debe a que Streamsson flojos y no tenemos idea de los próximos elementos en la transmisión.


1

Si está intentando obtener un índice basado en un predicado, intente esto:

Si solo te importa el primer índice:

OptionalInt index = IntStream.range(0, list.size())
    .filter(i -> list.get(i) == 3)
    .findFirst();

O si quieres encontrar múltiples índices:

IntStream.range(0, list.size())
   .filter(i -> list.get(i) == 3)
   .collect(Collectors.toList());

Agregue .orElse(-1);en caso de que desee devolver un valor si no lo encuentra.


1

Aquí está el código de AbacusUtil

Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

Divulgación: Soy el desarrollador de AbacusUtil.


1

Puede usar IntStream.iterate()para obtener el índice:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i < names.length, i -> i + 1)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

Esto solo funciona para Java 9 hacia arriba en Java 8, puede usar esto:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i + 1)
        .limit(names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

0

Puede crear una clase interna estática para encapsular el indexador como necesitaba hacer en el siguiente ejemplo:

static class Indexer {
    int i = 0;
}

public static String getRegex() {
    EnumSet<MeasureUnit> range = EnumSet.allOf(MeasureUnit.class);
    StringBuilder sb = new StringBuilder();
    Indexer indexer = new Indexer();
    range.stream().forEach(
            measureUnit -> {
                sb.append(measureUnit.acronym);
                if (indexer.i < range.size() - 1)
                    sb.append("|");

                indexer.i++;
            }
    );
    return sb.toString();
}

0

Esta pregunta ( Stream Way para obtener el índice del primer elemento que coincide con el booleano ) ha marcado la pregunta actual como un duplicado, por lo que no puedo responderla allí; Lo estoy respondiendo aquí.

Aquí hay una solución genérica para obtener el índice coincidente que no requiere una biblioteca externa.

Si tienes una lista.

public static <T> int indexOf(List<T> items, Predicate<T> matches) {
        return IntStream.range(0, items.size())
                .filter(index -> matches.test(items.get(index)))
                .findFirst().orElse(-1);
}

Y llámalo así:

int index = indexOf(myList, item->item.getId()==100);

Y si usa una colección, pruebe esta.

   public static <T> int indexOf(Collection<T> items, Predicate<T> matches) {
        int index = -1;
        Iterator<T> it = items.iterator();
        while (it.hasNext()) {
            index++;
            if (matches.test(it.next())) {
                return index;
            }
        }
        return -1;
    }

0

Una forma posible es indexar cada elemento en el flujo:

AtomicInteger index = new AtomicInteger();
Stream.of(names)
  .map(e->new Object() { String n=e; public i=index.getAndIncrement(); })
  .filter(o->o.n.length()<=o.i) // or do whatever you want with pairs...
  .forEach(o->System.out.println("idx:"+o.i+" nam:"+o.n));

El uso de una clase anónima a lo largo de una secuencia no se usa bien mientras es muy útil.


0

no necesita map necesariamente una
lambda más cercana al ejemplo de LINQ:

int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );

0
String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
String completeString
         =  IntStream.range(0,namesArray.length)
           .mapToObj(i -> namesArray[i]) // Converting each array element into Object
           .map(String::valueOf) // Converting object to String again
           .collect(Collectors.joining(",")); // getting a Concat String of all values
        System.out.println(completeString);

SALIDA: Sam, Pamela, Dave, Pascal, Erik

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

IntStream.range(0,namesArray.length)
               .mapToObj(i -> namesArray[i]) // Converting each array element into Object
               .map(String::valueOf) // Converting object to String again
               .forEach(s -> {
                //You can do various operation on each element here
                System.out.println(s);
               }); // getting a Concat String of all 

Para recoger en la lista:

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
 List<String> namesList
                =  IntStream.range(0,namesArray.length)
                .mapToObj(i -> namesArray[i]) // Converting each array element into Object
                .map(String::valueOf) // Converting object to String again
                .collect(Collectors.toList()); // collecting elements in List
        System.out.println(listWithIndex);

Se espera que la solución de la pregunta anterior sea un Listelemento que contenga Erik .
Kaplan

También he agregado un ejemplo para recopilar en la Lista.
Arpan Saini

0

Como dijo jean-baptiste-yunès, si su transmisión se basa en una Lista de Java, entonces usar un AtomicInteger y su método incrementAndGet es una muy buena solución al problema y el entero devuelto corresponde al índice en la Lista original siempre que No use una corriente paralela.


0

Si necesita el índice en forEach, esto proporciona una manera.

  public class IndexedValue {

    private final int    index;
    private final Object value;

    public IndexedValue(final int index, final Object value) { 
        this.index = index;
        this.value = value;
    }

    public int getIndex() {
        return index;
    }

    public Object getValue() {
        return value;
    }
}

Luego úsalo de la siguiente manera.

@Test
public void withIndex() {
    final List<String> list = Arrays.asList("a", "b");
    IntStream.range(0, list.size())
             .mapToObj(index -> new IndexedValue(index, list.get(index)))
             .forEach(indexValue -> {
                 System.out.println(String.format("%d, %s",
                                                  indexValue.getIndex(),
                                                  indexValue.getValue().toString()));
             });
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.