I can understand why the concurrency classes are difficult to write: it's very easy to get difficult-to-see bugs there.
Java has a nice way to avoid such bugs when writing immutable Collection
classes: each kind of Collection
has a method similar to java.util.Collections.unmodifiableSet(someSet), which will give you a wrapper that lets you see the underlying Collection
but blocks all mutation methods. It's only a wrapper, though: you can still change the underlying Collection
if you keep a reference to it lying around, so don't do that. Also, immediately clone and wrap any Collection
s that come from outside your control, since the programmers who handed them to you might modify them later, mutating your nice immutable data.
If you want to make a library to handle all these pedantic precautions for you, it's time-consuming but not that hard. To save you time, I've included an example of a minimally-optimized
FunctionalHashSet
with all necessary mutation prevention.
I made an abstract superclass by going down the API listing for Set
(not forgetting toString
). For nonmutating methods, I simply pass them on to the underlying Set
. For mutating methods, I throw UnsupportedOperationException
and provide alternative functional-style methods.
Here's that abstract class, FunctionalSet
:
import java.util.Collections;
import java.util.Collection;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
public abstract class FunctionalSet<E> implements Set<E> {
// final to prevent mutations through reassignment.
protected final Set<E> set;
// private to prevent any use of the default constructor.
private FunctionalSet()
{ this.set = null; }
// unmodifiableSet to prevent mutations through Iterator and in subclasses.
protected FunctionalSet(final Set<E> set)
{ this.set = Collections.unmodifiableSet(set); }
public abstract FunctionalSet<E> clone();
public abstract FunctionalSet<E> fAdd(final E element);
public abstract FunctionalSet<E> fAddAll(final Collection<? extends E> elements);
public abstract FunctionalSet<E> fRemove(final Object element);
public abstract FunctionalSet<E> fRemoveAll(final Collection<?> elements);
public abstract FunctionalSet<E> fRetainAll(final Collection<?> elements);
protected abstract FunctionalSet<E> newFSet(final Set<E> newSet);
protected abstract Set<E> newSet();
protected abstract Set<E> cloneSet();
protected final FunctionalSet<E> __fAdd(final E element) {
if (set.contains(element)) return this;
final Set<E> newSet = cloneSet();
newSet.add(element);
return newFSet(newSet);
}
protected final FunctionalSet<E> __fAddAll(final Collection<? extends E> elements) {
if (set.containsAll(elements)) return this;
final Set<E> newSet = cloneSet();
newSet.addAll(elements);
return newFSet(newSet);
}
protected final FunctionalSet<E> __fRemove(final Object element) {
if (!set.contains(element)) return this;
final Set<E> newSet = cloneSet();
newSet.remove(element);
return newFSet(newSet);
}
protected final Set<E> __fRemoveAll(final Collection<?> elements) {
boolean hasNone = true;
for (final Object element : elements) {
if (set.contains(element)) {
hasNone = false;
break;
}
}
if (hasNone) return this;
final Set<E> newSet = cloneSet();
newSet.removeAll(elements);
return newFSet(newSet);
}
@SuppressWarnings("unchecked")
protected final Set<E> __fRetainAll(final Collection<?> rawElements) {
final Set elements = rawElements instanceof Set ? (Set) rawElements : new HashSet(rawElements);
// If set is a subset of elements, we don't remove any of the elements.
if (set.size() <= elements.size() && elements.containsAll(set)) return this;
final Set<E> newSet = newSet();
for (final E element : set) {
if (elements.contains(element)) newSet.add(element);
}
return newFSet(newSet);
}
private final UnsupportedOperationException unsupported(final String call, final String goodCall) {
return new UnsupportedOperationException(
String.format(this.getClass().getName() + "s are immutable. Use %s instead of %s.", goodCall, call)
);
}
public final boolean add(final E element)
{ throw unsupported("add", "fAdd"); }
public final boolean addAll(final Collection<? extends E> elements)
{ throw unsupported("addAll", "fAddAll"); }
public final void clear()
{ throw unsupported("clear", "new " + this.getClass().getName() + "()"); }
public final boolean remove(final Object element)
{ throw unsupported("remove", "fRemove"); }
public final boolean removeAll(final Collection<?> elements)
{ throw unsupported("removeAll", "fRemoveAll"); }
public final boolean retainAll(final Collection<?> elements)
{ throw unsupported("retainAll", "fRetainAll"); }
public final boolean contains(final Object element)
{ return set.contains(element); }
public final boolean containsAll(final Collection<?> elements)
{ return set.containsAll(elements); }
public final boolean equals(final Object object)
{ return set.equals(object); }
public final int hashCode()
{ return set.hashCode(); }
public final boolean isEmpty()
{ return set.isEmpty(); }
public final Iterator<E> iterator()
{ return set.iterator(); }
public final int size()
{ return set.size(); }
public final Object[] toArray()
{ return set.toArray(); }
public final <E> E[] toArray(final E[] irrelevant)
{ return set.toArray(irrelevant); }
public final String toString()
{ return set.toString(); }
}
In the implementation, there's very little left to do. I supply a few constructors and utility methods and simply use the default implementations of all the mutating methods.
Here's an implementation, FunctionalHashSet
. :
import java.util.Collection;
import java.util.Set;
import java.util.HashSet;
public final class FunctionalHashSet<E> extends FunctionalSet<E> implements Cloneable {
public static final FunctionalHashSet EMPTY = new FunctionalHashSet();
public FunctionalHashSet()
{ super(new HashSet<E>()); }
public FunctionalHashSet(final HashSet<E> set)
{ this(set, true); }
@SuppressWarnings("unchecked")
private FunctionalHashSet(final HashSet<E> set, final boolean clone)
{ super(clone ? (HashSet<E>) set.clone() : set); }
public FunctionalHashSet(final Collection<E> elements)
{ this(new HashSet<E>(elements)); }
protected FunctionalHashSet<E> newFSet(final Set<E> newSet)
{ return new FunctionalHashSet<E>((HashSet<E>) newSet, false); }
protected HashSet<E> newSet()
{ return new HashSet<E>(); }
@SuppressWarnings("unchecked")
protected HashSet<E> cloneSet()
{ return new HashSet<E>(set); }
public FunctionalHashSet<E> clone()
{ return this; }
public FunctionalHashSet<E> fAdd(final E element)
{ return (FunctionalHashSet<E>) __fAdd(element); }
public FunctionalHashSet<E> fAddAll(final Collection<? extends E> elements)
{ return (FunctionalHashSet<E>) __fAddAll(elements); }
public FunctionalHashSet<E> fRemove(final Object element)
{ return (FunctionalHashSet<E>) __fRemove(element); }
public FunctionalHashSet<E> fRemoveAll(final Collection<?> elements)
{ return (FunctionalHashSet<E>) __fRemoveAll(elements); }
public FunctionalHashSet<E> fRetainAll(final Collection<?> elements)
{ return (FunctionalHashSet<E>) __fRetainAll(elements); }
}
A few notes :
- In every single functional mutation method, I check whether there will actually be any changes. If not, I just return the exact same
FunctionalSet
.
- In
clone
, I just return the exact same FunctionalSet
- Running
set
through java.util.Collections.unmodifiableSet
and declaring it final
prevents two sources of mutation: users can't mutate through the Iterator
and implementors can't accidentally mutate in their implementations.
You can take this and modify it a bit to support other Collection
s.