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?
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?
Respostas:
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 Collector
para alterar uma coleção existente.
A resposta curta é não , pelo menos não em geral, você não deve usar a Collector
para 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.
toCollection
mé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.
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()
.
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.
forEach(existing::add)
como possibilidade uma resposta há dois meses . Eu deveria ter acrescentado forEachOrdered
, bem ...
forEachOrdered
vez de forEach
?
forEachOrdered
funciona para fluxos sequenciais e paralelos . Por outro lado, forEach
pode 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>
.
target::add
. Independentemente de quais threads o método é chamado, não há corrida de dados . Eu esperava que você soubesse disso.
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 à list
variá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 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));
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.
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.
Collection
"