Search code examples
javahashmapjvmkeyset

How is the underlying keyset of a Hashmap implemented so that add method fails?


public class KeySetImmutable {
    public static void main(String[] args) {
        Map<String, String> hashMap = new HashMap<>();
        
        hashMap.put("Key1", "String1");
        hashMap.put("Key2", "String2");
        hashMap.put("Key3", "String3");
        hashMap.put("Key4", "String4");
        
        Set<String> keySet = hashMap.keySet();
        
        keySet.add("Key4");
        
        System.out.println(hashMap.keySet());
        System.out.println(keySet);
    }
}

In the above code keySet.add("Key4") throws java.lang.UnsupportedOperationException. Does it mean this particular instance of Set is a special implementation that prevents addition of keys? How is the underlying implementation achieving this?

Whereas keySet.remove("Key3"); works fine and removes the element from the HashMap as well.


Solution

  • keySet() returns a specific Set implementation that overrides remove() but inherits AbstractSet's add(), which inherits AbstractCollection's add(), which throws UnsupportedOperationException.

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }
    
    final class KeySet extends AbstractSet<K> {
        ...
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        ...
    }
    
    public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
       ...
    }
    
    public abstract class AbstractCollection<E> implements Collection<E> {
        ...
        /**
         * {@inheritDoc}
         *
         * @implSpec
         * This implementation always throws an
         * {@code UnsupportedOperationException}.
         *
         * @throws UnsupportedOperationException {@inheritDoc}
         * @throws ClassCastException            {@inheritDoc}
         * @throws NullPointerException          {@inheritDoc}
         * @throws IllegalArgumentException      {@inheritDoc}
         * @throws IllegalStateException         {@inheritDoc}
         */
        public boolean add(E e) {
            throw new UnsupportedOperationException();
        }
        ...
    }
    

    Note that these are just implementation details of a specific JDK version.

    The important thing is what the Javadoc of keySet() states:

    Returns a Set view of the keys contained in this map.The set is backed by the map, so changes to the map are reflected in the set, and vice-versa. If the map is modified while an iteration over the set is in progress (except through the iterator's own remove operation), the results of the iteration are undefined. The set supports element removal, which removes the corresponding mapping from the map, via the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations. It does not support the add or addAll operations.