Java 8 Lambda表达式:获取并从列表中删除元素

131

给定一组元素,我想要获取具有特定属性的元素并且从列表中删除它。我发现的最佳解决方案是:

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

能否在Lambda表达式中结合get和remove方法?


9
这似乎是一个很经典的情况,只需要使用循环和迭代器即可。 - chrylis -cautiouslyoptimistic-
2
@chrylis 我有点不同意 ;) 我们太习惯命令式编程了,以至于其他任何方式都听起来太奇异了。想象一下,如果现实情况是相反的:我们非常习惯函数式编程,并且在Java中添加了一种新的命令式范例。你会说这是否是流、谓词和可选项的经典案例呢? - fps
11
不要在这里调用 get()!你无法确定它是否为空。如果元素不存在,这将抛出异常。相反,使用其中一个安全方法,例如 ifPresentorElseorElseGetorElseThrow - Brian Goetz
1
只是为了澄清:您想删除list中谓词为真的所有元素,还是仅删除第一个(可能是零个、一个或多个元素)? - Kedar Mhaswade
@FedericoPeraltaSchaffner 这与风格偏好无关;我喜欢lambda,无论是Java 8还是Groovy。但在这种情况下,迭代API提供了一种效率高的操作,而流无法使用。这只是一个API的缺陷。 - chrylis -cautiouslyoptimistic-
显示剩余4条评论
14个回答

233

从列表中删除元素

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
我建议这个作为最佳答案。 - S. Pauk
1
这非常整洁。如果您自己的对象没有实现equals/hashCode,那么您可能需要实现它们。(在此示例中使用了默认情况下具有它们的String)。 - bram000
37
在我看来,这个答案没有考虑“获取”部分:“removeIf”是从集合中移除元素的一种优雅解决方案,但它不会返回被移除的元素。 - Marco Stramezzi
这是一种非常简单的方法,可以从Java ArrayList中排除一个对象。非常感谢。对我很有效。 - Marcelo Rebouças
FYI,removeIf 的时间复杂度为 O(n)。来源:https://www.javabrahman.com/java-8/java-8-collection-removeif-method-tutorial-with-examples/ - 50shadesofbae
16
这并没有回答这个问题。要求从列表中移除元素,并将被移除的项/项添加到一个新列表中。 - Shanika Ediriweera

41

虽然这个线程已经很旧了,但我仍然想提供一个解决方案 - 使用Java8

使用removeIf函数。时间复杂度为O(n)

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

API 参考:removeIf 文档

假设: producersProcedureActive 是一个 List

注意:使用此方法将无法获取已删除的项目。


2
除了从列表中删除元素之外,操作者仍然希望引用该元素。 - eee
@eee:非常感谢您指出这一点。我错过了OP原始问题中的那部分内容。 - asifsid88
2
只是需要注意的是,这将删除所有与条件匹配的项目。但是,OP似乎只需要删除第一个项目(OP使用了findFirst())。 - nantitv
1
@asifsid88,你怎么获取已删除的元素?标题不是“获取并删除”吗? - horvoje

26

考虑使用原生的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. 您不能将其作为单个表达式直接执行(需要辅助方法或变量)

至于

是否可能在lambda表达式中结合get和remove?

其他答案清楚地表明这是可能的,但您应该注意以下问题

  1. 搜索和删除可能会遍历列表两次
  2. 从正在迭代的列表中删除元素时可能会抛出ConcurrentModificationException

5
我喜欢这个解决方案,但请注意一个严重的缺点,即您忽略了许多Iterable实现具有抛出UOE的remove()方法(当然,并非针对JDK集合的这些实现,但我认为说“适用于任何Iterable”是不公平的)。 - Brian Goetz
我认为我们可以假设,如果一个元素在一般情况下可以被移除,那么它也可以通过迭代器被移除。 - Vasily Liaskovsky
6
你可以这样认为,但是在查看了数百个迭代器实现之后,这将是一个错误的假设。(我仍然喜欢这种方法;只是你夸大了它的价值。) - Brian Goetz
2
@Brian Goetz:removeIfdefault 实现做出了相同的假设,但是它当然是在 Collection 上定义而不是 Iterable... - Holger

21

可以直接调用findFirst()返回的Optional上的ifPresent(consumer)方法。当Optional不为空时,将调用此Consumer。另外一个好处是,如果查找操作返回了一个空的Optional,这种方式也不会抛出异常,与您当前的代码不同; 相反,什么都不会发生。

如果要返回已删除的值,可以对Optional进行map操作,将其映射为调用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: 对于一个LinkedList,也许你不应该使用流API,因为没有解决方案可以只遍历两次。但是我不知道任何现实场景下链表的学术优势能够弥补其实际开销。所以简单的解决方案就是永远不要使用LinkedList - Holger
2
哦,编辑了这么多...现在第一个解决方案不提供已删除的元素,因为remove(Object)仅返回一个布尔值,指示是否有要删除的元素。 - Holger
3
很遗憾,解释它的评论已被删除。如果没有使用 boxed(),你会得到一个 OptionalInt,它只能从 int 映射到 int。与 IntStream 不同,它没有 mapToObj 方法。使用 boxed(),你将获得一个 Optional<Integer>,允许映射到任意对象,即 remove(int) 返回的 ProducerDTO。从 Integerint 的强制转换是必要的,以消除 remove(int)remove(Object) 之间的歧义。 - Holger
显示剩余4条评论

12

您可以使用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]将保存找到的元素或为空。

这里的“技巧”是通过使用一个有效地final的数组引用来绕过“有效地final”问题,但设置其第一个元素。


1
在这种情况下,情况并不那么糟糕,但与仅调用 .orElse(null) 来获取 ProducerDTOnull 的可能性相比并没有改进... - Holger
在这种情况下,只使用 .orElse(null) 并带有一个 if 可能会更容易,不是吗? - Tunaki
@Holger,但是你如何通过使用orElse(null)来调用remove()呢? - Bohemian
1
只需使用结果。if(p!=null) producersProcedureActive.remove(p); 这仍然比您的 ifPresent 调用中的 lambda 表达式更短。 - Holger
@holger 我理解这个问题的目标是避免多个语句 - 也就是一行代码的解决方案。 - Bohemian
@Bohemian:我总是觉得“一行代码”的伸展非常有趣。你发布的代码跨越了六行,但是,如果我们将“一行”解释为“一个语句”,你仍然没有这个,因为你需要数组声明,加上流语句,再加上从数组中提取实际值的表达式。我不认为这比单个流语句加上单个if语句更少。 - Holger

4

使用Eclipse Collections,您可以在任何java.util.List上使用detectIndexremove(int)

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

如果您使用Eclipse Collections中的MutableList类型,您可以直接在列表上调用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 Collections 的提交者。

2
以下逻辑是在不修改原始列表的情况下提供的解决方案。
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"]

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.partitioningBy部分,了解更多信息。

原始答案:最初的回答


1
resumoRemessaPorInstrucoes.removeIf(item -> 
            item.getTipoOcorrenciaRegistro() == TipoOcorrenciaRegistroRemessa.PEDIDO_PROTESTO.getNome() ||
            item.getTipoOcorrenciaRegistro() == TipoOcorrenciaRegistroRemessa.SUSTAR_PROTESTO_BAIXAR_TITULO.getNome());

目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community
虽然这段代码可能解决了问题,但是包括解释它如何以及为什么解决了问题将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请[编辑]您的答案以添加解释并指出适用的限制和假设。 - Yunnosch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接