【问题标题】:Java 8 streams: find items from one list that match conditions calculated based on values from another listJava 8 流:从一个列表中查找与根据另一列表中的值计算的条件匹配的项目
【发布时间】:2017-07-30 23:49:34
【问题描述】:

有两个类和两个对应的列表:

class Click {
   long campaignId;
   Date date;
}

class Campaign {
   long campaignId;
   Date start;
   Date end;
   String type;
}

List<Click> clicks = ..;
List<Campaign> campaigns = ..;

并且想在clicks中找到所有Clicks:

  1. campaigns列表中有一个对应的Campaign,即Campaign与相同的campaignId AND

  2. 这个Campaigntype = "预期" AND

  3. 这个Campaigns.start click.date Campaigns.end

到目前为止,我有以下实现(这对我来说似乎令人困惑和复杂):

clicks.
        stream().
        filter(click -> campaigns.stream().anyMatch(
                campaign -> campaign.getCampaignType().equals("prospecting") &&
                        campaign.getCampaignId().equals(click.getCampaignId()) &&
                        campaign.getStart().after(click.getDate()) &&
                        campaign.getEnd().before(click.getDate()))).
        collect(toList());

我想知道这个问题是否有更简单的解决方案。

【问题讨论】:

  • 我认为您真正需要做的就是将较大的 lambda 提取到命名方法中。
  • @JoeC 这真的可能吗?这个 lambda 指的是点击和活动,这意味着我需要在过滤器接受谓词时使用 BiPredicate
  • click -&gt; checkCampaigns(click, campaigns)
  • 无论基于流的解决方案如何(感知或真实)优雅:我强烈建议使用算法不同的方法。 anyMatch 可能看起来很方便,但有 O(n),所以对于 m 点击和 n 活动,这是 O(nm)。构建一个 Map&lt;ID, Campaign&gt; 用作查找会将其更改为 O(n+m)(一些 TreeMap 可能支持按日期索引,但这可能是第二步)
  • 顺便说一下,麻烦的Date 类已经被java.time.Instant 类取代了。尽可能避免使用Date,但在必须使用旧类中添加的新方法进行转换时:myDate.toInstant()

标签: java java-8 java-stream


【解决方案1】:

嗯,有一种非常巧妙的方法可以解决您的问题 IMO,最初的想法来自 Holger(我会在此处找到问题并将其链接)。

您可以定义进行检查的方法(我已经简化了一点):

static boolean checkClick(List<Campaign> campaigns, Click click) {
    return campaigns.stream().anyMatch(camp -> camp.getCampaignId() 
               == click.getCampaignId());
}

并定义一个绑定参数的函数:

public static <T, U> Predicate<U> bind(BiFunction<T, U, Boolean> f, T t) {
    return u -> f.apply(t, u);
}

而用法是:

BiFunction<List<Campaign>, Click, Boolean> biFunction = YourClass::checkClick;
Predicate<Click> predicate = bind(biFunction, campaigns);

clicks.stream()
      .filter(predicate::test)
      .collect(Collectors.toList());

【讨论】:

  • 这确实很简洁,但是 OP 说他们发现他们当前的解决方案“令人困惑和复杂”,所以我无法想象这个解决方案不那么简单。
【解决方案2】:

突出的一点是您的第二个要求与匹配无关,它只是campaigns 的一个条件。您必须测试这是否对您更好:

clicks.stream()
    .filter(click -> campaigns.stream()
        .filter(camp -> "prospecting".equals(camp.type))
        .anyMatch(camp -> 
            camp.campaignId == click.campaignId &&
            camp.end.after(click.date) &&
            camp.start.before(click.date)
        )
    )
    .collect(Collectors.toList());

否则,我从未见过不涉及在第一个谓词中流式传输第二个集合的流解决方案,因此您无法做得比您所做的更好。在可读性方面,如果它看起来让您感到困惑,那么创建一个测试布尔条件并调用它的方法:

clicks.stream()
    .filter(click -> campaigns.stream()
        .filter(camp -> "pre".equals(camp.type))
        .anyMatch(camp -> accept(camp, click))
    )
    .collect(Collectors.toList());

static boolean accept(Campaign camp, Click click) {
    return camp.campaignId == click.campaignId &&
            camp.end.after(click.date) &&
            camp.start.before(click.date);
}

最后,2条不相关的建议:

  1. 不要使用旧的Date 类,而是使用新的java.time APILocalDate
  2. 如果Campaigntype 只能有一些预定义的值(如“已提交”、“预期”、“已接受”...),那么enum 将比一般的@987654330 更合适@。

【讨论】:

    【解决方案3】:

    我的 2 美分: 由于 OP 中没有太多样板代码。所以可能不可能/没有必要减少代码中的行/字符。我们可以重写它以使其更清晰:

    Map<Long, List<Campaign>> map = campaigns.stream().filter(c -> c.type.equals("prospecting"))
                                             .collect(Collectors.groupingBy(c -> c.campaignId));
    
    clicks.stream().filter(k -> map.containsKey(k.campaignId))
                   .filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.before(k.date) && c.end.after(k.date)))
                   .collect(Collectors.toList());
    

    代码并不比原始代码短多少。但它会将性能从 O(nm) 提高到 O(n+m),正如 cmets 中提到的@Marco13。如果你想要更短,试试StreamEx

    Map<Long, List<Campaign>> map = StreamEx.of(campaigns)
                    .filter(c -> c.type.equals("prospecting")).groupingBy(c -> c.campaignId);
    
    StreamEx.of(clicks).filter(k -> map.containsKey(k.campaignId))
            .filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.after(k.date) && c.end.before(k.date)))
           .toList();
    

    【讨论】:

      【解决方案4】:
      public List<Click> findMatchingClicks(List<Campaign> cmps, List<Click> clicks) {
          List<Campaign> cmpsProspective = cmps.stream().filter(cmp -> "prospective".equals(cmp.type)).collect(Collectors.toList());
          return clicks.stream().filter(c -> matchesAnyCmp(c, cmpsProspective).collect(Collectors.toList());
      }
      
      public boolean matchesAnyCmp(Click click, List<Campaign> cmps) {
           return cmps.stream().anyMatch(click -> cmp.start.before(click.date) && cmp.end.after(click.date));
      }
      

      用getter替换字段,快写吧。

      【讨论】:

        猜你喜欢
        • 2020-03-04
        • 1970-01-01
        • 2013-03-11
        • 2019-06-27
        • 2022-08-17
        • 2022-06-16
        • 2013-12-08
        • 2021-04-23
        • 2020-11-26
        相关资源
        最近更新 更多