Search code examples
javalistsortingdictionarycomparator

How can I sort a Map<String, List<CustomObject>>?


I have done a lot of research for this question, but I have not found a way to sort a map of custom object lists (Map<String, List<CustomObj>>), basing the comparison on CustomObj attributes (as SORT_BY_NAME, SORT_BY_DATE, etc).

A motivating example of my question is:

  • I have a custom object: Person (with attribute as Name, DateOfBith, etc ...);
  • I have a Map of Person object List as: Map<String, List<Person>>. The map key is a String used for other purposes;
  • I would like to create a comparator and a sorting method that sorts the map in ascending order based on comparisons between the attributes of the Person object (name, date, etc ..)

For simplicity I report the real code but adapted to a simplified case of Person object, because it would already represent the concept of entity.

Person.java -> Custom object

public class Person {

     private String name;
     private Date dateOfBirth;
     ...

     // Empty and Full attrs Constructors
     ...

     // Getter and Setter
     ...

     // Comparator by name
     public static Comparator<Person> COMPARE_BY_NAME = Comparator.comparing(one -> one.name);
     // Comparator by date
     public static Comparator<Person> COMPARE_BY_DATE = Comparator.comparing(one -> one.dateOfBirth);

}

Sorter.java -> Sorter object

public class Sorter {

     // List Comparator of Person by Date 
     public static final Comparator<? super List<Person>> COMPARATOR_BY_DATE = (Comparator<List<Person>>) (p1, p2) -> {
          for (Persontab person1: p1) {
              for (Person person2: p2) {
                  return Person.COMPARE_BY_DATE.compare(person1, person2);
              }
          }
          return 0;
     };

     // List Comparator of Person by Name
     public static final Comparator<? super List<Person>> COMPARATOR_BY_NAME = (Comparator<List<Person>>) (p1, p2) -> {
          for (Persontab person1: p1) {
              for (Person person2: p2) {
                  return Person.COMPARE_BY_NAME.compare(person1, person2);
              }
          }
          return 0;
     };

     // Sorting method
     public Map<String, List<Person>> sort(Map<String, List<Person>> map, Comparator<? super List<Person>> comparator) {
          return map.entrySet()
            .stream()
            .sorted(Map.Entry.comparingByValue(comparator))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1, LinkedHashMap::new));
     }

}

Main.java -> Start code

public class MainApp {

     public static void main(String[] args) {

          Map<String, List<Person>> exampleMap = new HashMap<>();
          List<Person> personList = new ArrayList<>();
          personList.add(new Person("name1", new Date("2022-01-01")));              
          personList.add(new Person("name12", new Date("2022-01-05")));
          personList.add(new Person("name13", new Date("2022-01-03")));
          map.put("2022-01", personList);

          personList.clear();
          personList.add(new Person("name14", new Date("2021-02-01")));              
          personList.add(new Person("name3", new Date("2021-02-05")));
          personList.add(new Person("name4", new Date("2021-02-03")));
          map.put("2021-02", personList);

          Sorter sorter = new Sorter();

          // Example of sorting by date
          map = sorter.sort(exampleMap, Sorter.COMPARATOR_BY_DATE);
          // In this case the sorting works correctly, or rather it sorts the items by date as I expect
         
          // Example of sorting by name
          map = sorter.sort(exampleMap, Sorter.COMPARATOR_BY_NAME);
          // In this case, I don't think sorting works correctly. Sort each list of elements for each key in ascending order. But it doesn't sort the map elements.

          /* I expect to have the following map when sort by date:
             "2021-02": [
               Person("name14", new Date("2021-02-01")),
               Person("name4", new Date("2021-02-03")),
               Person("name3", new Date("2021-02-05"))
             ], 
             "2022-01": [
               Person("name14", new Date("2021-02-01")),
               Person("name13", new Date("2022-01-03")),
               Person("name12", new Date("2022-01-05"))
             ]
             
     }

}

Solution

  • By changing the type of Map used in my code above, as suggested by @Thomas, in TreeMap<>, I found the solution to the problem as follows:

    1. I first merged all the Person object lists into one list. This was then sorted by the chosen criterion, for example Person.COMPARE_BY_NAME;
    2. I created an algorithm that would re-group the sorted lists, in according to the criteria of my project, in a map. The key of this map corresponds to the concatenation of the month + year of the Person object. The algorithm is reported at the bottom of the comment;
    3. I sort the map based on the chosen attribute, for example Sorter.COMPARATOR_BY_NAME;

    Di seguito il codice è come segue:

    Merge all List<Person> in one -> main or somewhere before the Map was created

        ...
        //
        List<Person> newPersonList = new ArrayList<>();
        newPersonList.addAll(oldPersonList1);
        newPersonList.addAll(oldPersonList2);
        ...
    

    Main or somewhere before the Map was created

        ...
        groupList(Person.COMPARE_BY_NAME, Sorter.COMPARATOR_BY_NAME);
        ...
    

    GroupPerson -> method to group the merged List<Person> in a TreeMap<String, List<Person>>

        public Map<String, List<Person>> groupList(final Comparator<? super Person> itemComparator, final Comparator<? super List<Person>> listComparator)
             
             // Sort Person list by comparator before create TreeSet
             newPersonList.sort(itemComparator);
    
             Map<String, List<Person>> personMapGrouped = new TreeMap<>();
             
             // Here, create a Map of list
             for (Person person: newPersonList) {
                 final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy MM", Locale.getDefault());
                 final String groupKey = dateFormat.format(person.getDateOfBirth());
    
                 if (personMapGrouped.containsKey(groupKey)) {
                    // The key is already in the TreeMap; add the Person object against the existing key.
                    final List<Person> personListGrouped = personMapGrouped.get(groupKey);
                    if (personListGrouped!= null) {
                       personListGrouped.add(person);
                    }
                 } else {
                    // The key is not there in the TreeMap; create a new key-value pair
                    final List<Person> personListGrouped = new ArrayList<>();
                    personListGrouped.add(person);
                    personMapGrouped.put(groupKey, personListGrouped);
                 }
             }
             // Here sort the Map by params passed
             final TabPersonSorter sorter = new TabPersonSorter();
             personMapGrouped = sorter.sort(personMapGrouped, listComparator);
        }
    

    In this case, using the lists created in the main above, the results obtained are:

        "List<Person> mergedList": [
            Person("name1", new Date("2022-01-01")),
            Person("name3", new Date("2021-02-05")),
            Person("name4", new Date("2021-02-03")),
            Person("name12", new Date("2022-01-05")),
            Person("name13", new Date("2022-01-03")),
            Person("name14", new Date("2021-02-01"))
        ]
    
        "Map<String, List<Person>> mergedMap": {
            "2022-01": [
                Person("name1", new Date("2022-01-01")),
                Person("name12", new Date("2022-01-05")),
                Person("name13", new Date("2022-01-03"))
            ], 
            "2021-02": [
                Person("name3", new Date("2021-02-05")),
                Person("name4", new Date("2021-02-03"))
            ],
            "2022-02": [
                Person("name14", new Date("2021-02-01"))
            ]
        } 
    

    Obviously if the grouping in the map were not bound by such a restrictive date as only year + month, the sorting would have the desired effect in distinct groups. In fact, in the case of the sorting by date, this is respected very well.