3

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"))
             ]
             
     }

}
  • 5
    Note: you cannot sort a `HashMap`. That's one of its basic properties, i.e. it doesn't have any order. If you want a notion of order either use a `TreeMap` which sorts by key or a `LinkedHashMap` which orders by insert order. – Thomas Jan 26 '22 at 09:32
  • Second note: `return Person.COMPARE_BY_DATE.compare(person1, person2);` etc. in your comparators would return 0 if the first 2 people have the value (date in this case). You probably want to keep comparing until you find some non-0 result or hit the end of either list. – Thomas Jan 26 '22 at 09:34
  • You can use the entrySet() if you want to sort. For example https://stackoverflow.com/questions/29567575/sort-map-by-value-using-lambdas-and-streams but this does not sort the backing HashMap. Just FYI – JCompetence Jan 26 '22 at 09:34
  • Thank you @Thomas. As for the first note, it is my basic mistake. For the second note: the intention is to continue sorting all the elements of the lists. In particular, the entire map must be sorted on the basis of the dates of each element of the list and not of the keys. – Giuseppe Mondelli Jan 26 '22 at 09:42
  • First: Your requirement is unclear. Sorting a list of Persons by some attributes is clear. But what do you mean by sorting a map with a list of Persons? Second: In Java there are SortedMaps (like TreeMap). But according to the documentation a SortedMap is ordered only by key and not by value. So in the context of Java collections sorting a map by values is not possible. Of course you can implement your own map ordered by value but you have first define what that exactly mean. – vanje Jan 26 '22 at 09:45
  • "The intention is to continue sorting all the elements of the lists. ... In particular, the entire map must be sorted." - So what do you want to sort? The lists? The map entries (via `LinkedHashMap`)? Both? – Thomas Jan 26 '22 at 09:47
  • Thanks @Thomas. That's right, the intention is to sort both: both the entire map and each of its values ​​(for example, to have a map sorted by the dates of all the objects in the its lists) – Giuseppe Mondelli Jan 26 '22 at 09:51
  • Ok, to clarify I modified the main body in such a way as to make the script as close as possible to my real use case. The map keys correspond to an abbreviation of the whole date present in the DateOfBirth field of Person. My request is therefore to be able to order the entire map but also all its lists. – Giuseppe Mondelli Jan 26 '22 at 10:02
  • 1
    So, if you need to sort the lists as well, you need to first do that, then sort the elements in the stream and finally construct the map. Depending on whether the lists should be sorted in place (which is then reflected in the original map as well) or you want to create a copy you can use `map(list -> new ArrayList<>(list))` to create the copy and `peek(list-> Collections.sort(list, comparator))` to sort each list, then apply the stream sort and collect. – Thomas Jan 26 '22 at 11:59

2 Answers2

1

First, let's reiterate: a HashMap is unordered so you need something else. Your Sorter.sort() method actually collects the values into a LinkedHashMap which provides an iteration order based on insert order and would be ok for your use case. Just to be clear (also for the sake of others): this doesn't sort the map itself but creates a new LinkedHashMap.

Now to your comparators: if you want to compare 2 lists you probably want to compare elements at equal indices. Thus your comparator needs to be something like this:

Comparator<List<Person>> = (l1, l2) -> {
   Iterator<Person> itr1 = l1.iterator();
   Iterator<Person> itr2 = l2.iterator();

   while( itr1.hasNext() && itr2.hasNext() ) {
     Person p1 = itr1.next();
     Person p2 = itr1.next();

     int result = Person.COMPARE_BY_DATE.compare(p1, p2);
     if( result != 0 ) {
       return result;
     }
   }

   return 0;
};
 

However, the lists might have different lengths as well so you might want to handle that too:

Comparator<List<Person>> = (l1, l2) -> {
   //iterators and loop here

   //after the loop it seems all elements at equal indices are equal too
   //now compare the sizes

   return Integer.compare(l1.size(), l2.size());
}
Thomas
  • 87,414
  • 12
  • 119
  • 157
1

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.