Wednesday, December 30, 2015

Applying grouping on a Collection

Best practice to convert List to a Map. In other words how to apply grouping on a List or Collection without writing separate methods for each.

I recently came across a case where grouping was done at many places on different grouping criteria. Every thing in that is same except of the grouping criteria.
For example; If I have a collection of Persons and I want to group them by Salary. Then it becomes a Map with String as key and List of persons as the value
// Original list
List<Person>
// Expected group
Map<String, List<Person>>
    private static Map<String, List<Person>> groupBySalary(List<Person> persons) {
        Map<String, List<Person>> groupedMap = new HashMap<String, List<Person>>();
        for (Person p : persons) {
            // Grouping criteria
            String role = p.getRole();
            List<Person> list = groupedMap.get(role);
            if (list == null) {
                // not exist in group. create and add for first time.
                list = new ArrayList<Person>();
                groupedMap.put(role, list);
            }
            list.add(p);
        }
        return groupedMap;
    }

Later if we want to group it by Role or First name or maybe some thing different; then the Declaration remains the same but we need to write different methods. This is not a nice practice to duplicate the code. See the class ListToMapHelper which demonstrate how a list can be grouped easily to Map by providing the grouping criteria on the fly. Something like you provide the Sorting criteria for Collections class. In same way you provide the Grouping criteria to this Helper static method. With lambda expressions in Java 8 this is again must simpler with just one line code. See main method for usage.

package in.mbm.playground;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


/**
 * 
 * @author Mohammed Bin Mahmood
 *
 */
public class ListToMapHelper {

    public interface IGroupingCriteria<G, T> {
        /**
         * Takes a Type T from which Grouping has to be done. check main method for the usage.
         * 
         * @param type T
         * @return Object of type G which is the grouping criteria.
         */
        public G group(T type);
    };

    /**
     * Takes an Iterable and Grouping criteria implementation and returns a Map grouped by the
     * criteria.
     * 
     * @param iteratableCollection
     * @param groupBy
     * @return
     */
    public static <G, T> Map<G, List<T>> groupBy(Iterable<T> iteratableCollection, IGroupingCriteria<G, T> groupBy) {
        Map<G, List<T>> map = new HashMap<G, List<T>>();
        for (T type : iteratableCollection) {
            // grouping criteria
            G group = groupBy.group(type);

            List<T> list = map.get(group);
            if (list == null) {
                // not exist in group. create and add for first time.
                list = new ArrayList<T>();
                map.put(group, list);
            }
            list.add(type);
        }
        return map;
    }

    public static void main(String[] args) {
        // assume list of persons is populated
        List<Person> persons = fillSomePersons();

        // group persons by Role.
        // provide the implementation of IGroupingCriteria with String as Key and Person as the Type
        // itself

        Map<String, List<Person>> map = groupBy(persons, new IGroupingCriteria<String, Person>() {

            @Override
            public String group(Person p) {
                // group by role.
                return p.getRole();
            }
        });

        // by Java 8
        map = groupBy(persons, p -> p.getRole());

        // print and see the results.
        for (Entry<String, List<Person>> entry : map.entrySet()) {
            System.out.println(entry.getKey() + "\t" + entry.getValue());
        }
    }

    private static class Person {
        private final int id;
        private String name, role;

        Person(int id) {
            this.id = id;
        }

        Person setName(String name) {
            this.name = name;
            return this;
        }

        String getRole() {
            return role;
        }

        Person setRole(String role) {
            this.role = role;
            return this;
        }

        @Override
        public String toString() {
            return id + " " + name + " " + role;
        }
    }

    private static List<Person> fillSomePersons() {
        List<Person> persons = new ArrayList<ListToMapHelper.Person>(5);
        persons.add(new Person(1).setName("Person A").setRole("Role A"));
        persons.add(new Person(2).setName("Person B").setRole("Role A"));
        persons.add(new Person(3).setName("Person C").setRole("Role B"));
        persons.add(new Person(4).setName("Person D").setRole("Role B"));
        persons.add(new Person(5).setName("Person E").setRole("Role A"));
        persons.add(new Person(6).setName("Person F").setRole("Role A"));
        persons.add(new Person(7).setName("Person G").setRole("Role B"));
        persons.add(new Person(8).setName("Person H").setRole("Role C"));
        persons.add(new Person(9).setName("Person I").setRole("Role A"));
        return persons;
    }

    private static Map<String, List<Person>> groupByRole(List<Person> persons) {
        Map<String, List<Person>> groupedMap = new HashMap<String, List<Person>>();
        for (Person p : persons) {
            // Grouping criteria
            String role = p.getRole();
            List<Person> list = groupedMap.get(role);
            if (list == null) {
                // not exist in group. create and add for first time.
                list = new ArrayList<Person>();
                groupedMap.put(role, list);
            }
            list.add(p);
        }
        return groupedMap;
    }
}


Hope this example helps. Do share your feedback and pass it on.