javacollectionsenumssortedmapenum-map

Why is EnumMap not a SortedMap in Java?


EnumMap<K extends Enum<K>, V> in Java is clearly ordered by definition of the associated enum, as you can also see in the javadoc:

Enum maps are maintained in the natural order of their keys (the order in which the enum constants are declared). This is reflected in the iterators returned by the collections views (keySet(), entrySet(), and values()).

What I need is a SortedMap using an enum as key type. I want to use methods like headMap() or firstKey(), but I want to profit from the added cpu+memory performance of EnumMaps. A TreeMap sounds like way too much overhead here.

Question: was this just missed in implementation, was it laziness (derived from AbstractMap) or is there a good reason why EnumMap is not a SortedMap?


Solution

  • This won't make an answer to your primary question (because only the original designers have the answer), but one approach I was considering was for you to implement it yourself. While trying to make a SortedMap implementation based on EnumMap, I came up with the following class.

    This is surely a quick and dirty implementation (and note that it is not fully compliant with SortedMap - because the view requirements is not met), but if you need one, you can improve it:

    class SortedEnumMap<K extends Enum<K>, V> 
        extends EnumMap<K, V> 
        implements SortedMap<K, V> {
    
        private Class<K> enumClass;
        private K[] values;
    
        public SortedEnumMap(Class<K> keyType) {
            super(keyType);
            this.values = keyType.getEnumConstants();
            this.enumClass = keyType;
    
            if (this.values.length == 0) {
                throw new IllegalArgumentException("Empty values");
            }
        }
    
        @Override
        public Comparator<? super K> comparator() {
            return Comparator.comparingInt(K::ordinal);
        }
    
        @Override
        public SortedMap<K, V> subMap(K fromKey, K toKey) {
            List<K> keys = Arrays.stream(this.values)
                    .dropWhile(k -> k.ordinal() < fromKey.ordinal())
                    .takeWhile(k -> k.ordinal() < toKey.ordinal())
                    .collect(Collectors.toList());
    
            return this.forKeys(keys);
        }
    
        @Override
        public SortedMap<K, V> headMap(K toKey) {
            List<K> keys = new ArrayList<>();
    
            for (K k : this.values) {
                if (k.ordinal() < toKey.ordinal()) {
                    keys.add(k);
                } else {
                    break;
                }
            }
    
            return this.forKeys(keys);
        }
    
        @Override
        public SortedMap<K, V> tailMap(K fromKey) {
            List<K> keys = new ArrayList<>();
    
            for (K k : this.values) {
                if (k.ordinal() >= fromKey.ordinal()) {
                    keys.add(k);
                }
            }
    
            return this.forKeys(keys);
        }
    
        //Returned map is NOT a "view" or the current one
        private SortedEnumMap<K, V> forKeys(List<K> keys) {
            SortedEnumMap<K, V> n = new SortedEnumMap<>(this.enumClass);
            keys.forEach(key -> n.put(key, super.get(key)));
    
            return n;
        }
    
        @Override
        public K firstKey() {
            return this.values[0];
        }
    
        @Override
        public K lastKey() {
            return this.values[this.values.length - 1];
        }
    }
    

    And for a quick test (bugs yet to be found):

    SortedMap<Month, Integer> m = new SortedEnumMap(Month.class);
    
    for (Month v : Month.values()) {
        m.put(v, v.getValue());
    }
    
    System.out.println("firstKey():       " + m.firstKey());
    System.out.println("lastKey():        " + m.lastKey());
    System.out.println("headMap/June:     " + m.headMap(Month.JUNE));
    System.out.println("tailMap/June:     " + m.tailMap(Month.JUNE));
    System.out.println("subMap/April-July " + m.subMap(Month.APRIL, Month.JULY));
    

    I get:

    firstKey():       JANUARY
    lastKey():        DECEMBER
    headMap/June:     {JANUARY=1, FEBRUARY=2, MARCH=3, APRIL=4, MAY=5}
    tailMap/June:     {JUNE=6, JULY=7, AUGUST=8, SEPTEMBER=9, OCTOBER=10, NOVEMBER=11, DECEMBER=12}
    subMap/April-July {APRIL=4, MAY=5, JUNE=6}