Java 8 лямбда получить и удалить элемент из списка

88

Учитывая список элементов, я хочу получить элемент с заданным свойством и удалить его из списка. Лучшее решение, которое я нашел:

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

Можно ли комбинировать получение и удаление в лямбда-выражении?

Марко Страмецци
источник
9
Это действительно похоже на классический случай, когда вместо этого просто использовать цикл и итератор.
chrylis -cautiouslyoptimistic-
1
@chrylis Я не согласен;) Мы настолько привыкли к императивному программированию, что любой другой способ звучит слишком экзотично. Представьте себе, если бы в действительности было наоборот: мы очень привыкли к функциональному программированию, и в Java добавляется новая императивная парадигма. Вы бы сказали, что это классический случай для потоков, предикатов и опций?
fps,
9
Не звоните get()сюда! Вы не представляете, пустой он или нет. Вы вызовете исключение, если элемента не было. Вместо этого используйте один из безопасных методов, например ifPresent, orElse, orElseGet или orElseThrow.
Брайан Гетц,
@FedericoPeraltaSchaffner Наверное, дело вкуса. Но я бы также предложил не совмещать то и другое. Когда я вижу, что какой-то код получает значение с использованием потоков, я обычно предполагаю, что операции в потоке не имеют побочных эффектов. Смешанное удаление элемента может привести к появлению кода, который может ввести читателей в заблуждение.
Маттиас Виммер,
Просто чтобы уточнить: вы хотите удалить все элементы в, listдля которых Predicateистинно, или только первый (из, возможно, нуля, одного или нескольких элементов)?
Kedar Mhaswade

Ответы:

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

ВЫХОД: A B C

Ума Шанкар
источник
Я бы посоветовал это как лучший ответ
Сергей Паук
1
Это очень аккуратно. Вам может потребоваться реализовать equals / hashCode, если это не сделано для ваших собственных объектов. (В этом примере используется строка, у которой они есть по умолчанию).
bram000 01
15
IMHO ответ не учитывает часть «получить»: removeIfэто элегантное решение для удаления элементов из коллекции, но оно не возвращает удаленный элемент.
Марко Страмецци
Это очень простой режим для исключения объекта из java ArrayList. Большое спасибо. Мне хорошо работать.
Марсело Ребусас
2
Это не отвечает на вопрос. Требуется удалить элемент из списка и перенести удаленный элемент / элементы в новый список.
Shanika Ediriweera
34

Хотя поток довольно старый, все еще думается, что решение - использование Java8.

Используйте removeIfфункцию. Сложность времени составляетO(n)

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

Справочник по API: removeIf docs

Предположение: producersProcedureActiveэтоList

ПРИМЕЧАНИЕ. При таком подходе вы не сможете получить удаленный элемент.

asifsid88
источник
Помимо удаления элемента из списка, OP по-прежнему требует ссылки на элемент.
eee
@eee: Большое спасибо, что указали на это. Я пропустил эту часть из исходного вопроса OP.
asifsid88 02
просто отметить, что это удалит все элементы, соответствующие условию. Но OP, похоже, должен удалить только первый элемент (OP использовал 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. Вы не можете сделать это на месте как одно выражение (требуется вспомогательный метод или переменная)

Для

Можно ли комбинировать получение и удаление в лямбда-выражении?

другие ответы ясно показывают, что это возможно, но вы должны знать

  1. Поиск и удаление могут проходить по списку дважды
  2. ConcurrentModificationException может быть выброшено при удалении элемента из повторяемого списка
Василий Лясковский
источник
4
Мне нравится это решение, но обратите внимание, что у него есть один серьезный недостаток, который вы упустили: многие реализации Iterable имеют remove()методы, которые вызывают UOE. (Конечно, не для коллекций JDK, но я считаю несправедливым сказать «работает на любом Iterable».)
Брайан Гетц
Я думаю, мы можем предположить, что если элемент можно удалить вообще , он может быть удален итератором
Василий Лясковский
5
Вы могли предположить это, но, просмотрев сотни реализаций итераторов, это было бы плохим предположением. (Мне все еще нравится этот подход; вы просто его переоцениваете.)
Брайан Гетц,
2
@Brian Goetz: defaultреализация removeIfделает то же предположение, но, конечно же, оно определено, Collectionа не Iterable
Holger
13

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

Если вы хотите вернуть удаленное значение, вы можете mapOptional к результату вызова remove:

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

Но обратите внимание, что remove(Object) операция снова будет проходить по списку, чтобы найти элемент, который нужно удалить. Если у вас есть список с произвольным доступом, например 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
Это патология для связного списка.
chrylis -cautiouslyoptimistic-
1
@chrylis Индексное решение действительно было бы. В зависимости от реализации списка один предпочтительнее другого. Сделал небольшую правку.
Tunaki
1
@chrylis: в случае a 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).
Хольгер
9

Используйте фильтр 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] будет либо содержать найденный элемент, либо иметь значение NULL.

«Уловка» здесь состоит в том, чтобы обойти проблему «фактически окончательного», используя ссылку на массив, которая фактически является окончательной, но устанавливает ее первый элемент.

Богемный
источник
1
В данном случае это не так уж плохо, но это не улучшение по сравнению с возможностью просто позвонить, .orElse(null)чтобы получить ProducerDTOили null
Хольгер
В таком случае может быть проще просто иметь .orElse(null)и иметь if, не так ли?
Tunaki
@Holger, но как вы remove()тоже можете вызывать с помощью orElse(null)?
Bohemian
1
Просто используйте результат. if(p!=null) producersProcedureActive.remove(p);это все еще короче, чем лямбда-выражение в вашем ifPresentвызове.
Holger
@holger Я истолковал цель вопроса как избегание множественных утверждений, то есть однострочное решение
Bohemian
4

С Затмением коллекциями вы можете использовать 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 Collections, вы можете вызвать 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. Collectors.partitioning По разделу этой статьи .

Шаника Эдиривира
источник
1

Как предполагали другие, это может быть вариант использования циклов и итераций. На мой взгляд, это самый простой подход. Если вы хотите изменить список на месте, это все равно нельзя считать «настоящим» функциональным программированием. Но вы можете использовать Collectors.partitioningBy(), чтобы получить новый список с элементами, которые удовлетворяют вашему условию, и новый список тех, которые не удовлетворяют. Конечно, при таком подходе, если у вас есть несколько элементов, удовлетворяющих условию, все они будут в этом списке, а не только первым.

user140547
источник
Намного лучше отфильтровать поток и собрать результаты в новый список
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"]
KimchiMan
источник
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
  } ) );
Каплан
источник