Java 8 lambda obtém e remove elemento da lista

88

Dada uma lista de elementos, desejo obter o elemento com uma determinada propriedade e removê-lo da lista. A melhor solução que encontrei é:

ProducerDTO p = producersProcedureActive
                .stream()
                .filter(producer -> producer.getPod().equals(pod))
                .findFirst()
                .get();
producersProcedureActive.remove(p);

É possível combinar get e remove em uma expressão lambda?

Marco Stramezzi
fonte
9
Este realmente parece um caso clássico de quando usar apenas um loop e um iterador.
chrylis
1
@chrylis Eu discordo gentilmente;) Estamos tão acostumados com a programação imperativa que qualquer outra forma soa muito exótica. Imagine se a realidade fosse o contrário: estamos muito acostumados com a programação funcional e um novo paradigma imperativo é adicionado ao Java. Você diria que este seria o caso clássico para fluxos, predicados e opcionais?
fps de
9
Não ligue get()aqui! Você não tem ideia se está vazio ou não. Você lançará uma exceção se o elemento não estiver lá. Em vez disso, use um dos métodos seguros como ifPresent, orElse, orElseGet ou orElseThrow.
Brian Goetz
@FedericoPeraltaSchaffner Provavelmente uma questão de gosto. Mas eu também proporia não combinar os dois. Quando vejo algum código obtendo um valor usando fluxos, normalmente presumo que as operações no fluxo estão livres de efeitos colaterais. Misturar na remoção do elemento pode resultar em código que pode enganar os leitores.
Matthias Wimmer
Apenas para esclarecer: Você deseja remover todos os elementos do listpara os quais o Predicateé verdadeiro ou apenas o primeiro (de um possivelmente zero, um ou vários elementos)?
Kedar Mhaswade

Respostas:

148

Para remover o elemento da lista

objectA.removeIf(x -> conditions);

por exemplo:

objectA.removeIf(x -> blockedWorkerIds.contains(x));

List<String> str1 = new ArrayList<String>();
str1.add("A");
str1.add("B");
str1.add("C");
str1.add("D");

List<String> str2 = new ArrayList<String>();
str2.add("D");
str2.add("E");

str1.removeIf(x -> str2.contains(x)); 

str1.forEach(System.out::println);

SAÍDA: A B C

uma shankar
fonte
Eu sugeriria que esta é a melhor resposta
Sergey Pauk
1
Isso é muito legal. Você pode precisar implementar equals / hashCode se isso não for feito para seus próprios objetos. (Neste exemplo, String é usada, que os tem por padrão).
bram000
15
IMHO a resposta não considera a parte "get": removeIfé uma solução elegante para remover elementos de uma coleção, mas não retorna o elemento removido.
Marco Stramezzi
É um modo muito simples de excluir um objeto de java ArrayList. Thkns muito. Funciona bem para mim.
Marcelo Rebouças
2
Isso não responde à pergunta. O requisito é remover o elemento da lista e obter o item / itens removidos para uma nova lista.
Shanika Ediriweera
34

Embora o segmento seja bastante antigo, ainda se pensa em fornecer solução - utilizando Java8.

Faça uso da removeIffunção. A complexidade do tempo éO(n)

producersProcedureActive.removeIf(producer -> producer.getPod().equals(pod));

Referência da API: removeIf docs

Suposição: producersProcedureActiveé umList

NOTA: Com esta abordagem, você não conseguirá reter o item excluído.

asifsid88
fonte
Além de excluir o elemento da lista, o OP ainda deseja uma referência ao elemento.
eee
@eee: Muito obrigado por apontar isso. Perdi essa parte da pergunta original de OP.
asifsid88 02 de
apenas notar que isso removerá todos os itens que correspondem à condição. Mas OP parece precisar remover apenas o primeiro item (OP usou findFirst ())
nantitv
17

Considere o uso de iteradores Java vanilla para executar a tarefa:

public static <T> T findAndRemoveFirst(Iterable<? extends T> collection, Predicate<? super T> test) {
    T value = null;
    for (Iterator<? extends T> it = collection.iterator(); it.hasNext();)
        if (test.test(value = it.next())) {
            it.remove();
            return value;
        }
    return null;
}

Vantagens :

  1. É claro e óbvio.
  2. Ele percorre apenas uma vez e apenas até o elemento correspondente.
  3. Você pode fazer isso em qualquer, Iterablemesmo sem stream()suporte (pelo menos aqueles que implementam remove()em seu iterador) .

Desvantagens :

  1. Você não pode fazer isso no lugar como uma única expressão (método auxiliar ou variável necessária)

Quanto ao

É possível combinar get e remove em uma expressão lambda?

outras respostas mostram claramente que é possível, mas você deve estar ciente de

  1. A pesquisa e a remoção podem percorrer a lista duas vezes
  2. ConcurrentModificationException pode ser lançado ao remover o elemento da lista que está sendo iterada
Vasily Liaskovsky
fonte
4
Gosto dessa solução, mas observe que ela tem uma séria desvantagem que você perdeu: muitas implementações Iterable têm remove()métodos que lançam UOE. (Não os das coleções JDK, é claro, mas acho injusto dizer "funciona em qualquer Iterável".)
Brian Goetz
Acho que podemos supor que, se um elemento pode ser removido em geral , ele pode ser removido pelo iterador
Vasily Liaskovsky
5
Você poderia supor isso, mas tendo examinado centenas de implementações de iteradores, seria uma suposição ruim. (Eu ainda gosto da abordagem; você está apenas vendendo demais.)
Brian Goetz
2
@Brian Goetz: a defaultimplementação de removeIffaz a mesma suposição, mas, é claro, é definida em Collectionao invés de Iterable
Holger
13

A solução direta seria invocar ifPresent(consumer)no Opcional retornado por findFirst(). Este consumidor será chamado quando o opcional não estiver vazio. O benefício também é que não lançará uma exceção se a operação find retornou um opcional vazio, como seu código atual faria; em vez disso, nada acontecerá.

Se você quiser retornar o valor removido, você pode mapo Optionalao resultado da chamada remove:

producersProcedureActive.stream()
                        .filter(producer -> producer.getPod().equals(pod))
                        .findFirst()
                        .map(p -> {
                            producersProcedureActive.remove(p);
                            return p;
                        });

Mas observe que a remove(Object)operação percorrerá novamente a lista para encontrar o elemento a ser removido. Se você tiver uma lista com acesso aleatório, como um ArrayList, seria melhor fazer um Stream sobre os índices da lista e encontrar o primeiro índice correspondente ao predicado:

IntStream.range(0, producersProcedureActive.size())
         .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))
         .boxed()
         .findFirst()
         .map(i -> producersProcedureActive.remove((int) i));

Com essa solução, a remove(int)operação atua diretamente no índice.

Tunaki
fonte
3
Isso é patológico para uma lista encadeada.
chrylis
1
@chrylis A solução do índice seria de fato. Dependendo da implementação da lista, um prefere um em vez do outro. Fez uma pequena edição.
Tunaki
1
@chrylis: no caso de um, LinkedListtalvez você não deva usar a API de fluxo, pois não há solução sem atravessar pelo menos duas vezes. Mas não conheço nenhum cenário da vida real em que a vantagem acadêmica de uma lista vinculada possa compensar sua sobrecarga real. Portanto, a solução simples é nunca usar LinkedList.
Holger
2
Oh, tantas edições ... agora a primeira solução não fornece o elemento removido, pois remove(Object)apenas retorna um booleaninformando se havia um elemento para remover ou não.
Holger
3
@Marco Stramezzi: infelizmente, o comentário explicando isso foi removido. Sem boxed()você consegue um OptionalIntque só pode mapde intpara int. Ao contrário IntStream, não há mapToObjmétodo. Com boxed(), você obterá um Optional<Integer>que permite mapa um objeto arbitrário, ou seja, o ProducerDTOretornado por remove(int). O elenco de Integerpara inté necessário para eliminar a ambigüidade entre remove(int)e remove(Object).
Holger
9

Use pode usar o filtro do Java 8 e criar outra lista se não quiser alterar a lista antiga:

List<ProducerDTO> result = producersProcedureActive
                            .stream()
                            .filter(producer -> producer.getPod().equals(pod))
                            .collect(Collectors.toList());
Toi Nguyen
fonte
5

Tenho certeza de que essa será uma resposta impopular, mas funciona ...

ProducerDTO[] p = new ProducerDTO[1];
producersProcedureActive
            .stream()
            .filter(producer -> producer.getPod().equals(pod))
            .findFirst()
            .ifPresent(producer -> {producersProcedureActive.remove(producer); p[0] = producer;}

p[0] conterá o elemento encontrado ou será nulo.

O "truque" aqui é contornar o problema "efetivamente final" usando uma referência de array que seja efetivamente final, mas definindo seu primeiro elemento.

Boêmio
fonte
1
Nesse caso, não é tão ruim, mas não uma melhoria em relação à possibilidade de apenas ligar .orElse(null)para obter o ProducerDTOou null...
Holger
Nesse caso, pode ser mais fácil apenas ter .orElse(null)e ter um if, não?
Tunaki
@Holger mas como você pode invocar remove()também usando orElse(null)?
Bohemian
1
Basta usar o resultado. if(p!=null) producersProcedureActive.remove(p);ainda é mais curto do que a expressão lambda em sua ifPresentchamada.
Holger
@holger Eu interpretei o objetivo da pergunta como evitar várias declarações - ou seja, uma solução de 1 linha
Boêmio
4

Com Eclipse coleções você pode usar detectIndexjunto com remove(int)em qualquer java.util.List.

List<Integer> integers = Lists.mutable.with(1, 2, 3, 4, 5);
int index = Iterate.detectIndex(integers, i -> i > 2);
if (index > -1) {
    integers.remove(index);
}

Assert.assertEquals(Lists.mutable.with(1, 2, 4, 5), integers);

Se você usar o MutableListtipo de Coleções Eclipse, poderá chamar o detectIndexmétodo diretamente na lista.

MutableList<Integer> integers = Lists.mutable.with(1, 2, 3, 4, 5);
int index = integers.detectIndex(i -> i > 2);
if (index > -1) {
    integers.remove(index);
}

Assert.assertEquals(Lists.mutable.with(1, 2, 4, 5), integers);

Nota: Eu sou um committer para Eclipse Collections

Donald Raab
fonte
2

Quando queremos obter vários elementos de uma lista em uma nova lista (filtrar usando um predicado) e removê-los da lista existente , não consegui encontrar uma resposta adequada em lugar nenhum.

Aqui está como podemos fazer isso usando o particionamento da API Java Streaming.

Map<Boolean, List<ProducerDTO>> classifiedElements = producersProcedureActive
    .stream()
    .collect(Collectors.partitioningBy(producer -> producer.getPod().equals(pod)));

// get two new lists 
List<ProducerDTO> matching = classifiedElements.get(true);
List<ProducerDTO> nonMatching = classifiedElements.get(false);

// OR get non-matching elements to the existing list
producersProcedureActive = classifiedElements.get(false);

Dessa forma, você remove efetivamente os elementos filtrados da lista original e os adiciona a uma nova lista.

Consulte 5.2. Collectors.partitioningPor seção deste artigo .

Shanika Ediriweera
fonte
1

Como outros sugeriram, este pode ser um caso de uso para loops e iteráveis. Em minha opinião, esta é a abordagem mais simples. Se você deseja modificar a lista no local, ela não pode ser considerada uma programação funcional "real" de qualquer maneira. Mas você pode usar o Collectors.partitioningBy()para obter uma nova lista com elementos que satisfaçam sua condição e uma nova lista daqueles que não satisfazem. Claro, com essa abordagem, se você tiver vários elementos que satisfaçam a condição, todos eles estarão nessa lista e não apenas o primeiro.

user140547
fonte
É muito melhor filtrar o stream e coletar os resultados para uma nova lista
fps
1

A lógica abaixo é a solução sem modificar a lista original

List<String> str1 = new ArrayList<String>();
str1.add("A");
str1.add("B");
str1.add("C");
str1.add("D");

List<String> str2 = new ArrayList<String>();
str2.add("D");
str2.add("E");

List<String> str3 = str1.stream()
                        .filter(item -> !str2.contains(item))
                        .collect(Collectors.toList());

str1 // ["A", "B", "C", "D"]
str2 // ["D", "E"]
str3 // ["A", "B", "C"]
KimchiMan
fonte
0

Combinando minha ideia inicial e suas respostas, cheguei ao que parece ser a solução para minha própria pergunta:

public ProducerDTO findAndRemove(String pod) {
    ProducerDTO p = null;
    try {
        p = IntStream.range(0, producersProcedureActive.size())
             .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))
             .boxed()
             .findFirst()
             .map(i -> producersProcedureActive.remove((int)i))
             .get();
        logger.debug(p);
    } catch (NoSuchElementException e) {
        logger.error("No producer found with POD [" + pod + "]");
    }
    return p;
}

Permite remover o objeto usando remove(int)que não percorre novamente a lista (como sugerido por @Tunaki) e permite retornar o objeto removido ao chamador da função.

Li suas respostas que me sugerem escolher métodos seguros como em ifPresentvez de, getmas não encontro uma maneira de usá-los neste cenário.

Existe alguma desvantagem importante neste tipo de solução?

Edite os seguintes conselhos de @Holger

Esta deve ser a função que eu precisava

public ProducerDTO findAndRemove(String pod) {
    return IntStream.range(0, producersProcedureActive.size())
            .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))      
            .boxed()                                                                
            .findFirst()
            .map(i -> producersProcedureActive.remove((int)i))
            .orElseGet(() -> {
                logger.error("No producer found with POD [" + pod + "]"); 
                return null; 
            });
}
Marco Stramezzi
fonte
2
Você não deve usar gete capturar a exceção. Isso não é apenas um estilo ruim, mas também pode causar um desempenho ruim. A solução limpa é ainda mais simples,return /* stream operation*/.findFirst() .map(i -> producersProcedureActive.remove((int)i)) .orElseGet(() -> { logger.error("No producer found with POD [" + pod + "]"); return null; });
Holger
0

a tarefa é: obter ✶ e ✶ remover o elemento da lista

p.stream().collect( Collectors.collectingAndThen( Collector.of(
    ArrayDeque::new,
    (a, producer) -> {
      if( producer.getPod().equals( pod ) )
        a.addLast( producer );
    },
    (a1, a2) -> {
      return( a1 );
    },
    rslt -> rslt.pollFirst()
  ),
  (e) -> {
    if( e != null )
      p.remove( e );  // remove
    return( e );    // get
  } ) );
Kaplan
fonte