0

I have a class Student:

public class Student {
    private String name;
    private int age;
    private String city;
    private double salary;
    private double incentive;
    
    // getters, all-args constructor, etc.
}

And I have a list of Student instances called students.

I want to create a new list which will contain the Students grouped by their name, age and city. The salary and incentive of the students having these attributes identical should be summed up.

Example:

Input:

Student("Raj",10,"Pune",10000,100)
Student("Raj",10,"Pune",20000,200)
Student("Raj",20,"Pune",10000,100)
Student("Ram",30,"Pune",10000,100)
Student("Ram",30,"Pune",30000,300)
Student("Seema",10,"Pune",10000,100)

Output:

Student("Raj",10,"Pune",30000,300)
Student("Raj",20,"Pune",10000,100)
Student("Ram",30,"Pune",40000,400)
Student("Seema",10,"Pune",10000,100)

My attempt:

List<Student> students = // initializing the list

List<Student> res = new ArrayList<>(students.stream()
    .collect(Collectors.toMap(
        ec -> new AbstractMap.SimpleEntry<>(ec.getName(),ec.getAge(),ec.getCity()),
        Function.identity(),
        (a, b) -> new Student(
            a.getName(), a.getAge(), a.getCity(), a.getSalary().add(b.getSalary()),a.getIncentive().add(b.getIncentive())
        )
    ))
    .values())));

Which produces a compilation error:

Compile error- Cannot resolve constructor 'SimpleEntry(String, int, String)' and Cannot resolve method 'add(double)

I've also tried some other options, but without success. How can I achieve that?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • I tried this code only for sum up salary- `Function> compositeKey1 = personRecord -> Arrays.asList(personRecord.getName(), personRecord.getAge(),personRecord.getCity()); student.stream().collect(Collectors.groupingBy(compositeKey1,Collectors.summingDouble(s -> s.getSalary()))). entrySet().stream().map(entry -> entry.getKey().toArray()) .collect(Collectors.toList());` No luck – prachi Kadam Nov 23 '22 at 11:32
  • I want the solution of groupby of more than 2 fields and then sum up more than 1 field which will return the List of same abject. I tried for groupby more than 2 fields but sumup only one field but the solution didnt workout. – prachi Kadam Nov 23 '22 at 15:40
  • Sorry, but you have not provided a **compilation error** (the code you've posted obviously would not compile). See the guidelines on how to ask questions [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask). Phrases like "didnt work" doesn't add information problem description. – Alexander Ivanchenko Nov 23 '22 at 15:42
  • Compilation error for above code is- ```Required type: List Provided: List no instance(s) of type variable(s) exist so that Object[] conforms to Student inference variable T has incompatible bounds: equality constraints: Student lower bounds: Object[]``` – prachi Kadam Nov 24 '22 at 04:53
  • Also tried one more solution with constructor- ```List res = new ArrayList<>(student.stream() .collect(Collectors.toMap( ec -> new AbstractMap.SimpleEntry<>(ec.getName(),ec.getAge(),ec.getCity()), Function.identity(), (a, b) -> new Student(a.getName(), a.getAge(), a.getCity(),a.getSalary().add(b.getSalary()),a.getIncentive().add(b.getIncentive())))) .values());``` Compile error- Cannot resolve constructor 'SimpleEntry(String, int, String)' and Cannot resolve method 'add(double)' – prachi Kadam Nov 24 '22 at 06:51

1 Answers1

0

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}
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • Thanks for the answer. One solution from my side also- – prachi Kadam Nov 25 '22 at 13:47
  • ```Function> compositeKey1 = personRecord -> Arrays.asList(personRecord.getName(), personRecord.getAge(),personRecord.getCity()); student.stream().collect(Collectors.groupingBy(compositeKey1)).entrySet().stream().map(e -> e.getValue().stream().reduce((f1, f2) -> new Student(f1.getName(), f1.getAge(), f1.getCity(), f1.getSalary()+f2.getSalary()),f1.getIncentive()+f2.getIncentive()))).map(f -> f.get()).collect(Collectors.toList());``` – prachi Kadam Nov 25 '22 at 13:48
  • unable to round of the salary and incentive values here. – prachi Kadam Nov 25 '22 at 13:50
  • @prachiKadam `reduce()` would create lots of intermediate objects you don't need. If properties that have to be accumulated are primitives, mutable reduction is more suitable, since it's more performant (I mentioned that in the answer). Regarding the list as a key, see the recommendation in the answer as well. Question about rounding floating point numbers has been covered on SO many times, examine existing questions, for instance [**see here**](https://stackoverflow.com/questions/2808535/round-a-double-to-2-decimal-places). – Alexander Ivanchenko Nov 25 '22 at 14:42