【问题标题】:Iterate efficiently through 2 different List with same Type of Object(Java8)通过具有相同对象类型的 2 个不同列表有效地迭代(Java8)
【发布时间】:2017-08-17 14:34:28
【问题描述】:

我有两个列表,其中包含重要数量的对象,每个 N 元素:

List<Foo> objectsFromDB = {{MailId=100, Status=""}, {{MailId=200, Status=""}, {MailId=300, Status=""} ... {MailId=N , Status= N}}

List <Foo> feedBackStatusFromCsvFiles = {{MailId=100, Status= "OPENED"}, {{MailId=200, Status="CLICKED"}, {MailId=300, Status="HARDBOUNCED"} ... {MailId=N , Status= N}} 

小见解: objectFromDB 通过调用 Hibernate 方法检索我的数据库的行。

feedBackStatusFromCsvFiles 调用 CSVparser 方法并解组为 Java 对象。

我的实体类Foo 有所有的setter 和getter。所以我知道基本的想法是使用这样的 foreach:

     for (Foo fooDB : objectsFromDB) {
          for(Foo fooStatus: feedBackStatusFromCsvFiles){
              if(fooDB.getMailId().equals(fooStatus.getMailId())){
                    fooDB.setStatus(fooStatus.getStatus());
                }
               }
            }

就我对初级开发人员的了解而言,我认为这样做是一种非常糟糕的做法?我应该实现一个比较器并使用它来迭代我的对象列表吗?我还应该检查 null 案例吗?

感谢大家的回答!

【问题讨论】:

  • 可能有更好的方法,但是根据您当前的实现,您至少应该休息一下; setStatus 之后的行,因为一旦找到匹配项,就没有必要检查列表中的其余对象。
  • 如果你使用嵌套的for 循环,那么内部循环的主体将被执行 O(n^2) 次。如果元素的数量肯定很小,这可能没问题,但如果它可能增长到数百或数千个元素,那么它可能太昂贵了。
  • 您确定这两个元素的计数相同吗?当您使用 N 时,我认为您将拥有相同的数字。否则我会期待 N 和 M 或类似的东西。
  • 如果这两个列表都按mailId 排序,那么您可以以O(n) 的成本协同迭代它们。但是,如果它们还没有已经以这种方式排序,那么首先对它们进行排序可能会花费O(n log n)。对于大于某个最小大小的列表,这在性能方面是值得的,但如果您的列表确实很小,则不是。
  • 感谢大家的回答。我的列表确实会有大量的元素(一万个)。我的意思是 N ,我的列表中有 N 个可能的元素。 @JohnBollinger 列表根本没有按 mailId 排序,也没有按其他字段排序。我应该按照你的建议做吗?我的另一个重要问题是,我应该如何迭代?使用比较器或根据你们的每个?

标签: java performance for-loop collections java-8


【解决方案1】:

假设 Java 8 并考虑到 feedbackStatus 可能包含多个具有相同 ID 的元素这一事实。

  1. 将列表转换为使用 ID 作为键并具有元素列表的 Map。
  2. 迭代列表并使用地图查找所有消息。

代码是:

final Map<String, List<Foo>> listMap = 
objectsFromDB.stream().collect(
      Collectors.groupingBy(item -> item.getMailId())
);

for (final Foo feedBackStatus : feedBackStatusFromCsvFiles) {
        listMap.getOrDefault(feedBackStatus.getMailId(), Colleactions.emptyList()).forEach(item -> item.setStatus(feedBackStatus.getStatus()));
}

【讨论】:

  • 如果 ID 不存在,可能将 listMap.get(feedBackStatus.getMailId()) 更改为 listMap.getOrDefault(feedBackStatus.getMailId(), Collections.emptyList()) 以不执行任何操作,就像使用原始代码一样。
【解决方案2】:

您的问题是将 Foo 的最后状态合并到数据库对象中。因此您可以分两步完成,使其更清晰易读。

  1. 过滤需要合并的 Foo。
  2. 将 Foos 与最后状态合并。

    //because the status always the last,so you needn't use groupingBy methods to create a complex Map.
    Map<String, String> lastStatus = feedBackStatusFromCsvFiles.stream()
            .collect(toMap(Foo::getMailId, Foo::getStatus
                           , (previous, current) -> current));
    //find out Foos in Database that need to merge
    Predicate<Foo> fooThatNeedMerge = it -> lastStatus.containsKey(it.getMailId());
    //merge Foo's last status from cvs.
    Consumer<Foo> mergingFoo = it -> it.setStatus(lastStatus.get(it.getMailId()));
    
    objectsFromDB.stream().filter(fooThatNeedMerge).forEach(mergingFoo);
    

【讨论】:

    【解决方案3】:

    使用集合中的映射来避免嵌套循环。

        List<Foo> aList = new ArrayList<>();
        List<Foo> bList = new ArrayList<>();
        for(int i = 0;i<5;i++){
            Foo foo = new Foo();
            foo.setId((long) i);
            foo.setValue("FooA"+String.valueOf(i));
            aList.add(foo);
            foo = new Foo();
            foo.setId((long) i);
            foo.setValue("FooB"+String.valueOf(i));
            bList.add(foo);
        }
    
        final Map<Long,Foo> bMap = bList.stream().collect(Collectors.toMap(Foo::getId, Function.identity()));
    
        aList.stream().forEach(it->{
            Foo bFoo = bMap.get(it.getId());
            if( bFoo != null){
                it.setValue(bFoo.getValue());
            }
        });
    

    唯一的其他解决方案是让 DTO 层返回 MailId->Foo 对象的映射,因为您可以使用 CVS 列表进行流式传输,并简单地查找 DB Foo 对象。否则,对两个列表进行排序或迭代的代价不值得在性能时间上进行权衡。前面的陈述一直成立,直到它最终导致平台上的内存限制,直到那时让垃圾收集器完成它的工作,而您尽可能轻松地完成您的工作。

    【讨论】:

    • 您可以简单地遍历键/值对,例如 aMap.forEach((it,aFoo) -&gt; {…,而不是遍历键并为每个键执行查找,例如 aMap.forEach((it,aFoo) -&gt; {…,但由于这是线性迭代这根本不会从地图中受益,您实际上不需要aMap,因为您可以首先使用aList.forEach(aFoo -&gt; { Long it = aFoo.getId(); …
    • 您当然是正确的,但是我将其打破,因此由于 OP 没有使用任何流,因此发生的事情更加明显。这也是我在 for 循环中迭代而不是使用流来生成的原因。
    • 不允许我编辑评论,但我将更新并删除额外的地图,因为 bList 已经显示了该功能,这对于大型数据集。谢谢!
    【解决方案4】:

    鉴于您的列表可能包含数以万计的元素,您应该担心简单的嵌套循环方法会太慢。它肯定会执行比它需要做的更多的比较。

    如果内存相对丰富,那么最快的合适方法可能是从您的一个列表中形成一个从mailId 到对应Foo 的映射,有点像@MichaelH 建议的那样,并使用它匹配mailIds。但是,如果 mailId 值在一个或两个列表中不一定是唯一的,那么您将需要一些与 Michael 的特定方法不同的东西。即使mailIds 在两个列表中肯定是唯一的,只形成一个映射会更有效率。

    对于最一般的情况,您可能会这样做:

    // The initial capacity is set (more than) large enough to avoid any rehashing
    Map<Long, List<Foo>> dbMap = new HashMap<>(3 * objectFromDb.size() / 2);
    
    // Populate the map
    // This could be done more effciently if the objects were ordered by mailId,
    // which perhaps the DB could be enlisted to ensure.
    for (Foo foo : objectsFromDb) {
        Long mailId = foo.getMailId();
        List<Foo> foos = dbMap.get(mailId);
    
        if (foos == null) {
            foos = new ArrayList<>();
            dbMap.put(mailId, foos);
        }
        foos.add(foo);
    }
    
    // Use the map
    for (Foo fooStatus: feedBackStatusFromCsvFiles) {
        List<Foo> dbFoos = dbMap.get(fooStatus.getMailId());
    
        if (dbFoos != null) {
            String status = fooStatus.getStatus();
    
            // Iterate over only the Foos that we already know have matching Ids
            for (Foo fooDB : dbFoos) {
                fooDB.setStatus(status);
            }
        }
    }
    

    另一方面,如果您的空间有限,因此创建地图是不可行的,但是重新排序两个列表是可以接受的,那么您仍然应该通过首先对两个列表进行排序来提高性能。大概您会为此目的使用Collections.sort() 和适当的Comparator。然后,您将在每个列表上获得一个Iterator,并使用它们在两个列表上进行协作迭代。我没有提供代码,但它会让人想起合并排序的合并步骤(但这两个列表实际上并未合并;您只需将状态信息从一个复制到另一个)。但这只有在 feedBackStatusFromCsvFilesfeedBackStatusFromCsvFiles 中的 mailIds 都不同时才有意义,否则整个任务的预期结果无法很好地确定。

    【讨论】:

    • 我的示例让它们只是为了生成一个列表,但在我的示例中没有要求大小相同。按照他的逻辑,驱动因素是将数据库中的记录与 CSV 匹配。您应该将主要的效率问题(例如“内存空间”)交给运行时以在 Java 8 中进行优化,因为它甚至会比您的“更好”优化更快。
    • @Holger,这是一个错字:我的意思是3 * objectFromDb.size() / 2(现已修复)。这给出了地图中的初始容量(哈希桶的数量)。该数字略大于避免对地图进行任何重新散列所需的数字。有关详细信息,请参阅HashMap 的文档。
    • @MichaelH,别荒谬了。这里有各种替代方案之间的空间与速度权衡,而且空间不是无限资源。某些环境确实是空间受限的,即使在具有大物理和虚拟内存的计算机上也是如此,无论如何这绝不是当今唯一感兴趣的机器。作为一名程序员,您可能拥有访问您可能需要的所有空间的奢侈,但您确实没有拥有假设你有这么多空间却没有的奢侈实际上考虑这个问题。 Java 无法“优化”这一点。
    • 您在理论上毫无疑问是正确的。但是,您在提出该论点时并未在示例中利用运行时优化的流处理。由于增加了数万条记录的约束,我们的方法都不是最优的。然而,虽然我们的策略相似,但您的策略是使用次优迭代策略,仅通过单个变量而不是 2 个变量来感知内存空间增益。同时,您为映射中的每个 ID 实例化一个列表。我将使用 OP 提供的其他信息更新我的帖子。
    • 我明白了,3 * size / 2 在默认负载因子 .75f 下确实有意义
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多