带有流的Oracle trails on reduction给出了一个示例,该示例说明了如何将一群人转换为包含基于性别的平均年龄的地图。它使用以下Person
类和代码:
public class Person {
private int age;
public enum Sex {
MALE,
FEMALE
}
private Sex sex;
public Person (int age, Sex sex) {
this.age = age;
this.sex = sex;
}
public int getAge() { return this.age; }
public Sex getSex() { return this.sex; }
}
Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getSex,
Collectors.averagingInt(Person::getAge)));
上面的流代码很好用,但是我想看看如何使用收集器的 custom 实现来执行相同的操作。在Stack Overflow或网络上,我找不到如何执行此操作的完整示例。至于为什么要这样做,例如,也许我们想计算某种涉及年龄的加权平均值。在这种情况下,Collectors.averagingInt
的默认行为是不够的。
答案 0 :(得分:5)
在这些情况下只需使用Collector.of(Supplier, BiConsumer, BinaryOperator, [Function,] Characteristics...)
:
Collector.of(() -> new double[2],
(a, t) -> { a[0] += t.getAge(); a[1]++; },
(a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
a -> (a[1] == 0) ? 0.0 : a[0] / a[1])
)
尽管定义PersonAverager
可能更容易理解:
class PersonAverager {
double sum = 0;
int count = 0;
void accept(Person p) {
sum += p.getAge();
count++;
}
PersonAverager combine(PersonAverager other) {
sum += other.sum;
count += other.count;
return this;
}
double average() {
return count == 0 ? 0 : sum / count;
}
}
并将其用作:
Collector.of(PersonAverager::new,
PersonAverager::accept,
PersonAverager::combine,
PersonAverager::average)
答案 1 :(得分:3)
此答案已经过测试,基于许多不同的来源。 Collectors#averagingInt
的源代码有助于弄清楚下面使用的lambda语法。使用的供应商是大小为Double[]
的数组。第一个索引用于存储累积的人员年龄,而第二个索引用于存储计数。
public class PersonCollector<T extends Person> implements Collector<T, double[], Double> {
private ToIntFunction<Person> mapper;
public PersonCollector(ToIntFunction<Person> mapper) {
this.mapper = mapper;
}
@Override
public Supplier<double[]> supplier() {
return () -> new double[2];
}
@Override
public BiConsumer<double[], T> accumulator() {
return (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; };
}
@Override
public BinaryOperator<double[]> combiner() {
return (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; };
}
@Override
public Function<double[], Double> finisher() {
return a -> (a[1] == 0) ? 0.0 : a[0] / a[1];
}
@Override
public Set<Characteristics> characteristics() {
// do NOT return IDENTITY_FINISH here, which would bypass
// the custom finisher() above
return Collections.emptySet();
}
}
List<Person> list = new ArrayList<>();
list.add(new Person(34, Person.Sex.MALE));
list.add(new Person(23, Person.Sex.MALE));
list.add(new Person(68, Person.Sex.MALE));
list.add(new Person(14, Person.Sex.FEMALE));
list.add(new Person(58, Person.Sex.FEMALE));
list.add(new Person(27, Person.Sex.FEMALE));
final Collector<Person, double[], Double> pc = new PersonCollector<>(Person::getAge);
Map<Person.Sex, Double> averageAgeBySex = list
.stream()
.collect(Collectors.groupingBy(Person::getSex, pc));
System.out.println("Male average: " + averageAgeBySex.get(Person.Sex.MALE));
System.out.println("Female average: " + averageAgeBySex.get(Person.Sex.FEMALE));
这将输出:
Male average: 41.666666666666664
Female average: 33.0
请注意,我们将方法引用Person::getAge
传递给自定义收集器,该收集器将集合中的每个Person
映射到整数年龄值。另外,我们不会从Characteristics.IDENTITY_FINISH
方法返回characateristics()
。这样做意味着我们的自定义finisher()
将被绕过。