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 Collections 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 Collections.