Java 8中如何限制groupBy的数量

19

如何限制每个条目的 groupBy?

例如(基于此示例:流 groupBy):

studentClasses.add(new StudentClass("Kumar", 101, "Intro to Web"));
studentClasses.add(new StudentClass("White", 102, "Advanced Java"));
studentClasses.add(new StudentClass("Kumar", 101, "Intro to Cobol"));
studentClasses.add(new StudentClass("White", 101, "Intro to Web"));
studentClasses.add(new StudentClass("White", 102, "Advanced Web"));
studentClasses.add(new StudentClass("Sargent", 106, "Advanced Web"));
studentClasses.add(new StudentClass("Sargent", 103, "Advanced Web"));
studentClasses.add(new StudentClass("Sargent", 104, "Advanced Web"));
studentClasses.add(new StudentClass("Sargent", 105, "Advanced Web"));

这个方法返回一个简单组:

   Map<String, List<StudentClass>> groupByTeachers = studentClasses
            .stream().collect(
                    Collectors.groupingBy(StudentClass::getTeacher));

如果我想限制返回的集合怎么办? 假设我只想要每个老师的前N个班级。该如何做?


3
“first”指的是什么?是指课程编号最低的班级,按字母表顺序排列最前面的班级名称,还是任意选择N个班级?注意:班级集合可能是无序的。 - Peter Lawrey
@PeterLawrey,你说得对,我没有提到,对我来说顺序无关紧要,但如果我们想要一个更全面和通用的解决方案——如果您添加一个排序示例(按其中一个字段)我会很高兴。 - yossico
4个回答

22

有可能引入一个新的收集器,以限制生成列表的元素数量。

该收集器将保留列表的头部元素(遇到顺序)。当达到收集限制时,累加器和合并器会丢弃每个元素。合并器代码有点棘手,但这样做的好处是不会添加任何额外的元素,只需稍后抛弃。

private static <T> Collector<T, ?, List<T>> limitingList(int limit) {
    return Collector.of(
                ArrayList::new, 
                (l, e) -> { if (l.size() < limit) l.add(e); }, 
                (l1, l2) -> {
                    l1.addAll(l2.subList(0, Math.min(l2.size(), Math.max(0, limit - l1.size()))));
                    return l1;
                }
           );
}

然后可以像这样使用它:
Map<String, List<StudentClass>> groupByTeachers = 
       studentClasses.stream()
                     .collect(groupingBy(
                          StudentClass::getTeacher,
                          limitingList(2)
                     ));

7
您可以使用 collectingAndThen在结果列表上定义完成操作。这样,您就可以对列表进行限制、过滤、排序等操作:
int limit = 2;

Map<String, List<StudentClass>> groupByTeachers =
    studentClasses.stream()
                  .collect(
                       groupingBy(
                           StudentClass::getTeacher,
                           collectingAndThen(
                               toList(),
                               l -> l.stream().limit(limit).collect(toList()))));

这仍然是在将值添加到映射中之后进行过滤,但目前为止是最好的答案。 - Razvan Manolescu
3
“完成器”的想法不错,但“完成器”不需要O(n)的代价。您可以尝试使用 list -> list.size() <= limit ? list : list.subList(0, limit)) 这样的代码来实现。然而,我仍然更喜欢Tunaki的解决方案,因为它完全不需要向列表中添加额外的元素。 - Brian Goetz

4
为此,您需要对Map结果进行.stream()操作。您可以通过以下方式实现:
// Part that comes from your example
Map<String, List<StudentClass>> groupByTeachers = studentClasses
            .stream().collect(
                    Collectors.groupingBy(StudentClass::getTeacher));

// Create a new stream and limit the result
groupByTeachers =
    groupByTeachers.entrySet().stream()
        .limit(N) // The actual limit
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> e.getValue()
        ));

这不是一个很优化的方法。但是如果你在初始列表上使用 .limit(),那么分组结果将是不正确的。这是保证限制的最安全方式。

编辑:

如评论中所述,这限制了老师而不是每个老师的班级。在这种情况下,您可以执行以下操作:

groupByTeachers =
        groupByTeachers.entrySet().stream()
            .collect(Collectors.toMap(
                e -> e.getKey(),
                e -> e.getValue().stream().limit(N).collect(Collectors.toList()) // Limit the classes PER teacher
            ));

不是很优化,我认为他的意思是在初始分组中完成这个操作。 - Razvan Manolescu
这限制了返回的教师数量,而不是每个教师的课程数量。 - siegi
3
在后处理步骤中使用 Map.replaceAll 比为每个元素做一个独立的流更好。但是无论如何,@Tunaki 的答案都更好。 - Brian Goetz

3
这样做可以得到所需的结果,但仍然将流的所有元素分类:
final int N = 10;
final HashMap<String, List<StudentClass>> groupByTeachers = 
        studentClasses.stream().collect(
            groupingBy(StudentClass::getTeacher, HashMap::new,
                collectingAndThen(toList(), list -> list.subList(0, Math.min(list.size(), N)))));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接