溪流 - 按物业和最高收集

时间:2018-01-25 15:13:15

标签: java java-stream

问题陈述

给出以下课程(简化问题):

public static class Match {

  private final String type;
  private final int score;

  public Match(String type, int score) {
    this.type = type;
    this.score = score;
  } 

  public String getType() {
    return type;
  }

  public int getScore() {
    return score;
  }
}

我有一个Stream<Match>,其中包含该类的多个实例,相同类型多次出现,但分数不同:

Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10),
          new Match("B", 3), new Match("B", 6), new Match("B", 12),
          new Match("C", 1));

我现在想要收集流,结果是List<Match>只包含每种类型得分最高的实例。

我尝试了什么

以下代码正在运行,但我不确定它是否是“最佳”解决方案(除了可怕的阅读和格式化):

.collect(Collectors.collectingAndThen(
          Collectors.groupingBy(Match::getType, Collectors.collectingAndThen(
              Collectors.toList(),
              l -> l.stream().max(Comparator.comparing(Match::getScore)).get())), Map::values))
      .forEach(m -> System.out.println(m.getType() + ": " + m.getScore()));

.collect(Collectors.collectingAndThen(
          Collectors.groupingBy(Match::getType, Collectors.maxBy(Comparator.comparing(Match::getScore))), Map::values))
      .forEach(m -> m.ifPresent(ma -> System.out.println(ma.getType() + ": " + ma.getScore())));

输出(正确):

  

A:10
  B:12
  C:1

此外,我无法提取返回收集器的通用静态方法,以便我可以在以下方式使用它:   .collect(distinctMaxByProperty(Match::getType, Match::getScore)

非常感谢任何帮助!

3 个答案:

答案 0 :(得分:5)

当你可以在第一时间收集最大元素时,不要收集到List,只提取一个值,例如

Map<String,Match> result =
    Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10),
              new Match("B", 3), new Match("B", 6), new Match("B", 12), new Match("C", 1))
        .collect(Collectors.groupingBy(Match::getType, Collectors.collectingAndThen(
            Collectors.reducing(BinaryOperator.maxBy(
                                    Comparator.comparingInt(Match::getScore))),
            Optional::get)));

但是当你遇到在Optional的上下文中提取groupingBy的必要性时,值得检查带有合并函数的toMap`是否可以给出更简单的结果:

Map<String,Match> result =
    Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10),
              new Match("B", 3), new Match("B", 6), new Match("B", 12), new Match("C", 1))
        .collect(Collectors.toMap(Match::getType, Function.identity(),
                 BinaryOperator.maxBy(Comparator.comparingInt(Match::getScore))));

获得Map后,您可以通过

生成所需的输出
result.values().forEach(m -> System.out.println(m.getType() + ": " + m.getScore()));

但是如果你不需要实际的Match个实例,你可以做得更简单:

Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10),
          new Match("B", 3), new Match("B", 6), new Match("B", 12), new Match("C", 1))
    .collect(Collectors.toMap(Match::getType, Match::getScore, Math::max))
    .forEach((type,score) -> System.out.println(type + ": " + score));

答案 1 :(得分:3)

我有两种方法。

第一种方法:Collectors.toMap()

可能的实施方法是使用Collectors.toMap()来解决您的问题。

stream.collect(Collectors.toMap(Match::getType, Match::getScore, Math::max));

如果您希望获得List<Match>(),则可以重新映射结果

stream
    .collect(Collectors.toMap(Match::getType, Match::getScore, Math::max))
    .entrySet()
    .stream()
    .map(e -> new Match(e.getKey(), e.getValue()))
    .collect(Collectors.toList());

第二种方法:自定义收集器

正如您在问题中所说,可以在自定义收集器中提取此逻辑。你可以这样做:

public class MaxMatch implements Collector<Match, Map<String, Integer>, List<Match>> {
    @Override
    public Supplier<Map<String, Integer>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<String, Integer>, Match> accumulator() {
        return (map, match) -> {
            Integer score = match.getScore();
            if(map.containsKey(match.getType())) {
                score = Math.max(score, map.get(match.getType()));
            }
            map.put(match.getType(), score);
        };
    }

    @Override
    public BinaryOperator<Map<String, Integer>> combiner() {
        return (mapA, mapB) -> {
            mapA.forEach((k, v) -> {
                if(mapB.containsKey(k)) { mapB.put(k, Math.max(v, mapB.get(k))); }
                else { mapB.put(k, v); }
            });
            return mapB;
        };
    }

    @Override
    public Function<Map<String, Integer>, List<Match>> finisher() {
        return (map) -> map.entrySet().stream().map(e -> new Match(e.getKey(), e.getValue())).collect(Collectors.toList());
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

并像这样使用它:

stream.collect(new MaxMatch());

希望这可以帮助你:)

答案 2 :(得分:1)

尝试使用TreeMap

<强>更新

List<Match> matchStream1 = matchStream.
            collect(Collectors.groupingBy(Match::getType,
                    Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Match::getScore)))))
            .values()
            .stream()
            .map(TreeSet::last)
            .collect(Collectors.toList());

如果您的Match类实现Comparable。你可以简化这个:

() -> new TreeSet<>(Comparator.comparing(Match::getScore))

到此:

TreeSet::new