假设我有以下Student
个对象集合,包括Name(String),Age(int)和City(String)。
我正在尝试使用Java的Stream API来实现以下类似sql的行为:
SELECT MAX(age)
FROM Students
GROUP BY city
现在,我发现了两种不同的方法:
final List<Integer> variation1 =
students.stream()
.collect(Collectors.groupingBy(Student::getCity, Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge())))
.values()
.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.map(Student::getAge)
.collect(Collectors.toList());
另一个:
final Collection<Integer> variation2 =
students.stream()
.collect(Collectors.groupingBy(Student::getCity,
Collectors.collectingAndThen(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge()),
optional -> optional.get().getAge())))
.values();
在两种方式中,都必须.values() ...
并过滤从收集器返回的空组。
有没有其他方法可以实现这种必要的行为?
这些方法让我想起了over partition by
sql语句......
由于
编辑:下面的所有答案都非常有趣,但不幸的是,这不是我想要的,因为我试图获得的只是价值观。我不需要键,只需要值。
答案 0 :(得分:29)
不要总是坚持使用groupingBy
。有时toMap
就是你需要的东西:
Collection<Integer> result = students.stream()
.collect(Collectors.toMap(Student::getCity, Student::getAge, Integer::max))
.values();
在这里,您只需创建一个Map
,其中键是城市,值是年龄。如果几个学生拥有相同的城市,则使用合并功能,这里只选择最大年龄。它更快更清洁。
答案 1 :(得分:12)
作为使用toMap
代替groupingBy
的{{3}}的补充,这里是简短的解决方案,如果您想坚持groupingBy
:
Collection<Integer> result = students.stream()
.collect(Collectors.groupingBy(Student::getCity,
Collectors.reducing(-1, Student::getAge, Integer::max)))
.values();
请注意,这三个arg reducing
收集器已经执行了映射操作,因此我们不需要将它与mapping
收集器嵌套,进一步提供标识值以避免处理{{1 }}。由于年龄总是正数,因此提供Optional
就足够了,因为一个组总是至少有一个元素,所以身份值永远不会显示为结果。
尽管如此,我认为在这种情况下,Tagir基于-1
的解决方案更可取。
当您希望让实际学生达到最大年龄时,基于toMap
的解决方案会变得更有趣,例如
groupingBy
实际上,即使这也可以使用Collection<Student> result = students.stream().collect(
Collectors.groupingBy(Student::getCity, Collectors.reducing(null, BinaryOperator.maxBy(
Comparator.nullsFirst(Comparator.comparingInt(Student::getAge)))))
).values();
收集器来表达:
toMap
您可以使用两个收藏家表达几乎所有内容,但如果您想对这些值执行Tagir’s great answer,则Collection<Student> result = students.stream().collect(
Collectors.toMap(Student::getCity, Function.identity(),
BinaryOperator.maxBy(Comparator.comparingInt(Student::getAge)))
).values();
会有优势。
答案 2 :(得分:1)
第二种方法在get()
上调用Optional
;这通常是一个坏主意,因为您不知道可选项是否为空(请改用orElse()
,orElseGet()
,orElseThrow()
方法。虽然您可能会争辩说,在这种情况下,由于您从学生列表本身生成值,因此始终存在一个值,这是需要牢记的。
基于此,您可以将变体2转换为:
final Collection<Integer> variation2 =
students.stream()
.collect(collectingAndThen(groupingBy(Student::getCity,
collectingAndThen(
mapping(Student::getAge, maxBy(naturalOrder())),
Optional::get)),
Map::values));
虽然它真的开始难以阅读,但我可能会使用变体1:
final List<Integer> variation1 =
students.stream()
.collect(groupingBy(Student::getCity,
mapping(Student::getAge, maxBy(naturalOrder()))))
.values()
.stream()
.map(Optional::get)
.collect(toList());
答案 3 :(得分:0)
Here is my implementation
public class MaxByTest {
static class Student {
private int age;
private int city;
public Student(int age, int city) {
this.age = age;
this.city = city;
}
public int getCity() {
return city;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return " City : " + city + " Age : " + age;
}
}
static List<Student> students = Arrays.asList(new Student[]{
new Student(10, 1),
new Student(9, 2),
new Student(8, 1),
new Student(6, 1),
new Student(4, 1),
new Student(8, 2),
new Student(9, 2),
new Student(7, 2),
});
public static void main(String[] args) {
final Comparator<Student> comparator = (p1, p2) -> Integer.compare( p1.getAge(), p2.getAge());
final List<Student> studets =
students.stream()
.collect(Collectors.groupingBy(Student::getCity,
Collectors.maxBy(comparator))).values().stream().map(Optional::get).collect(Collectors.toList());
System.out.println(studets);
}
}