Лямбда-Java 8 отримує та видаляє елемент зі списку


88

Враховуючи список елементів, я хочу отримати елемент із заданою властивістю та видалити його зі списку. Найкраще рішення, яке я знайшов:

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

Чи можна поєднати get і remove у лямбда-виразі?


9
Це справді здається класичним випадком, коли замість цього просто використовувати цикл та ітератор.
chrisis

1
@chrylis Я люб'язно не погоджуюсь;) Ми настільки звикли до імперативного програмування, що будь-який інший спосіб звучить занадто екзотично. Уявіть, якби реальність була навпаки: ми дуже звикли до функціонального програмування, і до Java додана нова імперативна парадигма. Чи могли б ви сказати, що це буде класичний випадок для потоків, предикатів та опціоналів?
fps

9
Не дзвоніть get()сюди! Ви не уявляєте, порожній він чи ні. Ви викинете виняток, якщо елемента там не було. Натомість використовуйте один із безпечних методів, наприклад ifPresent, orElse, orElseGet або orElseThrow.
Брайан Гетц,

@FedericoPeraltaSchaffner Можливо, справа смаку. Але я також пропоную не поєднувати обидва. Коли я бачу, що якийсь код отримує значення за допомогою потоків, я зазвичай вважаю, що операції в потоці не мають побічних ефектів. Поєднання у видаленні елемента може призвести до появи коду, який може спричинити оманливий збір для читачів.
Маттіас Віммер,

Тільки для роз’яснення: Ви хочете видалити всі елементи, listдля яких Predicateістинно або лише перший (з, можливо, нуля, одного або багатьох елементів)?
Кедар Мхасвад

Відповіді:


148

Вилучити елемент зі списку

objectA.removeIf(x -> conditions);

наприклад:

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

ВИХІД: А Б В


я б запропонував це як найкращу відповідь
Сергій Паук

1
Це дуже акуратно. Можливо, вам доведеться реалізувати equals / hashCode, якщо це не зроблено для ваших власних об'єктів. (У цьому прикладі використовується рядок, який має їх за замовчуванням).
bram000

15
IMHO у відповіді не враховує частину "отримати": removeIfце елегантне рішення для видалення елементів із колекції, але воно не повертає видалений елемент.
Марко Страмецці

Це дуже простий режим для виключення об’єкта з java ArrayList. Дуже багато. Мені добре працювати.
Марсело

2
Це не відповідає на питання. Вимога полягає у тому, щоб вилучити елемент зі списку та отримати вилучений елемент / елементи до нового списку.
Шаніка Едірівера,

34

Незважаючи на те, що нитка досить стара, все ще думають, що вона пропонує рішення Java8.

Використовуйте removeIfфункцію. Складність часуO(n)

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

Посилання на API: removeIf docs

Припущення: producersProcedureActiveє aList

ПРИМІТКА. Завдяки такому підходу ви не зможете отримати утримання видаленого елемента.


На додаток до видалення елемента зі списку, OP все ще хоче посилання на елемент.
eee,

@eee: Велике спасибі, що вказали на це. Я пропустив цю частину з оригінального запитання О.П.
asifsid88

лише зауважимо, це видалить весь предмет, який відповідає умові. Але ОП, здається, потрібно видалити лише перший предмет (ОП використовував findFirst ())
nantitv

17

Подумайте про використання ванільних ітераторів Java для виконання завдання:

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

Переваги :

  1. Це просто і очевидно.
  2. Він проходить лише один раз і лише до відповідного елемента.
  3. Ви можете зробити це на будь-якому Iterableнавіть без stream()підтримки (принаймні тих, хто реалізує remove()на своєму ітераторі) .

Недоліки :

  1. Ви не можете зробити це на місці як єдиний вираз (потрібен допоміжний метод або змінна)

Що стосується

Чи можна поєднати get і remove у лямбда-виразі?

інші відповіді чітко показують, що це можливо, але ви повинні це знати

  1. Пошук і вилучення можуть пройти по списку двічі
  2. ConcurrentModificationException може бути викинуто під час вилучення елемента зі списку, що повторюється

4
Мені подобається це рішення, але зауважте, що він має один серйозний недолік, який ви пропустили: у багатьох реалізаціях Iterable є remove()методи, які кидають UOE. (Звичайно, не для колекцій JDK, але я вважаю несправедливим говорити "працює на будь-якому Iterable".)
Брайан Гетц,

Я думаю, ми могли б припустити, що якщо елемент можна взагалі видалити , його може видалити ітератор
Василь Лясковський

5
Ви могли б припустити, що, але, розглянувши сотню реалізацій ітераторів, це було б поганим припущенням. (Мені все ще подобається такий підхід; ви просто перепродаєте його.)
Брайан Гетц,

2
@Brian Goetz: defaultреалізація removeIfробить те саме припущення, але, звичайно, це визначається, Collectionа не Iterable...
Холгер

14

Прямим рішенням було б звернення ifPresent(consumer)до необов’язкового, поверненого findFirst(). Цей споживач буде викликаний, якщо опція не є порожньою. Перевага також полягає в тому, що він не видасть виняток, якщо операція пошуку повертає порожній необов’язковий елемент, як це робив би ваш поточний код; натомість нічого не станеться.

Якщо ви хочете , щоб повернути зняте значення, ви можете до результату виклику :mapOptionalremove

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

Але зауважте, що remove(Object)операція знову перегляне список, щоб знайти елемент для видалення. Якщо у вас є список із довільним доступом, наприклад an ArrayList, було б краще зробити Stream по індексах списку і знайти перший індекс, що відповідає предикату:

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

За допомогою цього рішення remove(int)операція працює безпосередньо над індексом.


3
Це патологічно для пов'язаного списку.
chrisis

1
@chrylis Рішення для індексу було б справді. Залежно від реалізації списку, один воліє один перед іншим. Зробив невелику редакцію.
Tunaki

1
@chrylis: у випадку, коли LinkedListви, можливо, не повинні використовувати API потоку, оскільки немає рішення без обходу принаймні двічі. Але я не знаю жодного реального сценарію, коли академічна перевага пов'язаного списку може компенсувати його фактичні накладні витрати. Тож просте рішення - ніколи не використовувати LinkedList.
Holger

2
О так багато редагувань ... тепер перше рішення не надає вилучений елемент, а remove(Object)лише повертає booleanповідомлення про те, чи був елемент для видалення чи ні.
Holger

3
@Marco Stramezzi: на жаль, коментар, що пояснює його, було видалено. Без boxed()вас отримати OptionalIntякий можна тільки mapвід intдо int. На відміну від цього IntStream, mapToObjметоду не існує . За допомогою boxed()ви отримаєте, Optional<Integer>що дозволяє до mapдовільного об'єкта, тобто ProducerDTOповертається remove(int). Акторський склад від Integerдо intнеобхідний для розмежування відмінностей між remove(int)і remove(Object).
Holger

9

Use може використовувати фільтр Java 8 і створити інший список, якщо ви не хочете змінювати старий список:

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

5

Я впевнений, що це буде непопулярна відповідь, але це працює ...

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

p[0] або буде містити знайдений елемент, або буде нульовим.

Тут «фішка» полягає в тому, щоб обійти проблему «ефективно остаточної», використовуючи посилання на масив, яке є фактично остаточним, але встановлюючи його перший елемент.


1
У цьому випадку це не так вже й погано, але не покращення можливості просто зателефонувати, .orElse(null)щоб отримати ProducerDTOабо null...
Холгер

У такому випадку може бути простіше просто мати .orElse(null)і мати if, ні?
Tunaki

@Holger, але як ти remove()також можеш викликати за допомогою orElse(null)?
Чеська

1
Просто використовуйте результат. if(p!=null) producersProcedureActive.remove(p);це все ще коротше, ніж лямбда-вираз у вашому ifPresentдзвінку.
Холгер

@holger Я інтерпретував мету запитання як уникання кількох тверджень - тобто 1-рядкове рішення
Богемський

4

З колекціями Eclipse ви можете використовувати detectIndexразом із remove(int)будь-яким 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);

Якщо ви використовуєте MutableListтип із колекцій Eclipse, ви можете викликати detectIndexметод безпосередньо зі списку.

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

Примітка: Я є учасником колекцій Eclipse


2

Коли ми хочемо отримати кілька елементів зі Списку в новий список (відфільтрувати за допомогою предиката) і видалити їх із існуючого списку , я ніде не міг знайти правильної відповіді.

Ось як ми можемо це зробити, використовуючи розділення 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);

Таким чином ви ефективно видаляєте відфільтровані елементи з вихідного списку та додаєте їх до нового списку.

Зверніться до 5.2. Колекціонери. Розділення за розділом цієї статті .


1

Як припускали інші, це може бути випадком використання для циклів та ітерацій. На мій погляд, це найпростіший підхід. Якщо ви хочете змінити список на місці, його все одно не можна вважати "справжнім" функціональним програмуванням. Але ви можете скористатися Collectors.partitioningBy()для того, щоб отримати новий список з елементами, які задовольняють ваші умови, і новий список тих, які цього не роблять. Звичайно, при такому підході, якщо у вас є кілька елементів, що задовольняють умові, всі вони будуть у цьому списку, а не лише перші.


Набагато краще фільтрувати потік і збирати результати до нового списку
fps

1

Наведена нижче логіка є рішенням без зміни вихідного списку

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

Поєднавши свою початкову ідею та ваші відповіді, я дійшов до того, що здається рішенням мого власного запитання:

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

Це дозволяє видалити об'єкт за допомогою, remove(int)який не перетинає список знову (як запропонував @Tunaki), і дозволяє повернути видалений об'єкт виклику функції.

Я прочитав ваші відповіді, які пропонують мені вибрати безпечні методи, наприклад ifPresentзамість, getале я не знаходжу способу використовувати їх у цьому сценарії.

Чи є у такого роду рішень важливі недоліки?

Редагуйте, дотримуючись порад @Holger

Це повинна бути функція, яка мені потрібна

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
Не слід використовувати getта ловити виняток. Це не тільки поганий стиль, але і може спричинити погану роботу. Чисте рішення ще простіше,return /* stream operation*/.findFirst() .map(i -> producersProcedureActive.remove((int)i)) .orElseGet(() -> { logger.error("No producer found with POD [" + pod + "]"); return null; });
Холгер

0

завдання: отримати ✶ та ✶ вилучити елемент зі списку

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
  } ) );
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.