Java 8 lambda ottiene e rimuove l'elemento dall'elenco


91

Dato un elenco di elementi, voglio ottenere l'elemento con una determinata proprietà e rimuoverlo dall'elenco. La migliore soluzione che ho trovato è:

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

È possibile combinare get e remove in un'espressione lambda?


9
Questo sembra davvero un classico caso in cui usare solo un ciclo e un iteratore.
chrylis -cautiouslyoptimistic-

1
@chrylis Gentilmente non sono d'accordo;) Siamo così abituati alla programmazione imperativa, che qualsiasi altro modo suona troppo esotico. Immagina se la realtà fosse il contrario: siamo molto abituati alla programmazione funzionale e un nuovo paradigma imperativo viene aggiunto a Java. Diresti che questo sarebbe il caso classico di stream, predicati e optionals?
fps

9
Non chiamare get()qui! Non hai idea se sia vuoto o no. Genererai un'eccezione se l'elemento non era presente. Utilizza invece uno dei metodi sicuri come ifPresent, orElse, orElseGet o orElseThrow.
Brian Goetz,

@FedericoPeraltaSchaffner Probabilmente una questione di gusti. Ma proporrei anche di non combinare entrambi. Quando vedo del codice che ottiene un valore utilizzando i flussi, normalmente presumo che le operazioni nel flusso siano prive di effetti collaterali. Mescolando la rimozione dell'elemento potrebbe risultare in codice che può essere fuorviante per i lettori.
Matthias Wimmer

Giusto per chiarire: vuoi rimuovere tutti gli elementi listper i quali Predicateè vero o solo il primo (di uno eventualmente zero, uno o più elementi)?
Kedar Mhaswade,

Risposte:


154

Per rimuovere un elemento dall'elenco

objectA.removeIf(x -> conditions);

per esempio:

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

USCITA: A B C


Suggerirei questa come la migliore risposta
Sergey Pauk

1
Questo è molto accurato. Potrebbe essere necessario implementare equals / hashCode se questo non viene fatto per i propri oggetti. (In questo esempio viene utilizzata la stringa che li ha per impostazione predefinita).
bram000

17
IMHO la risposta non considera la parte "get": removeIfè una soluzione elegante per rimuovere elementi da una collezione, ma non restituisce l'elemento rimosso.
Marco Stramezzi

È una modalità molto semplice per escludere un oggetto da java ArrayList. Pensa molto. Funziona bene per me.
Marcelo Rebouças

3
Questo non risponde alla domanda. Il requisito è rimuovere l'elemento dall'elenco e ottenere l'elemento / gli elementi rimossi in un nuovo elenco.
Shanika Ediriweera

35

Sebbene il thread sia piuttosto vecchio, si pensa ancora di fornire una soluzione: utilizzando Java8.

Usa la removeIffunzione. La complessità del tempo èO(n)

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

Riferimento API: removeIf docs

Presupposto: producersProcedureActiveè unList

NOTA: con questo approccio non sarai in grado di ottenere il blocco dell'elemento eliminato.


Oltre a cancellare l'elemento dalla lista, l'OP vuole ancora un riferimento all'elemento.
eee

@eee: Grazie mille per averlo sottolineato. Mi mancava quella parte della domanda originale di OP.
asifsid88

solo per notare che questo rimuoverà tutti gli elementi che corrispondono alla condizione. Ma OP sembra aver bisogno di rimuovere solo il primo elemento (OP ha usato findFirst ())
nantitv

18

Prendi in considerazione l'utilizzo di iteratori java vanilla per eseguire l'attività:

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

Vantaggi :

  1. È chiaro e ovvio.
  2. Attraversa solo una volta e solo fino all'elemento corrispondente.
  3. Puoi farlo su qualsiasi Iterableanche senza stream()supporto (almeno quelli che implementano remove()sul loro iteratore) .

Svantaggi :

  1. Non puoi farlo sul posto come una singola espressione (metodo ausiliario o variabile richiesta)

Per quanto riguarda la

È possibile combinare get e remove in un'espressione lambda?

altre risposte mostrano chiaramente che è possibile, ma dovresti esserne consapevole

  1. La ricerca e la rimozione possono attraversare l'elenco due volte
  2. ConcurrentModificationException può essere lanciato quando si rimuove un elemento dall'elenco che viene iterato

5
Mi piace questa soluzione, ma nota che ha un grave svantaggio che ti sei perso: molte implementazioni Iterable hanno remove()metodi che generano UOE. (Non quelli per le raccolte JDK, ovviamente, ma penso che sia ingiusto dire "funziona su qualsiasi Iterable".)
Brian Goetz,

Penso che potremmo supporre che se un elemento può essere rimosso , in generale , può essere rimosso dal iteratore
Vasily Liaskovsky

5
Si potrebbe presumere che, ma dopo aver esaminato centinaia di implementazioni di iteratori, sarebbe una cattiva supposizione. (Mi piace ancora l'approccio; lo stai solo vendendo troppo.)
Brian Goetz,

2
@Brian Goetz: l' defaultimplementazione di removeIffa lo stesso presupposto, ma, ovviamente, è definita Collectionpiuttosto che Iterable...
Holger

14

La soluzione diretta sarebbe invocare ifPresent(consumer)sull'Opzionale restituito dafindFirst() . Questo consumatore verrà richiamato quando l'opzionale non è vuoto. Il vantaggio è anche che non genererà un'eccezione se l'operazione find restituisse un opzionale vuoto, come farebbe il codice corrente; invece, non accadrà nulla.

Se si desidera restituire il valore rimosso, è possibile mapil Optionalper il risultato della chiamata remove:

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

Ma si noti che l' remove(Object)operazione attraverserà nuovamente l'elenco per trovare l'elemento da rimuovere. Se hai una lista con accesso casuale, come un ArrayList, sarebbe meglio creare uno Stream sugli indici della lista e trovare il primo indice che corrisponde al predicato:

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

Con questa soluzione l' remove(int)operazione opera direttamente sull'indice.


3
Questo è patologico per un elenco collegato.
chrylis -cautiouslyoptimistic-

1
@chrylis La soluzione dell'indice sarebbe davvero. A seconda dell'implementazione dell'elenco, uno preferirebbe uno sull'altro. Ho apportato una piccola modifica.
Tunaki

1
@chrylis: in caso di a LinkedList, forse non dovresti usare l'API di flusso poiché non c'è soluzione senza attraversare almeno due volte. Ma non conosco nessuno scenario di vita reale in cui il vantaggio accademico di un elenco collegato possa compensare il suo effettivo sovraccarico. Quindi la soluzione semplice è non usare mai LinkedList.
Holger

2
Oh tante modifiche ... ora la prima soluzione non fornisce l'elemento rimosso in quanto remove(Object)restituisce solo un booleandire se c'era un elemento da rimuovere o meno.
Holger

3
@Marco Stramezzi: purtroppo il commento che lo spiega è stato rimosso. Senza di boxed()te ottieni un OptionalIntche può solo mapda inta int. A differenza IntStream, non esiste un mapToObjmetodo. Con boxed(), otterrai un Optional<Integer>che permette dimap a un oggetto arbitrario, cioè il ProducerDTOrestituito da remove(int). Il cast da Integera intè necessario per chiarire le ambiguità tra remove(int)e remove(Object).
Holger

9

L'utente può utilizzare il filtro di Java 8 e creare un altro elenco se non si desidera modificare il vecchio elenco:

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

5

Sono sicuro che questa sarà una risposta impopolare, ma funziona ...

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

p[0] o conterrà l'elemento trovato o sarà nullo.

Il "trucco" qui è aggirare il problema "effettivamente finale" utilizzando un riferimento di array che è effettivamente definitivo, ma impostando il suo primo elemento.


1
In questo caso, non è così male, ma non è un miglioramento rispetto alla possibilità di chiamare solo .orElse(null)per ottenere il ProducerDTOo null...
Holger,

In tal caso, potrebbe essere più facile avere .orElse(null)e avere un if, no?
Tunaki

@Holger ma come puoi invocare remove()anche tu usando orElse(null)?
Bohemian

1
Usa solo il risultato. if(p!=null) producersProcedureActive.remove(p);è ancora più breve dell'espressione lambda nella tua ifPresentchiamata.
Holger

@holger Ho interpretato l'obiettivo della domanda come evitare affermazioni multiple, ovvero una soluzione di 1 riga
Bohemian

4

Con le collezioni Eclipse puoi usare detectIndexinsieme a remove(int)qualsiasi 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 utilizzi il MutableListtipo da Eclipse Collections, puoi chiamare il detectIndexmetodo direttamente nell'elenco.

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: sono un committer per le collezioni Eclipse


2

Quando vogliamo ottenere più elementi da un elenco in un nuovo elenco (filtrare utilizzando un predicato) e rimuoverli dall'elenco esistente , non sono riuscito a trovare una risposta corretta da nessuna parte.

Ecco come possiamo farlo utilizzando il partizionamento 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);

In questo modo rimuovi efficacemente gli elementi filtrati dall'elenco originale e li aggiungi a un nuovo elenco.

Fare riferimento alla sezione 5.2. Collectors.partitioning Dalla sezione di questo articolo .


1

Come altri hanno suggerito, questo potrebbe essere un caso d'uso per cicli e iterabili. Secondo me, questo è l'approccio più semplice. Se si desidera modificare l'elenco sul posto, non può essere considerato comunque una programmazione funzionale "reale". Ma potresti usarlo Collectors.partitioningBy()per ottenere un nuovo elenco con elementi che soddisfano la tua condizione e un nuovo elenco di quelli che non lo fanno. Ovviamente con questo approccio, se hai più elementi che soddisfano la condizione, saranno tutti in quell'elenco e non solo il primo.


Molto meglio filtrare lo streaming e raccogliere i risultati in un nuovo elenco
fps

1

La logica seguente è la soluzione senza modificare l'elenco originale

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"]

0

Combinando la mia idea iniziale e le tue risposte ho raggiunto quella che sembra essere la soluzione alla mia stessa domanda:

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

Consente di rimuovere l'oggetto utilizzando remove(int)che non attraversa nuovamente l'elenco (come suggerito da @Tunaki) e consente di restituire l'oggetto rimosso al chiamante della funzione.

Ho letto le tue risposte che mi suggeriscono di scegliere metodi sicuri come ifPresentinvece diget ma non trovo un modo per usarli in questo scenario.

Ci sono importanti inconvenienti in questo tipo di soluzione?

Modifica seguendo i consigli di @Holger

Questa dovrebbe essere la funzione di cui avevo bisogno

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

2
Non dovresti usare gete catturare l'eccezione. Non è solo un cattivo stile, ma può anche causare cattive prestazioni. La soluzione pulita è ancora più semplice,return /* stream operation*/.findFirst() .map(i -> producersProcedureActive.remove((int)i)) .orElseGet(() -> { logger.error("No producer found with POD [" + pod + "]"); return null; });
Holger

0

il compito è: ottenere ✶ e ✶ rimuovere l'elemento dalla 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
  } ) );
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.