如何在Java Streams中将多个属性映射到一个groupingBy属性下

3

我正在尝试按照部门对每个学生的总分进行分组,并按照他们的总分排序。

学生类属性:

private String firstName,lastName,branch,nationality,grade,shName;
private SubjectMarks subject;
private LocalDate dob;

SubjectMarks类:

public SubjectMarks(int maths, int biology, int computers) {
    this.maths = maths;
    this.biology = biology;
    this.computers = computers;
}
public double getAverageMarks() {
    double avg = (getBiology() + getMaths() + getComputers())/3;
    return avg;
}

主类:

    Collections.sort(stList, new Comparator<Student>() {
        @Override
        public int compare(Student m1, Student m2) {
             if(m1.getSubject().getAverageMarks() == m2.getSubject().getAverageMarks()){
                    return 0;
                }
                return m1.getSubject().getAverageMarks()< m2.getSubject().getAverageMarks()? 1 : -1;
        }
    });

Map<String, List<Student>> groupSt=stList.stream().collect(Collectors.groupingBy(Student::getBranch,
        LinkedHashMap::new,Collectors.toList()));


groupSt.forEach((k, v) -> System.out.println("\nBranch Name: " + k + "\n" + v.stream()
.flatMap(stud->Stream.of(stud.getFirstName(),stud.getSubject().getAverageMarks())).collect(Collectors.toList())));

代码已更新:这是我得到输出的方式。

  Branch Name: ECE
  [Bob, 96.0, TOM, 84.33333333333333]

  Branch Name: CSE
  [Karthik, 94.33333333333333, Angelina, 91.0, Arun, 80.66666666666667]

  Branch Name: EEE
  [Meghan, 85.0]

这是实际的排序顺序,但学生对象被压扁成一行,用逗号(,)分隔。
在上面的输出中,由于Bob获得了所有分支的最高综合分数,所以ECE排在第一位,然后是其他分支按学生综合分数排序。
期望的结果是: 按其综合分数排序的学生姓名列表。
  Branch Name: ECE
  [{Bob, 96.0},{TOM, 84.33333333333333}]

  Branch Name: CSE
  [{Karthik, 94.33333333333333}, {Angelina, 91.0}, {Arun, 
   80.66666666666667}]

  Branch Name: EEE
  [Meghan, 85.0]

有没有一种方法可以使用流(stream)将名称和平均值都映射到一个属性上的groupingBy函数中?

当你运行这段代码时,实际输出是什么?最好也发布一下。 - Gautham M
3
你不想告诉我们的秘密是,你的代码产生编译错误? - Holger
2
从输出来看,你实际上不需要一个内部的 List<Map<String, Double>,总体上 Map<String, Map<String, Double>> 对你来说应该足够了。你可以考虑使用一个基于你所需比较器的排序映射来处理内部映射。 - Naman
@Gautham M,很抱歉我无法回答你的问题,因为我很忙。实际上,我尝试的代码存在编译错误。 - Manmadh
@Holger 没有秘密。我曾经是Java的初学者。希望给出建议而不是保守秘密会更好。谢谢。 - Manmadh
3个回答

4
你可能更倾向于选择返回类型为 Map<String, Map<String, Double>> 或者一个具有适当的 equalshashCode 方法以确保在内部 List<Custom> 中的唯一性的自定义类。基于前一种方案,我将提出解决方案,你可以将其转换为更符合你实际代码的方法。
一旦你对每个 branch 的特定学生进行分组,你可以执行一个基于 Double::max 的 merge 的 toMap 归约操作来确保 firstName 映射到该学生的最大平均分数... 然后 收集这些条目并按照分数(值)排序。
以下代码可能看起来有点复杂,但也可以拆分成几个步骤。
Map<String, LinkedHashMap<String, Double>> branchStudentsSortedOnMarks = stList.stream()
    .collect(Collectors.groupingBy(Student::getBranch, // you got it!
            Collectors.collectingAndThen(
                    Collectors.toMap(Student::getFirstName,
                            s -> s.getSubject().getAverageMarks(), Double::max), // max average marks per name
                    map -> map.entrySet().stream()
                            .sorted(Map.Entry.<String, Double>comparingByValue().reversed()) // sorted inn reverse based on value
                            .collect(Collectors.toMap(Map.Entry::getKey,
                                    Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new)) 
            )));

谢谢,@Naman。我试了你的方法,很不错。但是它只能对LinkedHashMap进行排序,我想要对外部Map也进行排序。我的意思是,具有最高总分的学生所在的分支应该排在第一位,其余学生紧随其后。我认为我们可以先对列表进行排序。请查看我在更新的问题中的输出。再次感谢。 - Manmadh
2
@Manmadh 这将是按值排序的另一个用例。否则,您也可以根据最大平均分对初始列表进行排序,并在分组时收集外部映射到 LinkedHashMap 中。 - Naman

1
首先,在你的Map<String, List<Map<String,Double>>>中,列表内的映射只包含一个键值对。因此,我建议您返回Map<String, List<Entry<String, Double>>>。(java.util.Map中的Entry
此外,在您的学生类中创建一个getAverageMarks方法,该方法将返回:
return subject.getAverageMarks();

// First define a function to sort based on average marks
UnaryOperator<List<Entry<String, Double>>> sort = 
    list -> {
        Collections.sort(list, Collections.reverseOrder(Entry.comparingByValue()));
        return list;
    };

// function to create entry
Function<Student, Entry<String, Double>> getEntry = 
    s -> Map.entry(s.getFirstName(), s.getAverageMarks());

// return this
list.stream()
    .collect(Collectors.groupingBy(
        Student::getBranch,        
        Collectors.mapping(getEntry, // map each student
                           // collect and apply sort as finisher
                           Collector.of(ArrayList::new,
                                        List::add,
                                        (x,y) -> {x.addAll(y); return x;},
                                        sort))));            

1
最好将Map<String,List<Entry<String,Double>>>表示为Map<String,Map<String,Double>> - Naman
@Naman 我最初考虑使用 Map<String, Map<String, Double>> 的方式,其中 TreeMap 是内部映射。但是由于 SortedMap 接受一个比较器,该比较器按 key 而不是 value 进行排序,因此我想以这种方式完成它。在收集到映射时是否可以进行排序?还是应该在映射收集后再进行排序? - Gautham M
2
请注意,自Java 8以来,您可以写成list.sort(comparator)而不是Collections.sort(list, comparator) - Holger
2
在我的理解中,除非你编写一个基于比较值的自定义SortedMap实现,否则不能直接实现这个目标。此外,我评论返回类型的主要意图是内部的“List”允许根据名称重复输入,否则它将是内部映射的“key”。 - Naman
@Naman 是的,我现在明白了。我实际上没有预料到同一分支机构中同一人员会有多组标记。 - Gautham M
@GauthamM 谢谢。明白了如何做。这种方法也很酷,而且有效。 - Manmadh

0

输入:

List<Student> stList = Arrays.asList(
        new Student("John", "Wall", "A", "a", "C", "sa", new SubjectMarks(65, 67, 100), LocalDate.now()),
        new Student("Arun", "Wall", "B", "a", "C", "sa", new SubjectMarks(45, 61, 95), LocalDate.now()),
        new Student("Marry", "Wall", "A", "a", "C", "sa", new SubjectMarks(90, 80, 92), LocalDate.now())
);

思路:

  1. 按“分支”进行分组
  2. 对于每个组(列表)- 按成绩排序并将每个学生映射到一个“姓名”,“成绩”的映射中。

现在编码变得很容易:

Map<String, List<Map<String, Double>>> branchToSortedStudentsByGrade = stList.stream().collect(Collectors.groupingBy(
        Student::getBranch, Collectors.collectingAndThen(Collectors.toList(),
                l -> l.stream()
                        .sorted(Comparator.comparing(st -> st.getSubject().getAverageMarks(), Comparator.reverseOrder()))
                        .map(student -> Collections.singletonMap(student.getFirstName(), student.getSubject().getAverageMarks()))
                        .collect(Collectors.toList()))));

输出:

{
  A=[{Marry=87.0}, {John=77.0}],
  B=[{Arun=67.0}]
}

顺便提一下:

请注意,在getAverageMarks函数中,在浮点数环境中进行整数除法:

public double getAverageMarks() {
    double avg = (getBiology() + getMaths() + getComputers())/3;
    return avg;
}

这将导致所有成绩都以这种格式显示- xx.0

如果这是错误的,我建议采用以下方法:

public double getAverageMarks() {
    return DoubleStream.of(maths, biology, computers)
            .average()
            .getAsDouble();
}

谢谢,@MostNeededRabit。它正在对学生名单进行排序。但是,当我将John添加到C分公司时,由于Marry与C分公司的总分最高,因此应该首先列出C分公司。以下是我的输出: 分公司名称:B [{Arun=67.0}]分公司名称:C [{Marry=87.33333333333333}, {John=77.33333333333333}] 预期输出为: 分公司名称:C [{Marry=87.33333333333333}, {John=77.33333333333333}] 分公司名称:B [{Arun=67.0}] 请在更新的问题中检查我的输出。 - Manmadh
@MostNeededRabit 感谢您提供使用singletonMap()的建议,这对我帮助很大。我修改了一些代码并得到了实际输出。 - Manmadh

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