Como adicionar elementos de um fluxo Java8 a uma lista existente

155

Javadoc do Collector mostra como coletar elementos de um fluxo em uma nova lista. Existe um one-liner que adicione os resultados em um ArrayList existente?

codefx
fonte
1
Já existe uma resposta aqui . Procure o item "Adicionar a um existente Collection"
Holger

Respostas:

198

NOTA: a resposta do nosid mostra como adicionar a uma coleção existente usando forEachOrdered(). Essa é uma técnica útil e eficaz para alterar as coleções existentes. Minha resposta aborda por que você não deve usar a Collectorpara alterar uma coleção existente.

A resposta curta é não , pelo menos não em geral, você não deve usar a Collectorpara modificar uma coleção existente.

O motivo é que os coletores são projetados para oferecer suporte ao paralelismo, mesmo em coleções que não são seguras para threads. A maneira como eles fazem isso é fazer com que cada thread opere independentemente em sua própria coleção de resultados intermediários. A maneira como cada encadeamento obtém sua própria coleção é chamar o Collector.supplier()que é necessário para retornar uma nova coleção a cada vez.

Essas coleções de resultados intermediários são mescladas, novamente de maneira confinada por encadeamento, até que haja uma única coleção de resultados. Este é o resultado final da collect()operação.

Algumas respostas de Balder e assylias sugeriram o uso Collectors.toCollection()e a transmissão de um fornecedor que retorna uma lista existente em vez de uma nova. Isso viola o requisito do fornecedor, que é o de retornar uma coleção nova e vazia a cada vez.

Isso funcionará para casos simples, como demonstram os exemplos em suas respostas. No entanto, ele falhará, principalmente se o fluxo for executado em paralelo. (Uma versão futura da biblioteca pode mudar de alguma forma imprevista que fará com que ela falhe, mesmo no caso seqüencial.)

Vamos dar um exemplo simples:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Quando executo esse programa, geralmente recebo um ArrayIndexOutOfBoundsException. Isso ocorre porque vários threads estão operando ArrayList, uma estrutura de dados insegura. OK, vamos sincronizar:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Isso não falhará mais com uma exceção. Mas em vez do resultado esperado:

[foo, 0, 1, 2, 3]

dá resultados estranhos como este:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Este é o resultado das operações de acumulação / mesclagem confinadas em threads que descrevi acima. Com um fluxo paralelo, cada encadeamento chama o fornecedor para obter sua própria coleção para acumulação intermediária. Se você passar um fornecedor que retorna a mesma coleção, cada thread anexa seus resultados a essa coleção. Como não há nenhuma ordem entre os encadeamentos, os resultados serão anexados em alguma ordem arbitrária.

Então, quando essas coleções intermediárias são mescladas, isso basicamente mescla a lista consigo mesma. As listas são mescladas usando List.addAll(), o que indica que os resultados serão indefinidos se a coleção de origem for modificada durante a operação. Nesse caso, ArrayList.addAll()faz uma operação de cópia de matriz, por isso acaba se duplicando, o que é uma espécie do que se esperaria, eu acho. (Observe que outras implementações da lista podem ter um comportamento completamente diferente.) De qualquer forma, isso explica os resultados estranhos e os elementos duplicados no destino.

Você pode dizer: "Certifico-me de executar meu fluxo seqüencialmente" e vá em frente e escreva um código como este

stream.collect(Collectors.toCollection(() -> existingList))

de qualquer forma. Eu recomendaria não fazer isso. Se você controla o fluxo, com certeza, pode garantir que ele não será executado em paralelo. Espero que um estilo de programação surja onde os fluxos são entregues em vez de coleções. Se alguém lhe entregar um fluxo e você usar esse código, falhará se o fluxo for paralelo. Pior ainda, alguém pode entregar a você um fluxo seqüencial e esse código funcionará bem por um tempo, passará em todos os testes etc. Então, durante algum tempo arbitrário depois, o código em outra parte do sistema poderá mudar para usar fluxos paralelos que causarão seu código quebrar.

OK, lembre-se de ligar sequential()para qualquer fluxo antes de usar este código:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Claro, você vai se lembrar de fazer isso toda vez, certo? :-) Digamos que você faça. Então, a equipe de desempenho se perguntará por que todas as suas implementações paralelas cuidadosamente criadas não estão fornecendo nenhuma aceleração. E mais uma vez, eles rastrearão o código, o que está forçando todo o fluxo a executar sequencialmente.

Não faça isso.

Stuart Marks
fonte
Ótima explicação! - obrigado por esclarecer isso. Vou editar minha resposta para recomendar nunca fazer isso com possíveis fluxos paralelos.
Balder
3
Se a pergunta for, se houver uma linha para adicionar elementos de um fluxo a uma lista existente, a resposta curta será sim . Veja minha resposta. No entanto, concordo com você, que o uso de Collectors.toCollection () em combinação com uma lista existente é o caminho errado.
Nos 31/03
Verdade. Acho que todos nós pensávamos em colecionadores.
Stuart Marcas
Ótima resposta! Fico muito tentado a usar a solução seqüencial, mesmo se você não o aconselhar claramente, porque, como afirmado, deve funcionar bem. Mas o fato de o javadoc exigir que o argumento do fornecedor do toCollectionmétodo retorne uma coleção nova e vazia a cada vez me convence a não fazê-lo. Eu realmente quero quebrar o contrato javadoc das principais classes Java.
zoom
1
@AlexCurvers Se você deseja que o fluxo tenha efeitos colaterais, quase certamente deseja usá-lo forEachOrdered. Os efeitos colaterais incluem adicionar elementos a uma coleção existente, independentemente de ela já ter elementos. Se você deseja que os elementos de um fluxo sejam inseridos em uma nova coleção, use collect(Collectors.toList())ou toSet()ou toCollection().
Stuart Marks
169

Até onde eu posso ver, todas as outras respostas até agora usaram um coletor para adicionar elementos a um fluxo existente. No entanto, existe uma solução mais curta e funciona para fluxos sequenciais e paralelos. Você pode simplesmente usar o método forEachOrdered em combinação com uma referência de método.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

A única restrição é que a origem e o destino são listas diferentes, porque você não tem permissão para fazer alterações na origem de um fluxo, desde que seja processado.

Observe que esta solução funciona para fluxos sequenciais e paralelos. No entanto, ele não se beneficia da simultaneidade. A referência do método passada para forEachOrdered sempre será executada sequencialmente.

nosid
fonte
6
+1 É engraçado como tantas pessoas afirmam que não há possibilidade quando existe uma. Btw. Incluí forEach(existing::add)como possibilidade uma resposta há dois meses . Eu deveria ter acrescentado forEachOrdered, bem ...
Holger
5
Existe algum motivo que você usou em forEachOrderedvez de forEach?
membersound
6
@membersound: forEachOrderedfunciona para fluxos sequenciais e paralelos . Por outro lado, forEachpode executar o objeto de função passado simultaneamente para fluxos paralelos. Nesse caso, o objeto de função deve ser sincronizado corretamente, por exemplo, usando a Vector<Integer>.
Nos 25/03
@BrianGoetz: Devo admitir que a documentação do Stream.forEachOrdered é um pouco imprecisa. No entanto, não vejo nenhuma interpretação razoável dessa especificação , na qual não exista relação de antes do acontecimento entre duas chamadas de target::add. Independentemente de quais threads o método é chamado, não há corrida de dados . Eu esperava que você soubesse disso.
Nos 26/07/2015
Esta é a resposta mais útil, no que me diz respeito. Na verdade, mostra uma maneira prática de inserir itens em uma lista existente de um fluxo, que é o que a pergunta fazia (apesar da palavra enganosa "coletar")
Wheezil
12

A resposta curta é não (ou deveria ser não). EDIT: sim, é possível (veja a resposta dos assylias abaixo), mas continue lendo. EDIT2: mas veja a resposta de Stuart Marks por mais uma razão pela qual você ainda não deve fazê-lo!

A resposta mais longa:

O objetivo dessas construções no Java 8 é introduzir alguns conceitos de Programação Funcional na linguagem; na Programação Funcional, as estruturas de dados normalmente não são modificadas; em vez disso, novas são criadas fora das antigas por meio de transformações como mapa, filtro, dobra / redução e muitas outras.

Se você precisar modificar a lista antiga, basta coletar os itens mapeados em uma nova lista:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

e faça list.addAll(newList)novamente: se você realmente deve.

(ou construa uma nova lista concatenando a antiga e a nova e atribua-a de volta à listvariável - isso é um pouco mais no espírito do FP do que addAll)

Quanto à API: mesmo que a API permita (novamente, consulte a resposta de assylias), você deve evitar fazer isso independentemente, pelo menos em geral. É melhor não combater o paradigma (FP) e tentar aprendê-lo, em vez de combatê-lo (mesmo que Java geralmente não seja uma linguagem FP), e apenas recorrer a táticas "mais sujas" se for absolutamente necessário.

A resposta realmente longa: (ou seja, se você incluir o esforço de realmente encontrar e ler uma introdução / livro sobre FP, conforme sugerido)

Para descobrir por que modificar listas existentes geralmente é uma má idéia e leva a um código menos sustentável - a menos que você esteja modificando uma variável local e seu algoritmo seja curto e / ou trivial, o que está fora do escopo da questão da manutenção de códigos - encontre uma boa introdução à Programação Funcional (existem centenas) e comece a ler. Uma explicação de "visualização" seria algo como: é matematicamente mais fácil e mais racional para não modificar dados (na maioria das partes do seu programa) e leva a um nível mais alto e menos técnico (além de mais amigável ao ser humano, uma vez que seu cérebro transições do pensamento imperativo à moda antiga) definições da lógica do programa.

Erik Kaplun
fonte
@assylias: logicamente, não estava errado porque havia o ou parte; de qualquer forma, adicionou uma nota.
Erik Kaplun 31/03
1
A resposta curta está certa. Os one-liners propostos terão sucesso em casos simples, mas falharão no caso geral.
Stuart Marcas
A resposta mais longa está correta, mas o design da API é principalmente sobre paralelismo e menos sobre programação funcional. Embora, é claro, existam muitas coisas sobre FP que são passíveis de paralelismo, então esses dois conceitos estão bem alinhados.
Stuart Marcas
@StuartMarks: Interessante: em quais casos a solução fornecida nas respostas das assilias se decompõe? (e bons pontos sobre o paralelismo-Acho que fiquei muito ansioso para defender FP)
Erik Kaplun
@ErikAllik Adicionei uma resposta que cobre esse problema.
Stuart Marcas
11

Erik Allik já deu boas razões, por que você provavelmente não desejará coletar elementos de um fluxo em uma lista existente.

De qualquer forma, você pode usar a seguinte linha, se realmente precisar dessa funcionalidade.

Mas, como Stuart Marks explica em sua resposta, você nunca deve fazer isso, se os fluxos puderem ser paralelos - use por seu próprio risco ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Balder
fonte
ahh, isso é uma pena: P
Erik Kaplun 31/03
2
Essa técnica falhará terrivelmente se o fluxo for executado em paralelo.
Stuart Marks
1
Seria responsabilidade do provedor de coleta garantir que isso não falhe - por exemplo, fornecendo uma coleta simultânea.
Balder
2
Não, esse código viola o requisito de toCollection (), que é o de que o fornecedor retorne uma nova coleção vazia do tipo apropriado. Mesmo que o destino seja seguro para threads, a mesclagem feita para o caso paralelo dará origem a resultados incorretos.
Stuart Marks
1
@ Balder Adicionei uma resposta que deve esclarecer isso.
Stuart Marcas
4

Você apenas precisa indicar sua lista original para ser a que Collectors.toList()retorna.

Aqui está uma demonstração:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

E aqui está como você pode adicionar os elementos recém-criados à sua lista original em apenas uma linha.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

É isso que este Paradigma de Programação Funcional fornece.

Aman Agnihotri
fonte
Eu quis dizer como adicionar / coletar em uma lista existente e não apenas reatribuir.
Codefx 31/03
1
Bem, tecnicamente, você não pode fazer esse tipo de coisa no paradigma de Programação Funcional, que trata de fluxos. Na Programação Funcional, o estado não é modificado; em vez disso, novos estados são criados em estruturas de dados persistentes, tornando-o seguro para fins de simultaneidade e mais funcional. A abordagem que mencionei é o que você pode fazer, ou você pode recorrer à abordagem orientada a objetos do estilo antigo, onde itera sobre cada elemento e mantém ou remove os elementos como achar melhor.
Aman Agnihotri
0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());

AS Ranjan
fonte
0

Concatenaria a lista antiga e a nova lista como fluxos e salvaria os resultados na lista de destinos. Funciona bem em paralelo também.

Usarei o exemplo de resposta aceita dada por Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Espero que ajude.

Nikos Stais
fonte