Search code examples
javadictionarycomparator

How to use a customized comparator to define the innermost map of a nested map in Java?


I have a class Range and a comparator in Java:

class Range{
    int start;
    int end;
}

class RangeComparator implements Comparator<Range> {
  @Override
  public int compare(Range range1, Range range2) {
    //implementation
  }
}

and another class that that has a nested map;

class MyClass{
    private static Map<String, Map<String, Map<Range, List<String>>>> nestedMap;
}

I need:

  • the innermost map of the nestedMap to be a TreeMap that uses the RangeComparator to sort its keys, and
  • the nestedMap is empty when an object of MyClass is created, and
  • the innermost map has the property of using the specific comparator when an entry is inserted into the map.

How to achieve that?

In C++, those can be achieved by sepcifying the comparator in the declaration of the nestedMap as below:

struct Comp {
  const bool operator()(const Range& range1, const Range& range2) const {
    // implementation
  }
};

class MyClass{
  // declare the nested map with the comparator Comp
  map<string, map<string, map<Range, vector<string>, Comp>>> nestedMap;
};

I am wondering how to achieve the same in Java.


Solution

  • You can only initialize the outermost map directly, the inner maps are associated with a key and each key would have its own map as value. Unless you know the keys beforehand and initialize in advance, it's not possible.

    About what you need:

    1. the innermost map of the nestedMap to be a TreeMap that uses the RangeComparator to sort its keys - you can't declare that the Map will be a TreeMap with specific comparator.

    You can make the nested map a TreeMap and range comparable

    Map<String, Map<String, TreeMap<Range, List<String>>>> nestedMap;
    ...
    class Range implements Comparable<Range> {
    
      @Override
      public int compareTo(Range other) {
        //logic
      }
    }
    

    This may be undesirable for your case due to a myriad reasons - Range should not have natural ordering, you should not bind to a concrete Map implementation, the map can still be created with a different comparator so your order won't be followed, etc. The preferable solution in this case (and in principle) is to encapsulate the logic of creating this innermost map. See the example below how to do it.

    1. the nestedMap is empty when an object of MyClass is created - then nestedMap should not be static (this means it belongs to the class, not the instance).
    2. the innermost map has the property of using the specific comparator when an entry is inserted into the map - this is already covered by the first requirement, when a TreeMap is created with a Comparator it sorts the entries by their keys according to the comparator, without a comparator they are sorted according to their natural ordering (and will throw ClassCastException if the keys don't implement Comparable).

    One of the ways to initialize the map on demand the exact way you need it, is to use Map.computeIfAbsent().

    If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.

    As I said above this logic should be encapsulated and hidden from the outside world.

    public class MyClass {
    
      //random implementation to make it compile
      private static final Comparator<Range> RANGE_COMPARATOR = Comparator.comparing(Range::start).thenComparing(Range::end);
    
      private final Map<String, Map<String, TreeMap<Range, List<String>>>> nestedMap;
    
      public MyClass() {
        this.nestedMap = new HashMap<>();
      }
    
      public Map<Range, List<String>> getRangesMap(String key1, String key2) {
        return nestedMap
                .computeIfAbsent(key1, k -> new HashMap<>())
                .computeIfAbsent(key2, k -> new TreeMap<>(RANGE_COMPARATOR));
      }
    }
    

    I don't know the exact use case (you may need to get the map in a different way), but this shows the principle - now every Map<Range, List<String>> you get from MyClass will be a TreeMap sorting entries according to your comparator.