Existe uma maneira concisa de iterar em um fluxo com índices no Java 8?


382

Existe uma maneira concisa de iterar em um fluxo enquanto tiver acesso ao índice no fluxo?

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());

o que parece bastante decepcionante em comparação com o exemplo LINQ dado lá

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

Existe uma maneira mais concisa?

Além disso, parece que o zip foi movido ou removido ...


2
O que é intRange()? Até agora, não encontramos esse método no Java 8.
Rohit Jain

@RohitJain Provavelmente um IntStream.rangeClosed(x, y).
assylias

2
Como comentário lateral, o desafio 4 parece melhor (IMO) comList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
Sim, zipfoi removido, juntamente com fluxos experimentais de dois valores, chamados de BiStreamou MapStream. O principal problema é que, para fazer isso efetivamente, o Java realmente precisa de um tipo de par (ou tupla) de tipo estrutural. Na falta de uma, é fácil criar uma classe par ou tupla genérica - isso já foi feito várias vezes - mas todas elas são apagadas para o mesmo tipo.
Stuart Marks

3
Ah, outro problema com uma classe Pair ou Tuple genérica é que ela exige que todas as primitivas sejam encaixotadas.
Stuart Marks

Respostas:


435

A maneira mais limpa é começar a partir de um fluxo 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());

A lista resultante contém apenas "Erik".


Uma alternativa que parece mais familiar quando você está acostumado a fazer loops seria manter um contador ad hoc usando um objeto mutável, por exemplo 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());

Observe que o uso do último método em um fluxo paralelo pode ser interrompido, pois os itens não são necessariamente processados ​​"em ordem" .


28
Usar atômicos dessa maneira é problemático com fluxos paralelos. Primeiro, a ordem de processamento dos elementos não será necessariamente a mesma que a ordem na qual os elementos ocorrem na matriz inicial. Portanto, o "índice" atribuído usando o atômico provavelmente não corresponderá ao índice real da matriz. Segundo, enquanto os átomos são seguros para threads, você pode encontrar contenção entre vários threads que atualizam o atômico, degradando a quantidade de paralelismo.
Stuart Marks

11
Desenvolvi uma solução semelhante à da @assylias. Para contornar a problemática com o fluxo paralelo @StuartMarks mencionado, primeiro faço um determinado fluxo paralelo sequencial, faço o mapeamento e restauro o 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

4
@DanielDietrich Se você acha que ela resolve a pergunta, poste-a como resposta, e não como comentário (e o código também será mais legível!).
Assilias 15/08/2014

3
@DanielDietrich Desculpe, se eu estiver lendo esse código corretamente, ele não funcionará. Você não pode ter diferentes segmentos de um pipeline executando em paralelo versus seqüencial. Somente o último parallelou sequentialé respeitado quando a operação do terminal começa.
Stuart Marks

4
Por uma questão de justiça, "a maneira mais limpa" foi roubada da resposta de @ Stuart.
Vadzim

70

A API de fluxos Java 8 não possui os recursos para obter o índice de um elemento de fluxo, bem como a capacidade de compactar fluxos juntos. Isso é lamentável, pois torna certos aplicativos (como os desafios do LINQ) mais difíceis do que seriam.

No entanto, geralmente existem soluções alternativas. Geralmente, isso pode ser feito "direcionando" o fluxo com um intervalo inteiro e aproveitando o fato de que os elementos originais geralmente estão em uma matriz ou em uma coleção acessível por índice. Por exemplo, o problema do Desafio 2 pode ser resolvido desta maneira:

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 mencionei acima, isso tira proveito do fato de que a fonte de dados (a matriz de nomes) é diretamente indexável. Se não fosse, essa técnica não funcionaria.

Admito que isso não satisfaz a intenção do Desafio 2. No entanto, resolve o problema de maneira razoavelmente eficaz.

EDITAR

Meu exemplo de código anterior costumava flatMapfundir as operações de filtro e mapa, mas isso era complicado e não oferecia vantagem. Eu atualizei o exemplo pelo comentário de Holger.


7
Que tal IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Ele faz o trabalho sem o boxe ...
Holger

11
Hum, sim, por que eu achava que precisava usar flatMapassim mesmo?
Stuart Marcas

2
Finalmente revisitando isso ... Eu provavelmente usei flatMapporque meio que funde uma operação de filtragem e mapeamento em uma única operação, mas isso realmente não oferece nenhuma vantagem. Vou editar o exemplo.
Stuart Marks

Stream.of (Array) criará uma interface de fluxo para uma matriz. Tornando-o efetivamente em Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );Menos remoção da caixa de seleção e menos alocação de memória; pois não estamos mais criando um fluxo de intervalo.
Code Eyez

44

Desde a goiaba 21, você pode usar

Streams.mapWithIndex()

Exemplo (do documento oficial ):

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

3
Além disso, o pessoal da Guava ainda não implementou o ForEachWithIndex (aceitar um consumidor em vez de uma função), mas é um problema atribuído: github.com/google/guava/issues/2913 .
John Glassmyer

25

Eu usei a seguinte solução no meu projeto. Eu acho que é melhor do que usar objetos mutáveis ​​ou intervalos inteiros.

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 - conseguiu implementar uma função que "insere" um elemento que todos os N indexam usando StreamSupport.stream()e um iterador personalizado.
ach

13

Além do protonpack, o Seq do jOOλ fornece essa funcionalidade (e por bibliotecas de extensão que o compõem como cyclops-react , eu sou o autor desta biblioteca).

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

O Seq também suporta apenas o Seq.of (nomes) e criará um JDK Stream sob as cobertas.

O equivalente de reação simples seria semelhante a

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

A versão de reação simples é mais personalizada para processamento assíncrono / simultâneo.


John, vi hoje sua biblioteca, estou surpreso e confuso.
GOXR3PLUS

12

Apenas para completar, aqui está a solução que envolve minha biblioteca StreamEx :

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

Aqui criamos um EntryStream<Integer, String>que estende Stream<Entry<Integer, String>>e adiciona algumas operações específicas como filterKeyValueou values. Também toList()é usado um atalho.


Ótimo trabalho; existe um atalho para .forEach(entry -> {}) ?
31518 Steve

2
@ SteveOh se eu entendi sua pergunta corretamente, então sim, você pode escrever .forKeyValue((key, value) -> {}).
Tagir Valeev

8

Encontrei as soluções aqui quando o Stream é criado de lista ou matriz (e você sabe o tamanho). Mas e se o Stream tiver tamanho desconhecido? Nesse caso, tente 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

Com uma lista, você pode tentar

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));
        });

Resultado:

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

11
Na verdade, é uma boa idéia, mas strings :: indexOf pode ser um pouco caro. Minha sugestão é usar: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Você pode simplesmente usar o método size () para criar o índice.
Gil.fernandes

@ gil.fernandes Obrigado pela sugestão. Eu vou fazer as edições.
V0idst4r

3

Não há uma maneira de iterar durante um Streamtempo tendo acesso ao índice porque a Streamé diferente de qualquer outro Collection. A Streamé apenas um pipeline para transportar dados de um lugar para outro, conforme declarado na documentação :

Nenhum armazenamento. Um fluxo não é uma estrutura de dados que armazena elementos; em vez disso, eles carregam valores de uma fonte (que pode ser uma estrutura de dados, um gerador, um canal de IO etc.) através de um pipeline de operações computacionais.

Obviamente, como você parece sugerir na sua pergunta, você sempre pode convertê-lo Stream<V>em um Collection<V>, como um List<V>, no qual você terá acesso aos índices.


2
Está disponível em outros idiomas / ferramentas. É simplesmente um valor de incremento passado para a função mapa
Lee Campbell

Seu link para a documentação está quebrado.
Usman Mutawakil 28/09

3

Com https://github.com/poetix/protonpack, você pode fazer isso:

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

Se você não se importa em usar uma biblioteca de terceiros, o Eclipse Collections tem zipWithIndexe está forEachWithIndexdisponível para uso em vários tipos. Aqui está um conjunto de soluções para esse desafio para os tipos JDK e Eclipse Collections usando 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);

Aqui está uma solução usando em seu 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);

Se você alterar as lambdas para classes internas anônimas acima, todos esses exemplos de código também funcionarão no Java 5 - 7.

Nota: Eu sou um confirmador das Coleções Eclipse


2

Se você usar o Vavr (anteriormente conhecido como Javaslang), poderá aproveitar o método dedicado:

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

Se imprimirmos o conteúdo, veremos algo interessante:

Stream((A, 0), ?)

Isso ocorre porque Streamssão preguiçosos e não temos idéia dos próximos itens no fluxo.


1

Se você está tentando obter um índice com base em um predicado, tente o seguinte:

Se você se importa apenas com o primeiro índice:

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

Ou se você deseja encontrar vários índices:

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

Adicione .orElse(-1);no caso de você querer retornar um valor se ele não o encontrar.


1

Aqui está o código de AbacusUtil

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

Divulgação: sou o desenvolvedor do AbacusUtil.


1

Você pode usar IntStream.iterate()para obter o í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());

Isso funciona apenas para o Java 9 para cima no Java 8, você pode usar isto:

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

Você pode criar uma classe interna estática para encapsular o indexador, como eu precisava fazer no exemplo abaixo:

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 questão ( Stream Way para obter o índice do primeiro elemento correspondente a booleano ) marcou a pergunta atual como duplicada, por isso não posso respondê-la; Eu estou respondendo aqui.

Aqui está uma solução genérica para obter o índice correspondente que não requer uma biblioteca externa.

Se você tem uma 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);
}

E chame assim:

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

E se estiver usando uma coleção, tente 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

Uma maneira possível é indexar cada elemento no fluxo:

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));

O uso de uma classe anônima ao longo de um fluxo não é bem utilizado, sendo muito útil.


0

você não precisa map necessariamente de um
lambda mais próximo do exemplo 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);

SAÍDA: 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 coletar na 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);

Espera-se que a solução da pergunta acima seja Listum elemento contendo Erik .
Kaplan

Também adicionei um exemplo para coletar na lista.
Arpan Saini 27/10/19

0

Como jean-baptiste-yunès disse, se o seu fluxo for baseado em uma lista java, usar um AtomicInteger e seu método incrementAndGet é uma solução muito boa para o problema e o número inteiro retornado corresponde ao índice na lista original, desde que você não use um fluxo paralelo.


0

Se você precisar do índice no forEach, isso fornece uma maneira.

  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;
    }
}

Em seguida, use-o da seguinte maneira.

@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()));
             });
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.