Java 8 lambda 从列表中获取和删除元素

Posted

技术标签:

【中文标题】Java 8 lambda 从列表中获取和删除元素【英文标题】:Java 8 lambda get and remove element from list 【发布时间】:2016-06-12 14:20:27 【问题描述】:

给定一个元素列表,我想获取具有给定属性的元素将其从列表中删除。我找到的最佳解决方案是:

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

是否可以在 lambda 表达式中组合 get 和 remove?

【问题讨论】:

这似乎是一个经典案例,什么时候只使用循环和迭代器。 @chrylis 我很不同意 ;) 我们已经习惯了命令式编程,以至于任何其他方式听起来都太奇特了。想象一下,如果现实是相反的:我们非常习惯于函数式编程,而新的命令式范式被添加到 Java 中。你会说这将是流、谓词和可选项的经典案例吗? 不要在这里打电话给get()!你不知道它是否为空。如果该元素不存在,您将引发异常。相反,请使用 ifPresent、orElse、orElseGet 或 orElseThrow 等安全方法之一。 @FedericoPeraltaSchaffner 可能是口味问题。但我也建议不要将两者结合起来。当我看到一些代码使用流获取值时,我通常假设流中的操作没有副作用。混合删除元素可能会导致代码误导读者。 澄清一下:您要删除listPredicate 为真的所有元素还是仅删除第一个(可能为零、一个或多个元素)? 【参考方案1】:

从列表中删除元素

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

输出: 一种 乙 C

【讨论】:

我建议这是最佳答案 这很整洁。如果不是为您自己的对象执行此操作,您可能需要实现 equals/hashCode。 (在此示例中使用默认情况下具有它们的字符串)。 恕我直言,答案不考虑“获取”部分:removeIf 是从集合中删除元素的优雅解决方案,但它不会返回已删除的元素。 这是一种从java ArrayList 中排除对象的非常简单的模式。想了很多。对我来说工作得很好。 这没有回答问题。要求是从列表中删除元素并将删除的项目/项目添加到新列表中。【参考方案2】:

虽然线程已经很老了,但仍然认为可以提供解决方案 - 使用Java8

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

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

API 参考:removeIf docs

假设:producersProcedureActiveList

注意:使用这种方法,您将无法获取已删除项目的保留。

【讨论】:

除了从列表中删除元素之外,OP 还希望引用该元素。 @eee:非常感谢您指出这一点。我错过了 OP 原始问题中的那部分。 只是要注意这将删除所有符合条件的项目。但 OP 似乎只需要删除第一项(OP 使用 findFirst() ) @asifsid88 如何删除元素?标题不是“get and remove”吗?【参考方案3】:

考虑使用 vanilla 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;

优势

    简单明了。 它只遍历一次,并且只遍历到匹配的元素。 即使没有stream() 支持,您也可以在任何Iterable 上执行此操作(至少那些在其迭代器上实现remove() 的人)

缺点

    您不能将其作为单个表达式就地执行(需要辅助方法或变量)

至于

是否可以在 lambda 表达式中组合 get 和 remove?

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

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

【讨论】:

我喜欢这个解决方案,但请注意它有一个严重的缺点你错过了:许多 Iterable 实现有 remove() 抛出 UOE 的方法。 (当然,不是 JDK 集合,但我认为说“适用于任何 Iterable”是不公平的。) 我想我们可以假设如果一个元素一般可以被移除,那么它可以被通过迭代器移除 您可以假设,但是在查看了数百个迭代器实现之后,这将是一个错误的假设。 (我仍然喜欢这种方法;你只是过度推销它。) @Brian Goetz:removeIfdefault 实现做出了相同的假设,但是,当然,它是在 Collection 而不是 Iterable 上定义的……【参考方案4】:

直接的解决方案是在 findFirst() 返回的 Optional 上调用 ifPresent(consumer)。当可选项不为空时,将调用此使用者。好处还在于,如果 find 操作返回一个空的可选项,它不会抛出异常,就像您当前的代码一样;相反,什么都不会发生。

如果要返回删除的值,可以map Optional 到调用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) 操作直接对索引进行操作。

【讨论】:

这对于链表来说是病态的。 @chrylis 确实是索引解决方案。根据列表的实现,人们会更喜欢其中一个。做了一个小修改。 @chrylis:如果是LinkedList,您可能不应该使用流 API,因为没有至少遍历两次就没有解决方案。但我不知道任何现实生活场景中链表的学术优势可以弥补其实际开销。所以简单的解决方案是永远不要使用LinkedList 哦,这么多编辑……现在第一个解决方案不提供删除的元素,因为 remove(Object) 只返回一个 boolean 告诉是否有要删除的元素。 @Marco Stramezzi:不幸的是,解释它的评论被删除了。如果没有boxed(),您将得到一个OptionalInt,它只能从intintmap。与IntStream 不同,没有mapToObj 方法。使用boxed(),您将获得一个Optional&lt;Integer&gt;,它允许map 指向任意对象,即ProducerDTO 返回的remove(int)。从Integerint 的演员表对于消除remove(int)remove(Object) 之间的歧义是必要的。【参考方案5】:

使用可以使用Java 8的过滤器,如果您不想更改旧列表,请创建另一个列表:

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

【讨论】:

【参考方案6】:

我确信这将是一个不受欢迎的答案,但它确实有效......

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

p[0] 将保存找到的元素或为空。

这里的“技巧”是通过使用实际上是最终的 array 引用来规避“有效最终”的问题,但要设置它的第一个元素。

【讨论】:

在这种情况下,它并没有那么糟糕,但与调用.orElse(null) 来获取ProducerDTOnull 的可能性相比并没有改进…… 在这种情况下,拥有.orElse(null)if 可能更容易,不是吗? @Holger 但是你怎么能通过使用orElse(null) 来调用remove() 使用结果即可。 if(p!=null) producersProcedureActive.remove(p); 仍然比 ifPresent 调用中的 lambda 表达式短。 @holger 我将问题的目标解释为避免多个陈述 - 即单行解决方案【参考方案7】:

使用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 的提交者

【讨论】:

【参考方案8】:

当我们想要将一个列表中的多个元素放入一个新列表(使用谓词过滤)并将它们从现有列表中删除时,我在任何地方都找不到合适的答案。

下面是我们如何使用 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。 this article 的 Collectors.partitioningBy 部分。

【讨论】:

【参考方案9】:

正如其他人所建议的,这可能是循环和迭代的用例。在我看来,这是最简单的方法。如果您想就地修改列表,则无论如何都不能将其视为“真正的”函数式编程。但是您可以使用Collectors.partitioningBy() 来获得一个包含满足您条件的元素的新列表,以及一个不满足您条件的新列表。当然,使用这种方法,如果您有多个满足条件的元素,那么所有这些都将在该列表中,而不仅仅是第一个。

【讨论】:

更好地过滤流并将结果收集到新列表【参考方案10】:

下面的逻辑是不修改原始列表的解决方案

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"]

【讨论】:

【参考方案11】:

结合我最初的想法和您的回答,我找到了似乎是解决方案 我自己的问题:

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

【讨论】:

您不应该使用get 并捕获异常。这不仅是糟糕的风格,而且还可能导致糟糕的表现。干净的解决方案更简单,return /* stream operation*/.findFirst() .map(i -&gt; producersProcedureActive.remove((int)i)) .orElseGet(() -&gt; logger.error("No producer found with POD [" + pod + "]"); return null; );【参考方案12】:

任务是:获取✶✶从列表中删除元素

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

【讨论】:

以上是关于Java 8 lambda 从列表中获取和删除元素的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 lambda 删除列表列表中数字下方的元素? Python [重复]

Java 8 lambda从对象列表创建字符串列表

lambda 中的 Java 8 lambda 无法从外部 lambda 修改变量

从 java 8 流中的列表中过滤值

如何使用 Java 8 lambda 从流中获取一系列项目?

如何在java 8中将字符串排序为带有lambdas的int