Java 8 lambda pobiera i usuwa element z listy

88

Mając listę elementów, chcę pobrać element o danej właściwości i usunąć go z listy. Najlepsze rozwiązanie jakie znalazłem to:

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

Czy można łączyć get i remove w wyrażeniu lambda?

Marco Stramezzi
źródło
9
To naprawdę wygląda na klasyczny przypadek, kiedy zamiast tego po prostu użyć pętli i iteratora.
chrylis
1
@chrylis Nie zgadzam się;) Jesteśmy tak przyzwyczajeni do imperatywnego programowania, że ​​każdy inny sposób brzmi zbyt egzotycznie. Wyobraź sobie, że rzeczywistość byłaby odwrotna: jesteśmy bardzo przyzwyczajeni do programowania funkcjonalnego i do Javy dodano nowy imperatywny paradygmat. Czy powiedziałbyś, że byłby to klasyczny przypadek dla strumieni, predykatów i opcji?
fps
9
Nie dzwoń get()tutaj! Nie masz pojęcia, czy jest pusty, czy nie. Rzucisz wyjątek, jeśli elementu tam nie było. Zamiast tego użyj jednej z bezpiecznych metod, takich jak ifPresent, orElse, orElseGet lub orElseThrow.
Brian Goetz
@FedericoPeraltaSchaffner Prawdopodobnie kwestia gustu. Ale proponuję też, aby nie łączyć obu. Kiedy widzę, że jakiś kod uzyskuje wartość za pomocą strumieni, zwykle zakładam, że operacje w strumieniu są wolne od skutków ubocznych. Mieszanie w usuwaniu elementu może spowodować, że kod może być mylący dla czytelników.
Matthias Wimmer
Dla wyjaśnienia: Czy chcesz usunąć wszystkie elementy w elemencie, listdla których Predicatejest prawdziwe, czy tylko pierwszy (z prawdopodobnie zera, jednego lub wielu elementów)?
Kedar Mhaswade

Odpowiedzi:

148

Aby usunąć element z listy

objectA.removeIf(x -> conditions);

na przykład:

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

WYJŚCIE: A B C

uma shankar
źródło
sugerowałbym to jako najlepszą odpowiedź
Sergey Pauk
1
To jest bardzo fajne. Może być konieczne zaimplementowanie equals / hashCode, jeśli nie jest to zrobione dla twoich własnych obiektów. (W tym przykładzie jest używany ciąg znaków, który ma je domyślnie).
bram000
15
IMHO odpowiedź nie uwzględnia części „pobierz”: removeIfjest eleganckim rozwiązaniem do usuwania elementów z kolekcji, ale nie zwraca usuniętego elementu.
Marco Stramezzi
Jest to bardzo prosty tryb wykluczania obiektu z java ArrayList. Wielkie dzięki. Dla mnie działa dobrze.
Marcelo Rebouças
2
To nie odpowiada na pytanie. Wymagane jest usunięcie elementu z listy i przeniesienie usuniętej pozycji / pozycji na nową listę.
Shanika Ediriweera
34

Chociaż wątek jest dość stary, nadal uważa się, że zapewnia rozwiązanie - użycie Java8.

Skorzystaj z removeIffunkcji. Czas jest złożonyO(n)

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

Dokumentacja API: removeIf docs

Założenie: producersProcedureActivejestList

UWAGA: Przy takim podejściu nie będziesz w stanie przechwycić usuniętego elementu.

asifsid88
źródło
Oprócz usunięcia elementu z listy PO nadal chce mieć odniesienie do elementu.
eee
@eee: Wielkie dzięki za zwrócenie na to uwagi. Brakowało mi tej części z pierwotnego pytania OP.
asifsid88
wystarczy zauważyć, że usunie to cały element, który pasuje do warunku. Ale wydaje się, że OP musi usunąć tylko pierwszą pozycję (OP użył findFirst ())
nantitv
17

Rozważ użycie podstawowych iteratorów Java do wykonania zadania:

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

Zalety :

  1. To jest jasne i oczywiste.
  2. Przechodzi tylko raz i tylko do pasującego elementu.
  3. Możesz to zrobić na dowolnym, Iterablenawet bez stream()wsparcia (przynajmniej te implementujące remove()na swoim iteratorze) .

Wady :

  1. Nie można tego zrobić w miejscu jako pojedynczego wyrażenia (wymagana jest metoda pomocnicza lub zmienna)

Jeśli chodzi o

Czy można łączyć get i remove w wyrażeniu lambda?

inne odpowiedzi jasno pokazują, że jest to możliwe, ale powinieneś być tego świadomy

  1. Wyszukiwanie i usuwanie może dwukrotnie przechodzić przez listę
  2. ConcurrentModificationException może zostać wyrzucony podczas usuwania elementu z iterowanej listy
Wasilij Liaskovsky
źródło
4
Podoba mi się to rozwiązanie, ale zwróć uwagę, że ma ono jedną poważną wadę, którą przeoczyłeś: wiele iterowalnych implementacji ma remove()metody, które rzucają UOE. (Oczywiście nie te dla kolekcji JDK, ale myślę, że niesprawiedliwe jest powiedzenie „działa na dowolnych Iterowalnych”).
Brian Goetz,
Myślę, że moglibyśmy założyć, że jeśli element można ogólnie usunąć , można go usunąć za pomocą iteratora
Wasilij Liaskovsky
5
Można by to założyć, ale patrząc na setki implementacji iteratorów, byłoby to złe założenie. (Nadal podoba mi się to podejście; po prostu je sprzedajesz.)
Brian Goetz,
2
@Brian Goetz: defaultimplementacja removeIfprzyjmuje takie samo założenie, ale oczywiście jest zdefiniowana Collectionraczej niż Iterable
Holger
13

Bezpośrednim rozwiązaniem byłoby wywołanie ifPresent(consumer)opcjonalnego zwróconego przez findFirst(). Ten konsument zostanie wywołany, gdy opcja nie jest pusta. Zaletą jest również to, że nie zgłosi wyjątku, jeśli operacja find zwróci pusty element opcjonalny, tak jak zrobiłby to twój obecny kod; zamiast tego nic się nie stanie.

Jeśli chcesz zwrócić wartość usunięta, możesz do wyniku powołania :mapOptionalremove

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

Pamiętaj jednak, że remove(Object)operacja ponownie przeszuka listę, aby znaleźć element do usunięcia. Jeśli masz listę z dostępem losowym, taką jak an ArrayList, lepiej byłoby utworzyć strumień na indeksach listy i znaleźć pierwszy indeks pasujący do predykatu:

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

Dzięki temu rozwiązaniu remove(int)operacja działa bezpośrednio na indeksie.

Tunaki
źródło
3
Jest to patologiczne w przypadku listy połączonej.
chrylis
1
@chrylis Rozwiązanie indeksu byłoby rzeczywiście. W zależności od implementacji listy, jeden wolałby jeden od drugiego. Dokonałem małej edycji.
Tunaki,
1
@chrylis: w przypadku LinkedListnie powinieneś używać API strumienia, ponieważ nie ma rozwiązania bez przejścia co najmniej dwa razy. Ale nie znam żadnego scenariusza z prawdziwego życia, w którym akademicka przewaga połączonej listy mogłaby zrekompensować jej rzeczywisty narzut. Więc prostym rozwiązaniem jest nigdy nie używać LinkedList.
Holger,
2
O, tak wiele zmian… teraz pierwsze rozwiązanie nie zapewnia usuniętego elementu, ponieważ remove(Object)zwraca tylko booleaninformację, czy istnieje element do usunięcia, czy nie.
Holger,
3
@Marco Stramezzi: niestety komentarz wyjaśniający został usunięty. Bez boxed()ciebie możesz uzyskać OptionalInttylko mapod intdo int. W przeciwieństwie do tego IntStreamnie ma mapToObjmetody. Z boxed(), dostaniesz, Optional<Integer>który pozwala na mapdowolny obiekt, tj. ProducerDTOZwrócony przez remove(int). Rzut od Integerdo intjest niezbędny do ujednoznacznienia między remove(int)a remove(Object).
Holger
9

Użyj filtru Java 8 i utwórz kolejną listę, jeśli nie chcesz zmieniać starej listy:

List<ProducerDTO> result = producersProcedureActive
                            .stream()
                            .filter(producer -> producer.getPod().equals(pod))
                            .collect(Collectors.toList());
Toi Nguyen
źródło
5

Jestem pewien, że będzie to niepopularna odpowiedź, ale działa ...

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

p[0] albo będzie zawierał znaleziony element, albo będzie pusty.

„Sztuczka” polega tutaj na obejściu problemu „efektywnie ostatecznego” przez użycie odwołania do tablicy, które jest faktycznie ostateczne, ale ustawia swój pierwszy element.

Czeski
źródło
1
W tym przypadku nie jest tak źle, ale nie jest to poprawa w stosunku do możliwości zadzwonienia po prostu, .orElse(null)aby odebrać ProducerDTOlub null
Holger
W takim razie może być łatwiej po prostu mieć .orElse(null)i mieć if, nie?
Tunaki,
@Holger, ale jak możesz wywołać remove()również za pomocą orElse(null)?
Bohemian
1
Po prostu użyj wyniku. if(p!=null) producersProcedureActive.remove(p);to wciąż krócej niż wyrażenie lambda w ifPresentwywołaniu.
Holger,
@holger Zinterpretowałem cel pytania jako uniknięcie wielokrotnych stwierdzeń - tj. 1-wierszowe rozwiązanie
Bohemian
4

Z Eclipse Kolekcje można używać detectIndexrazem z remove(int)na dowolnym 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);

Jeśli używasz MutableListtypu z kolekcji Eclipse, możesz wywołać detectIndexmetodę bezpośrednio z listy.

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

Uwaga: jestem promotorem Eclipse Collections

Donald Raab
źródło
2

Kiedy chcemy przenieść wiele elementów z listy do nowej listy (filtrować za pomocą predykatu) i usunąć je z istniejącej listy , nigdzie nie mogłem znaleźć poprawnej odpowiedzi.

Oto, jak możemy to zrobić za pomocą partycjonowania Java Streaming API.

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

W ten sposób skutecznie usuniesz przefiltrowane elementy z oryginalnej listy i dodasz je do nowej listy.

Patrz 5.2. Kolektory. Partycjonowanie Według sekcji tego artykułu .

Shanika Ediriweera
źródło
1

Jak sugerowali inni, może to być przypadek użycia dla pętli i elementów iteracyjnych. Moim zdaniem to najprostsze podejście. Jeśli chcesz zmodyfikować listę w miejscu, i tak nie można jej uznać za „rzeczywiste” programowanie funkcjonalne. Ale możesz użyć Collectors.partitioningBy(), aby uzyskać nową listę z elementami, które spełniają twój warunek, i nową listę tych, które nie spełniają. Oczywiście przy takim podejściu, jeśli masz wiele elementów spełniających warunek, wszystkie będą na tej liście, a nie tylko na pierwszym.

user140547
źródło
Znacznie lepiej jest filtrować strumień i zbierać wyniki do nowej listy
fps
1

Poniższa logika jest rozwiązaniem bez modyfikowania oryginalnej listy

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
źródło
0

Łącząc mój wstępny pomysł i Twoje odpowiedzi, doszedłem do tego, co wydaje się być odpowiedzią na moje własne pytanie:

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

Pozwala usunąć obiekt za pomocą remove(int)tego, że nie przechodzi ponownie przez listę (zgodnie z sugestią @Tunaki) i pozwala zwrócić usunięty obiekt do obiektu wywołującego funkcję.

Czytałem twoje odpowiedzi, które sugerują mi wybór bezpiecznych metod, takich jak ifPresentzamiast, getale nie znajduję sposobu, aby ich użyć w tym scenariuszu.

Czy jest jakaś istotna wada tego rodzaju rozwiązania?

Edytuj następujące porady @Holger

To powinna być funkcja, której potrzebowałem

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
źródło
2
Nie powinieneś używać geti łapać wyjątku. To nie tylko zły styl, ale może również powodować złe wyniki. Czyste rozwiązanie jest jeszcze prostsze,return /* stream operation*/.findFirst() .map(i -> producersProcedureActive.remove((int)i)) .orElseGet(() -> { logger.error("No producer found with POD [" + pod + "]"); return null; });
Holger
0

zadaniem jest: pobrać ✶ i ✶ usunąć element z listy

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
źródło