To obtain the total salary
and incentive
for students having the same name
, age
and city
you can group the data using a Map
. So you were thinking in the right direction.
In order to achieve that you would need some object, that would hold references name
, age
and city
.
You can't place this data into a Map.Entry
because it can only hold two references. A quick and dirty option would be to pack these properties into a List
using List.of()
(Arrays.asList()
for Java 8), or nest a map entry into another map entry (which would look very ugly). Although it's doable, I would not recommend doing so if you care about maintainability of code. Therefore, I'll not use this approach (but if you wish - just change one expression in the Collector).
A cleaner way would be to introduce a class
, or a Java 16 record
to represent these properties.
Option with a record would be very concise because all the boilerplate code would be auto-generated by the compiler:
public record NameAgeCity(String name, int age, String city) {
public static NameAgeCity from(Student s) {
return new NameAgeCity(s.getName(), s.getAge(), s.getCity());
}
}
For JDK versions earlier than 16 you can use the following class:
public static class NameAgeCity {
private String name;
private int age;
private String city;
// getters, all-args constructor
public static NameAgeCity from(Student s) {
return new NameAgeCity(s.getName(), s.getAge(), s.getCity());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NameAgeCity that = (NameAgeCity) o;
return age == that.age && Objects.equals(name, that.name) && Objects.equals(city, that.city);
}
@Override
public int hashCode() {
return Objects.hash(name, age, city);
}
}
To make the solution compliant with Java 8, I would use this class.
In order to group the data from the stream into a Map
, where instances of the class above would be used as a Key, we can use a flavor of Collector groupingBy()
, which expects a keyMapper function and a downstream Collector responsible for generating the Values of the Map.
Since we need to perform plain arithmetic using primitive values, it would be performancewise to make use of the Collector which performs a mutable reduction, i.e. mutates the properties of it's underlying mutable container while consuming elements from the stream. And to create such Collector a type that would serve as a mutable container.
I'll introduce a class AggregatedValues
which would serve as accumulation type. It would make sense to do so if objects in the source list represent different people (in such case the result would not represent a particular person, and you would use Student
, or whatever it's named in the real code, for that purpose it would be confusing), or if Student
is immutable, and you want to keep it like that.
public class AggregatedValues implements Consumer<Student> {
private String name;
private int age;
private String city;
private double salary;
private double incentive;
// getters, all-args constructor
@Override
public void accept(Student s) {
if (name == null) name = s.getName();
if (age == 0) age = s.getAge();
if (city == null) city = s.getCity();
salary += s.getSalary();
incentive += s.getIncentive();
}
public AggregatedValues merge(AggregatedValues other) {
salary += other.salary;
incentive += other.incentive;
return this;
}
}
And that's how we can make use of it:
List<Student> students = new ArrayList<>();
Collections.addAll(students, // List.of() for Java 9+
new Student("Raj", 10, "Pune", 10000, 100),
new Student("Raj", 10, "Pune", 20000, 200),
new Student("Raj", 20, "Pune", 10000, 100),
new Student("Ram", 30, "Pune", 10000, 100),
new Student("Ram", 30, "Pune", 30000, 300),
new Student("Seema", 10, "Pune", 10000, 100)
);
List<AggregatedValues> res = students.stream()
.collect(Collectors.groupingBy(
NameAgeCity::from, // keyMapper
Collector.of( // custom collector
AggregatedValues::new, // supplier
AggregatedValues::accept, // accumulator
AggregatedValues::merge // combiner
)
))
.values().stream()
.collect(Collectors.toList()); // or toList() for Java 16+
If in your real code, it does make sense in your real code to have the same type of the resulting list we can do one small change by introducing method toStudent()
in the AggregatedValues
.
public class AggregatedValues implements Consumer<Student> {
// the rest code
public Student toStudent() {
return new Student(name, age, city, salary, incentive);
}
}
And this method can be used as a finisher Function of the Collector:
List<AggregatedValues> res = students.stream()
.collect(Collectors.groupingBy(
NameAgeCity::from, // keyMapper
Collector.of( // custom collector
AggregatedValues::new, // supplier
AggregatedValues::accept, // accumulator
AggregatedValues::merge // combiner
AggregatedValues::toStudent // finisherFunction
)
))
.values().stream()
.collect(Collectors.toList()); // or toList() for Java 16+
res.forEach(System.out::println);
Output:
Student{name='Raj', age=20, city='Pune', salary=10000.0, incentive=100.0}
Student{name='Raj', age=10, city='Pune', salary=30000.0, incentive=300.0}
Student{name='Ram', age=30, city='Pune', salary=40000.0, incentive=400.0}
Student{name='Seema', age=10, city='Pune', salary=10000.0, incentive=100.0}