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

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


Помимо удаления элемента из списка, 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(), чтобы получить новый список с элементами, которые удовлетворяют вашему условию, и новый список тех, которые не удовлетворяют. Конечно, при таком подходе, если у вас есть несколько элементов, удовлетворяющих условию, все они будут в этом списке, а не только первым.


Намного лучше отфильтровать поток и собрать результаты в новый список
— 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.