【问题标题】:Java stream - find most frequent element based on a specific fieldJava 流 - 根据特定字段查找最频繁的元素
【发布时间】:2021-12-05 02:04:19
【问题描述】:

我有一个Person 对象的列表,我想在列表中找到最常见的名称和频率,仅使用 java 流。 (平局时,返回任意结果)

目前,我的解决方案使用groupingBycounting,然后再次在结果映射中找到max 元素。 当前的解决方案在输入(列表/地图)上进行 2 次传递。 是否有可能使它更高效和可读?

Person p1 = Person.builder().id("p1").name("Alice").age(1).build();
Person p2 = Person.builder().id("p2").name("Bob").age(2).build();
Person p3 = Person.builder().id("p3").name("Charlie").age(3).build();
Person p4 = Person.builder().id("p4").name("Alice").age(4).build();
List<Person> people = ImmutableList.of(p1, p2, p3, p4);

Map.Entry<String, Long> mostCommonName = people
        .stream()
        .collect(collectingAndThen(groupingBy(Person::getName, counting()),
                map -> map.entrySet().stream().max(Map.Entry.comparingByValue()).orElse(null)
        ));

System.out.println(mostCommonName); // Alice=2

【问题讨论】:

  • 假设列表中有 2x Alice 和 2x Tom。你想同时得到这两个名字,只有 Alice,只有 Tom,或者只要它是一个重复次数最多的名字,哪一个都没有关系?

标签: java collections java-stream


【解决方案1】:

如果您坚持只使用流,那么您最好的选择可能是拥有一个自定义收集器,其中包含一次通过聚合所需的信息:

class MaxNameFinder implements Collector<Person, ?, String> {
    public class Accumulator {
        private final Map<String,Integer> nameFrequency = new HashMap<>();
        private int modeFrequency = 0;
        private String modeName = null;

        public String getModeName() {
            return modeName;
        }

        public void accept(Person person) {
            currentFrequency = frequency.merge(p.getName(), 1, Integer::sum);
            if (currentFrequency > modeFrequency) {
                modeName = person.getName();
                modeFrequency = currentFrequency;
            }
        }

        public Accumulator combine(Accumulator other) {
            other.frequency.forEach((n, f) -> this.frequency.merge(n, f, Integer::sum));
            if (this.frequency.get(other.modeName) > frequency.get(this.modeName))
                modeName = other.modeName;
            modeFrequency = frequency.get(modeName);
            return this;
        };

    }

    public BiConsumer<Accumulator,​Person> accumulator() {
        return Accumulator::accept;
    }

    public Set<Collector.Characteristics> characteristics() {
        return Set.of(Collector.Characteristics.CONCURRENT);
    }

    public BinaryOperator<Accumulator> combiner() {
        return Accumulator::combine;
    }

    public Function<Accumulator,String> finisher() {
        return Accumulator::getModeName;
    }

    public Supplier<Accumulator> supplier() {
        return Accumulator::new;
    }
}

用法如下:

people.stream().collect(new MaxNameFinder())

这将返回一个代表最常见名称的字符串。

【讨论】:

  • 我实际上知道这个解决方案,但想知道是否有更简单的方法。我会考虑更多..
  • 如果你想一次性完成,那么你需要逻辑来整理频率数据——这是没有办法的。您可以不使用组合逻辑(仅用于支持并发),但您无法做太多其他事情来简化它。老实说,这对于流来说并不是一个很好的用例:它们在这里并没有增加很多清晰度。
【解决方案2】:

使用循环和Map::merge 函数可以将两个通道压缩为一个,立即返回计算的频率值:

String mostCommonName = null;
int maxFreq = 0;
Map<String, Integer> freq = new HashMap<>();

for (Person p : people) {
    if (freq.merge(p.getName(), 1, Integer::sum) > maxFreq) {
        maxFreq = freq.get(p.getName());
        mostCommonName = p.getName();
    }
}

System.out.printf("Most common name '%s' occurred %d times.%n", mostCommonName, maxFreq);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-27
    • 2021-03-29
    • 2017-04-22
    • 2011-12-16
    • 1970-01-01
    • 2022-08-16
    • 1970-01-01
    相关资源
    最近更新 更多