基于条件和顺序进行过滤的Java 8 Lambda

20

我试图基于多个条件进行列表筛选并排序。

class Student{
        private int Age;
        private String className;
        private String Name;

        public Student(int age, String className, String name) {
            Age = age;
            this.className = className;
            Name = name;
        }

        public int getAge() {
            return Age;
        }

        public void setAge(int age) {
            Age = age;
        }

        public String getClassName() {
            return className;
        }

        public void setClassName(String className) {
            this.className = className;
        }

        public String getName() {
            return Name;
        }

        public void setName(String name) {
            Name = name;
        }
    }

现在,如果我有一个这样的列表,比如说

List<Student> students = new ArrayList<>();
        students.add(new Student(24, "A", "Smith"));
        students.add(new Student(24, "A", "John"));
        students.add(new Student(30, "A", "John"));
        students.add(new Student(20, "B", "John"));
        students.add(new Student(24, "B", "Prince"));

我如何得到一个具有不同姓名的最年长学生列表呢?在C#中,可以使用System.Linq GroupBy进行比较,然后使用select进行展开,但我不太确定如何在Java中实现相同的功能。


7
students.stream().collect(groupingBy(...)) 可以翻译为“对学生进行流式处理并按照指定方式进行分组”。 - Andy Turner
你只需要最老的那些,而且仅仅是最老的那些吗? - michaeak
请说明期望的结果,即地图/列表/任何其他内容的外观。 - michaeak
4个回答

19
使用toMap收集器:
Collection<Student> values = students.stream()
                .collect(toMap(Student::getName,
                        Function.identity(),
                        BinaryOperator.maxBy(Comparator.comparingInt(Student::getAge))))
                .values();

说明

我们正在使用toMap的这个重载:

toMap​(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper,
      BinaryOperator<U> mergeFunction)
  • Student::getName 上面是用于提取映射键值的 keyMapper 函数。
  • Function.identity() 上面是用于提取映射值的 valueMapper 函数,其中 Function.identity() 仅返回源中的元素本身,即 Student 对象。
  • BinaryOperator.maxBy(Comparator.comparingInt(Student::getAge)) 上面是合并函数,用于“在发生键冲突的情况下决定要返回哪个 Student 对象,即当两个给定的学生具有相同的姓名时”,在这种情况下选择年龄最大的 Student
  • 最后,调用 values() 返回一个学生集合。

等效的 C# 代码为:

var values = students.GroupBy(s => s.Name, v => v,
                          (a, b) => b.OrderByDescending(e => e.Age).Take(1))
                      .SelectMany(x => x);

说明(对于不熟悉.NET的人)

我们正在使用GroupBy的扩展方法:

System.Collections.Generic.IEnumerable<TResult> GroupBy<TSource,TKey,TElement,TResult> 
       (this System.Collections.Generic.IEnumerable<TSource> source, 
         Func<TSource,TKey> keySelector, 
         Func<TSource,TElement> elementSelector, 
     Func<TKey,System.Collections.Generic.IEnumerable<TElement>,TResult> resultSelector);
  • s => s.Name 上面是用于提取分组值的 keySelector 函数。
  • v => v 上面是用于提取值(即Student对象本身)的 elementSelector 函数。
  • b.OrderByDescending(e => e.Age).Take(1) 上面是 resultSelector,它接受表示为 bIEnumerable<Student> 并选择最年长的学生。
  • 最后,我们应用 .SelectMany(x => x); 将结果为 IEnumerable<IEnumerable<Student>> 的序列合并为一个 IEnumerable<Student>

很好的答案。只是想说,使用 v -> v 而不是 Function.identity() 会使代码更类似于 C# 的等效代码,因此更容易进行比较。 - walen

7

或者不使用流:

Map<String, Student> map = new HashMap<>();
students.forEach(x -> map.merge(x.getName(), x, (oldV, newV) -> oldV.getAge() > newV.getAge() ? oldV : newV));
Collection<Student> max = map.values();

在从您的最初建议开始时就考虑到了这一点,但是这里需要一个额外的Map,但是与toMap相比,map.merge能为我们节省吗? - Naman
3
这本应该是我的答案(我在这里是“不使用流的人”),所以我感觉我能回答你的问题……实际上,Collectors.toMap 在内部使用了 Map.merge。 它甚至 在文档中提到mergeFunction - 一个合并函数,用于解决与同一键相关联的值之间的冲突,如供 Map.merge(Object, Object, BiFunction) 使用。 - fps

2
如果你只需要对分组进行排序,那就非常简单:
Map<String, List<Student>> collect = students.stream() // stream capabilities
        .sorted(Comparator.comparingInt(Student::getAge).reversed()) // sort by age, descending
        .collect(Collectors.groupingBy(Student::getName)); // group by name.

collect中的输出:

  • Prince=[学生 [年龄=24, 班级=B, 名字=Prince]],
  • Smith=[学生 [年龄=24, 班级=A, 名字=Smith]],
  • John=[学生 [年龄=30, 班级=A, 名字=John], 学生 [年龄=24, 班级=A, 名字=John], 学生 [年龄=20, 班级=B, 名字=John]]

1

为了混合和合并其他解决方案,你可以选择执行以下操作:

Map<String, Student> nameToStudentMap = new HashMap<>();
Set<Student> finalListOfStudents = students.stream()
        .map(x -> nameToStudentMap.merge(x.getName(), x, (a, b) -> a.getAge() > b.getAge() ? a : b))
        .collect(Collectors.toSet());

3
这违反了Stream.map方法参数必须符合“无状态”(statelessness)的要求。 - Andy Turner

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