I was running into problems with different teams at my employer using different Immutable Collection implementations and wanted to avoid writing code to adapt each Collection

Larry Diamond ldiamond at ldiamond.com
Sun Jan 31 20:28:33 UTC 2021


I was thinking of submitting a proposal for UnmodifiableCollections for
Java.

I wrote up a first draft of a JEP for this.

What do you think?

Thank you very much
Larry Diamond
-------------- next part --------------

Summary
-------

Unmodifiable Collections interfaces

Add Unmodifiable interfaces as intermediary interfaces between Iterable and the Collections Interfaces.
By using an interface without mutator methods we can prevent attempts to mutate collections at compile time
rather than at run time and help developers produce better quality code

Goals
-----

Create interfaces UnmodifiableCollection, UnmodifiableSet, UnmodifiableList, and UnmodifiableMap 
as base interfaces to the existing Collections interfaces that developers can use when they want 
to ensure a Collection is immutable by contract.

These interfaces will allow developers to make APIs that guarantee at compile time that a 
Collection will not be modified within that method.   The Unmodifiable methods on Collections 
have been successful at wrapping Collections in a run time unmodifiable view of existing Collections, 
however, by using unmodifiable interfaces we can obtain a compile time error rather than a run time Exception.

Backwards compatibility and not breaking existing code is very important and I believe that 
adding these interfaces minimizes the opportunity for problems to arise.   Implementations of 
Collections interfaces may need to change if default implementation methods of the modified Collections 
interfaces are insufficient, but a definite goal of this JEP is that users of these interfaces 
will not be required to make changes to their code.


Non-Goals
---------

Refactoring existing working code that does not return the unmodifiable classes but could 
will not be changed in performing this JEP in order to reduce risk of creating defects
and to reduce the work to accomplish this JEP.   Modifying some methods in the Collections 
class to accept Unmodifiable parameters is absolutely in scope.


Success Metrics
---------------

I'm unsure if there are metrics taken on defect rate in open source projects but the existence
of these interfaces should reduce those metrics over time as these interfaces become generally used.


Motivation
----------

Developers are looking for ways to produce more error free code, and one of the 
things we are looking for is immutability on Collections.   Kotlin has a 
kotlinx.collections.immutable package (https://github.com/Kotlin/kotlinx.collections.immutable), 
and I've attempted to prototype similar interfaces in TypeScript 
(https://larrydiamond.github.io/typescriptcollectionsframework/)
Python has a Tuple type which is an immutable list.   
Other Collections libraries exist for Java and some consider immutability important 
(https://github.com/google/guava/wiki/ImmutableCollectionsExplained).   The Eclipse
Collections library includes some immutable interfaces as well 
(https://www.eclipse.org/collections/javadoc/10.4.0/org/eclipse/collections/api/list/ImmutableList.html)
as does Apache (https://commons.apache.org/proper/commons-collections/javadocs/api-4.4/index.html)

This is a place where the developer community clearly wants the ability to 
create Unmodifyable Collections and the language definition I believe is the 
right place to lead to provide developers the capabilities they want.


Description
-----------

Create interfaces UnmodifiableCollection, UnmodifiableSet, UnmodifiableList, and UnmodifiableMap.
Modify Collection, Set, List, and Map to derive from these classes.
Move the read only methods to the Unmodified versions.

We will create an UnmodifiableIterator interface and a new method on the 
Unmodifiable interfaces to return one since the Iterator has a remove method.

The SortedSet interfaces itself exposes no mutable methods, however it extends Set so an 
UnmodifiableSortedSet interface will be created.  The SortedMap interface has a number 
of methods that return Set or Collection so an UnmodifiableSortedMap interface will be 
created with Immutable methods to provide similar functionality.

NavigableSet and NavigableMap do have methods that return mutable Collections so 
Unmodifyable versions of these interfaces will be created as well.



public interface UnmodifiableCollection<E> extends UnmodifiableIterable<E> {
    int size(); // existing
    boolean isEmpty(); // existing
    boolean contains(Object o); // existing
    Object[] toArray(); // existing
    <T> T[] toArray(T[] a); // existing
    boolean containsAll(UnmodifiableCollection<?> c); // existing
    boolean equals(Object o); // existing
    int hashCode(); // existing

    @Override
    default Spliterator<E> spliterator() { // existing
        return Spliterators.spliterator(this, 0);
    }

    UnmodifiableIterator<E> unmodifiableIterator();

    //stream and parallelStream?
}


public interface UnmodifiableIterator<E> {
    boolean hasNext(); // existing
    E next(); // existing
    default void forEachRemaining(Consumer<? super E> action) { // existing
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}



public interface UnmodifiableList<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    UnmodifiableIterator<E> unmodifiableIterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean containsAll(UnmodifiableCollection<?> c);
    boolean equals(Object o);
    int hashCode();
    E get(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    UnmodifiableListIterator<E> unmodifiableListIterator();
    UnmodifiableListIterator<E> unmodifiableListIterator(int index);
    UnmodifiableList<E> unmodifiableSubList(int fromIndex, int toIndex);

    @Override
    default Spliterator<E> spliterator() {
        if (this instanceof RandomAccess) {
            return new AbstractList.RandomAccessSpliterator<>(this);
        } else {
            return Spliterators.spliterator(this, Spliterator.ORDERED);
        }
    }

    @SuppressWarnings("unchecked")
    static <E> UnmodifiableList<E> uof() {
        return (List<E>) ImmutableCollections.EMPTY_LIST;
    }

    static <E> UnmodifiableList<E> uof(E e1) {
        return new ImmutableCollections.List12<>(e1);
    }

    static <E> UnmodifiableList<E> uof(E e1, E e2) {
        return new ImmutableCollections.List12<>(e1, e2);
    }

    static <E> UnmodifiableList<E> uof(E e1, E e2, E e3) {
        return ImmutableCollections.listFromTrustedArray(e1, e2, e3);
    }

    @SafeVarargs
    @SuppressWarnings("varargs")
    static <E> UnmodifiableList<E> uof(E... elements) {
        switch (elements.length) { // implicit null check of elements
            case 0:
                @SuppressWarnings("unchecked")
                var list = (List<E>) ImmutableCollections.EMPTY_LIST;
                return list;
            case 1:
                return new ImmutableCollections.List12<>(elements[0]);
            case 2:
                return new ImmutableCollections.List12<>(elements[0], elements[1]);
            default:
                return ImmutableCollections.listFromArray(elements);
        }
    }

    static <E> UnmodifiableList<E> ucopyOf(UnmodifiableCollection<? extends E> coll) {
        return ImmutableCollections.listCopy(coll);
    }
}

public interface UnmodifiableListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
}


public interface UnmodifiableMap<K, V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    UnmodifiableSet<K> unmodifiableKeySet();
    UnmodifiableCollection<V> unmodifiableValues();
    UnmodifiableSet<UnmodifiableMap.Entry<K, V>> unmodifiableEntrySet();

    interface Entry<K, V> {
        K getKey();
        V getValue();
        boolean equals(Object o);
        int hashCode();
        public static <K extends Comparable<? super K>, V> Comparator<UnmodifiableMap.Entry<K, V>> comparingByKey() {
            return (Comparator<UnmodifiableMap.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

        public static <K, V extends Comparable<? super V>> Comparator<UnmodifiableMap.Entry<K, V>> comparingByValue() {
            return (Comparator<UnmodifiableMap.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }

        public static <K, V> Comparator<UnmodifiableMap.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<UnmodifiableMap.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }

        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<UnmodifiableMap.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

    boolean equals(Object o);
    int hashCode();
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }

    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (UnmodifiableMap.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch (IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }


    @SuppressWarnings("unchecked")
    static <K, V> UnmodifiableMap<K, V> uof() {
        return (Map<K,V>) ImmutableCollections.EMPTY_MAP;
    }

    static <K, V> UnmodifiableMap<K, V> uof(K k1, V v1) {
        return new ImmutableCollections.Map1<>(k1, v1);
    }

    static <K, V> UnmodifiableMap<K, V> uof(K k1, V v1, K k2, V v2) {
        return new ImmutableCollections.MapN<>(k1, v1, k2, v2);
    }

    static <K, V> UnmodifiableMap<K, V> uof(K k1, V v1, K k2, V v2, K k3, V v3) {
        return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3);
    }

    @SafeVarargs
    @SuppressWarnings("varargs")
    static <K, V> UnmodifiableMap<K, V> uofEntries(Entry<? extends K, ? extends V>... entries) {
        if (entries.length == 0) { // implicit null check of entries array
            @SuppressWarnings("unchecked")
            var map = (Map<K,V>) ImmutableCollections.EMPTY_MAP;
            return map;
        } else if (entries.length == 1) {
            // implicit null check of the array slot
            return new ImmutableCollections.Map1<>(entries[0].getKey(),
                    entries[0].getValue());
        } else {
            Object[] kva = new Object[entries.length << 1];
            int a = 0;
            for (Entry<? extends K, ? extends V> entry : entries) {
                // implicit null checks of each array slot
                kva[a++] = entry.getKey();
                kva[a++] = entry.getValue();
            }
            return new ImmutableCollections.MapN<>(kva);
        }
    }

    static <K, V> Entry<K, V> entry(K k, V v) {
        // KeyValueHolder checks for nulls
        return new KeyValueHolder<>(k, v);
    }

    @SuppressWarnings({"rawtypes","unchecked"})
    static <K, V> UnmodifiableMap<K, V> ucopyOf(UnmodifiableMap<? extends K, ? extends V> umap) {
        if (umap instanceof ImmutableCollections.AbstractImmutableMap) {
            return (Map<K,V>)umap;
        } else {
            return (Map<K,V>)Map.uofEntries(map.entrySet().toArray(new Entry[0]));
        }
    }
}


public interface UnmodifiableSet<E> extends UnmodifiableCollection<E> {
    int size(); // existing
    boolean isEmpty(); // existing
    boolean contains(Object o); // existing
    Object[] toArray(); // existing
    <T> T[] toArray(T[] a); // existing
    boolean containsAll(UnmodifiableCollection<?> c); // existing
    boolean equals(Object o);
    int hashCode();
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT);
    }

    @SuppressWarnings("unchecked")
    static <E> UnmodifiableSet<E> uof() {
        return (Set<E>) ImmutableCollections.EMPTY_SET;
    }

    static <E> UnmodifiableSet<E> uof(E e1) {
        return new ImmutableCollections.Set12<>(e1);
    }

    static <E> UnmodifiableSet<E> uof(E e1, E e2) {
        return new ImmutableCollections.Set12<>(e1, e2);
    }

    static <E> UnmodifiableSet<E> uof(E e1, E e2, E e3) {
        return new ImmutableCollections.SetN<>(e1, e2, e3);
    }

    @SafeVarargs
    @SuppressWarnings("varargs")
    static <E> UnmodifiableSet<E> uof(E... elements) {
        switch (elements.length) { // implicit null check of elements
            case 0:
                @SuppressWarnings("unchecked")
                var set = (Set<E>) ImmutableCollections.EMPTY_SET;
                return set;
            case 1:
                return new ImmutableCollections.Set12<>(elements[0]);
            case 2:
                return new ImmutableCollections.Set12<>(elements[0], elements[1]);
            default:
                return new ImmutableCollections.SetN<>(elements);
        }
    }

    @SuppressWarnings("unchecked")
    static <E> UnmodifiableSet<E> ucopyOf(UnmodifiableCollection<? extends E> coll) {
        if (coll instanceof ImmutableCollections.AbstractImmutableSet) {
            return (Set<E>)coll;
        } else {
            return (Set<E>)Set.of(new HashSet<>(coll).toArray());
        }
    }
}


public interface UnmodifiableSortedMap<K,V> extends UnmodifiableMap<K,V> {
    Comparator<? super K> comparator();
    UnmodifiableSortedMap<K,V> subMap(K fromKey, K toKey);
    UnmodifiableSortedMap<K,V> headMap(K toKey);
    UnmodifiableSortedMap<K,V> tailMap(K fromKey);
    K firstKey();
    K lastKey();
    UnmodifiableSet<K> unmodifiableKeySet();
    UnmodifiableCollection<V> unmodifiableValues();
    UnmodifiableSet<UnmodifiableMap.Entry<K, V>> unmodifiableEntrySet();
}


public interface UnmodifiableNavigableSet<E> extends UnmodifiableSortedSet<E> {
    E lower(E e);
    E floor(E e);
    E ceiling(E e);
    E higher(E e);
    E pollFirst();
    E pollLast();
    UnmodifiableIterator<E> unmodifiableIterator();
    UnmodifiableNavigableSet<E> unmodifiableDescendingSet();
    UnmodifiableIterator<E> unmodifiableDescendingIterator();
    UnmodifiableNavigableSet<E> unmodifiableSubSet
                          (E fromElement, boolean fromInclusive,
                           E toElement,   boolean toInclusive);
    UnmodifiableNavigableSet<E> unmodifiableHeadSet(E toElement, boolean inclusive);
    UnmodifiableNavigableSet<E> unmodifiableTailSet(E fromElement, boolean inclusive);
    UnmodifiableSortedSet<E> unmodifiableSubSet(E fromElement, E toElement);
    UnmodifiableSortedSet<E> unmodifiableHeadSet(E toElement);
    UnmodifiableSortedSet<E> unmodifiableTailSet(E fromElement);
}

public interface UnmodifiableNavigableMap<K,V> extends UnmodifiableSortedMap<K,V> {
    UnmodifiableMap.Entry<K,V> unmodifiableLowerEntry(K key);
    K lowerKey(K key);
    UnmodifiableMap.Entry<K,V> unmodifiableFloorEntry(K key);
    K floorKey(K key);
    UnmodifiableMap.Entry<K,V> unmodifiableCeilingEntry(K key);
    K ceilingKey(K key);
    UnmodifiableMap.Entry<K,V> unmodifiableHigherEntry(K key);
    K higherKey(K key);
    UnmodifiableMap.Entry<K,V> unmodifiableFirstEntry();
    UnmodifiableMap.Entry<K,V> unmodifiableLastEntry();
    UnmodifiableMap.Entry<K,V> unmodifiablePollFirstEntry();
    UnmodifiableMap.Entry<K,V> unmodifiablePollLastEntry();
    UnmodifiableNavigableMap<K,V> unmodifiableDescendingMap();
    UnmodifiableNavigableSet<K> unmodifiableNavigableKeySet();
    UnmodifiableNavigableSet<K> unmodifiableDescendingKeySet();
    UnmodifiableNavigableMap<K,V> unmodifiableSubMap
                            (K fromKey, boolean fromInclusive,
                             K toKey,   boolean toInclusive);
    UnmodifiableNavigableMap<K,V> unmodifiableHeadMap(K toKey, boolean inclusive);
    UnmodifiableNavigableMap<K,V> unmodifiableTailMap(K fromKey, boolean inclusive);
    UnmodifiableSortedMap<K,V> unmodifiableSubMap(K fromKey, K toKey);
    UnmodifiableSortedMap<K,V> unmodifiableHeadMap(K toKey);
    UnmodifiableSortedMap<K,V> unmodifiableTailMap(K fromKey);
}


public interface Collection<E> extends UnmodifiableCollection<E>, Iterable<E> {
public interface Iterator<E> extends UnmodifiableIterator<E> {
public interface List<E> extends Collection<E>, UnmodifiableList<E> {
public interface ListIterator<E> extends Iterator<E>, UnmodifiableListIterator<E> {
public interface Map<K, V> extends UnmodifiableMap<K, V> {
public interface Set<E> extends Collection<E>, UnmodifiableSet<E> {
public interface SortedSet<E> extends Set<E>, UnmodifiableSortedSet<E> {
public interface SortedMap<K,V> extends Map<K,V>, UnmodifiableSortedMap<K, V> {
public interface NavigableSet<E> extends SortedSet<E>, UnmodifiableNavigableSet<E> {
public interface NavigableMap<K,V> extends SortedMap<K,V>, UnmodifiableNavigableMap<K, V> {



Alternatives
------------

The goals I wanted to achieve was to provide immutability for collections and to provide 
checking at compile time rather than at run time.   There are third party libraries but 
I could use (Guava, Eclipse, Apache), but then I'd be bringing in another dependency and 
then we have to discuss as a team which of the third party dependencies we want to use 
which leads inevitably to all of them being imported by different teams and the need to 
have code that understands multiple Collection frameworks.   If the language definition 
provides this capability then the proliferation of third party dependencies and the need
to understand all of them can be prevented.


Testing
-------

A number of new unit tests will need to be written to validate that attempts to 
modify an instance of an Unmodifyable interface fail at compile time.   
Fortunately, there is believed to be existing tests related to not being able to invoke
methods from derived interfaces.


Risks and Assumptions
---------------------

Users of Java may be very happy with their third party dependencies and 
may be uninterested at first with this new capability.   But if the language
provides this capability to all users then the need to bring in a third 
party dependency is reduced.

There is a lot of documentation to update.




More information about the jdk-dev mailing list