Catalin Vladu
Catalin Vladu

Reputation: 389

Java collection - group by multiple attributes

I have a Java class:

class Person {
  String firstName;
  String lastName;
  int income;

public Person(String firstName, String lastName, int income)
{
  this.firstName = firstName;
  this.lastName = lastName;
  this.income = income;
}

I have a Collection<Person>, with 4 x Person objects:

  Collection<Person> persons = new ArrayList<>();
  persons.add(new Person("John", "Smith", 5));
  persons.add(new Person("Mary", "Miller", 2));
  persons.add(new Person("John", "Smith", 4));
  persons.add(new Person("John", "Wilson", 4));

I want to make a new Collection instance, but from the elements with the same "firstName" and "lastName", make 1 element, and the result "income" will be the sum of the "incomes" of each element. So, for this particular case, the resulting collection will have 3 elements, and "John Smith" will have the "income" = 9.

In SQL, the equivalent query is:

SELECT FIRSTNAME, LASTNAME, SUM(INCOME) FROM PERSON GROUP BY FIRSTNAME, LASTNAME

I found only answers which contain Map as result, and "key" contains the column(s) used for grouping by. I want to obtain directly a similar type of collection from the initial (ArrayList<Person>), and not a Map, because if in my collection I have millions of elements, it will decrease the performance of the code. I know it was easier if I worked on SQL side, but in this case I must work on Java side.

Upvotes: 3

Views: 3139

Answers (4)

alex.b
alex.b

Reputation: 1528

I found the answer below:

List<Person> collect = persons.stream()
            .collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
                    Collectors.summingInt(Person::getIncome)))
            .entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
                                                                      entry.getKey().split(".")[1],
                                                                      entry.getValue()))
            .collect(Collectors.toList());

Do not do that way! It uses memory a lot. Use Wrapper (PersonComparator) over the fields you need to group by.

public class Main {
    public static void main(String[] args) {
        Collection<Person> persons = new ArrayList<>();
        persons.add(new Person("John", "Smith", 5));
        persons.add(new Person("Mary", "Miller", 2));
        persons.add(new Person("John", "Smith", 4));
        persons.add(new Person("John", "Wilson", 4));

        Map<Person, Integer> groupedByIncomes = persons.stream()
                .collect(
                        Collectors.groupingBy(
                                Person::getPersonComparator,
                                Collectors.summingInt(Person::getIncome)
                        )
                )
                .entrySet()
                .stream()
                .collect(Collectors.toMap(
                        e -> e.getKey().person,
                        Map.Entry::getValue
                ));

        System.out.println(groupedByIncomes);
    }

    static class Person {
        String firstName;
        String lastName;
        int income;

        public Person(String firstName, String lastName, int income) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.income = income;
        }

        public String getFirstName() {
            return firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public int getIncome() {
            return income;
        }

        PersonComparator getPersonComparator() {
            return new PersonComparator(this);
        }

        static class PersonComparator {
            Person person;

            PersonComparator(Person person) {
                this.person = person;
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
                PersonComparator that = (PersonComparator) o;
                if (!person.getFirstName().equals(that.person.getFirstName())) return false;
                return person.getLastName().equals(that.person.getLastName());
            }

            @Override
            public int hashCode() {
                int result = person.getFirstName().hashCode();
                result = 31 * result + person.getLastName().hashCode();
                return result;
            }
        }
    }
}

If you need framework solution f.e. when you need some abstraction over the data types you have (SQL, Mongo or Collections) I suggest you to use QueryDSL: http://www.querydsl.com/

Upvotes: 1

cнŝdk
cнŝdk

Reputation: 32145

I think JoSQL is your way to go here, it allow you to run SQL queries over java objects:

JoSQL (SQL for Java Objects) provides the ability for a developer to apply a SQL statement to a collection of Java Objects. JoSQL provides the ability to search, order and group ANY Java objects and should be applied when you want to perform SQL-like queries on a collection of Java Objects.

And this is how to use it in your case:

Query q=new Query();
q.parse("SELECT firstname, lastname, SUM(income) FROM package.Person GROUP BY firstname, lastname");
List<?> results=q.execute(names).getResults();

You can also follow this JoSQL tutorial for further reading.

Upvotes: 1

Schidu Luca
Schidu Luca

Reputation: 3947

I don't know if that is the most beautiful solution, but you can try to groupBy firstName and lastName with a delimiter between them, let's say .. After you collect your data into Map<String, Integer> that contains your firstName.lastName, you create new list of Person from it.

 List<Person> collect = persons.stream()
            .collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
                    Collectors.summingInt(Person::getIncome)))
            .entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
                                                                      entry.getKey().split(".")[1],
                                                                      entry.getValue()))
            .collect(Collectors.toList());

Upvotes: 2

daniu
daniu

Reputation: 14999

You can use Java 8 streams' Collector's groupingBy:

    Map<String, Integer> sum = items.stream().collect(
            Collectors.groupingBy(p -> p.getFirstName()+p.getSecondName(), Collectors.summingInt(Person::getIncome)));

Upvotes: 0

Related Questions