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