Class that describes a immutable, ordered set

8.5k Views Asked by At

I need some class/interface name that describes an immutable, ordered set (in input order, like LinkedHashSet). I can of course just use the class like this:

class Foo {
    public final Set<Long> frozenOrderedSet;

    public Foo(List<Long> input) {
        frozenOrderedSet = Collections.unmodifiableSet(new LinkedHashSet(input));
    }
}

But this would not clarify my approach. I would like to make it clear to everyone reading the source that the Set is unmodifiable and unique while maintaining it's order with for(Long l : set){}.

4

There are 4 best solutions below

0
On BEST ANSWER

Guava's ImmutableSet provides a high-performance, immutable Set with reliable, user-specified iteration order. There are also variations like ImmutableSortedSet.

0
On

The simplest way would be extend Set to create a custom immutable Set.

public CustomImmutableSet(){ return Collections.unmodifiableSet(new LinkedHashSet(input)); }

This way it will make it clear to everyone reading the source that the Set is unmodifiable and unique

0
On

Bozho in question Java Immutable Collections:

Unmodifiable collections are usually read-only views (wrappers) of other collections. You can't add, remove or clear them, but the underlying collection can change.

Immutable collections can't be changed at all - they don't wrap another collection - they have their own elements.

Here's a quote from guava's ImmutableList

Unlike Collections.unmodifiableList(java.util.List), which is a view of a separate collection that can still change, an instance of ImmutableList contains its own private data and will never change.

So, basically, in order to get an immutable collection out of a mutable one, you have to copy its elements to the new collection, and disallow all operations.

So Basically you might create subclass of class TreeSet and override all :add, remove etc. methods to just throw an Exception saying that this is immutable collection. Also you would have to create copying constructor in this class with one parameter TreeSet setToCopyFrom.

0
On

The java.util.Collection source internally implements the methods that generate unmodifiable collections using static package-scoped internal classes.

You can copy the implementation for these classes from the source in the JDK into your package and make it public. Then you can use type specifiers in your code that explicitly state that a collection is unmodifiable, sorted, etc.

Copying the implementation from the JDK is not an ideal solution for many reasons, so consider the accepted answer which recommends using Guava. Or also consider just living with the limitation that unmodifiable collections methods in the java Collection class don't return type names which state that the collections are indeed immutable.

However, if you consider the alternatives and accept the drawbacks of the approach of copying code from the JDK, then this answer will work.

There is, no doubt, some reason that the JDK developers chose not to make these types public, but I don't know what it is.

Modified code from the JDK

This code is from JDK 21, the implementation is unchanged from the JDK, but the classes are defined to be public, so they can be used from any package.

UnmodifiableCollection.java

package org.example.unmodifable;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Stream;

class UnmodifiableCollection<E> implements Collection<E>, Serializable {
    @java.io.Serial
    private static final long serialVersionUID = 1820017752578914078L;

    @SuppressWarnings("serial") // Conditionally serializable
    final Collection<? extends E> c;

    public UnmodifiableCollection(Collection<? extends E> c) {
        if (c == null)
            throw new NullPointerException();
        this.c = c;
    }

    public int size() {
        return c.size();
    }

    public boolean isEmpty() {
        return c.isEmpty();
    }

    public boolean contains(Object o) {
        return c.contains(o);
    }

    public Object[] toArray() {
        return c.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return c.toArray(a);
    }

    public <T> T[] toArray(IntFunction<T[]> f) {
        return c.toArray(f);
    }

    public String toString() {
        return c.toString();
    }

    public Iterator<E> iterator() {
        return new Iterator<>() {
            private final Iterator<? extends E> i = c.iterator();

            public boolean hasNext() {
                return i.hasNext();
            }

            public E next() {
                return i.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

            @Override
            public void forEachRemaining(Consumer<? super E> action) {
                // Use backing collection version
                i.forEachRemaining(action);
            }
        };
    }

    public boolean add(E e) {
        throw new UnsupportedOperationException();
    }

    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }

    public boolean containsAll(Collection<?> coll) {
        return c.containsAll(coll);
    }

    public boolean addAll(Collection<? extends E> coll) {
        throw new UnsupportedOperationException();
    }

    public boolean removeAll(Collection<?> coll) {
        throw new UnsupportedOperationException();
    }

    public boolean retainAll(Collection<?> coll) {
        throw new UnsupportedOperationException();
    }

    public void clear() {
        throw new UnsupportedOperationException();
    }

    // Override default methods in Collection
    @Override
    public void forEach(Consumer<? super E> action) {
        c.forEach(action);
    }

    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Spliterator<E> spliterator() {
        return (Spliterator<E>) c.spliterator();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> stream() {
        return (Stream<E>) c.stream();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> parallelStream() {
        return (Stream<E>) c.parallelStream();
    }
}

UnmodifiableSet.java

package org.example.unmodifable;

import java.io.Serializable;
import java.util.Set;

class UnmodifiableSet<E> extends UnmodifiableCollection<E>
        implements Set<E>, Serializable {
    @java.io.Serial
    private static final long serialVersionUID = -9215047833775013803L;

    public UnmodifiableSet(Set<? extends E> s) {
        super(s);
    }

    public boolean equals(Object o) {
        return o == this || c.equals(o);
    }

    public int hashCode() {
        return c.hashCode();
    }
}

UnmodifiableSortedSet.java

package org.example.unmodifable;

import java.io.Serializable;
import java.util.Comparator;
import java.util.SortedSet;

public class UnmodifiableSortedSet<E>
        extends UnmodifiableSet<E>
        implements SortedSet<E>, Serializable {
    @java.io.Serial
    private static final long serialVersionUID = -4929149591599911165L;
    @SuppressWarnings("serial") // Conditionally serializable
    private final SortedSet<E> ss;

    public UnmodifiableSortedSet(SortedSet<E> s) {super(s); ss = s;}

    public Comparator<? super E> comparator() {return ss.comparator();}

    public SortedSet<E> subSet(E fromElement, E toElement) {
        return new UnmodifiableSortedSet<>(ss.subSet(fromElement,toElement));
    }
    public SortedSet<E> headSet(E toElement) {
        return new UnmodifiableSortedSet<>(ss.headSet(toElement));
    }
    public SortedSet<E> tailSet(E fromElement) {
        return new UnmodifiableSortedSet<>(ss.tailSet(fromElement));
    }

    public E first()                   {return ss.first();}
    public E last()                    {return ss.last();}
}

Test case

UnmodifiableSortedSetTest.java

package org.example.unmodifable;

import java.util.List;
import java.util.TreeSet;

public class UnmodifiableSortedSetTest {
    public static void main(String[] args) {
        UnmodifiableSortedSet<String> frozenAnimals = new UnmodifiableSortedSet<>(
                new TreeSet<>(List.of("dog", "cat", "lion", "cat"))
        );

        System.out.println(frozenAnimals);

        try {
            frozenAnimals.add("pigeon");
        } catch (UnsupportedOperationException e) {
            System.out.println("You can't modify the frozen list of animals.");
        }
    }
}

Test output:

[cat, dog, lion]
You can't modify the frozen list of animals.

What I use in my code with Java records

I came across this question because I wanted an immutable ordered set as a field in a Java record.

A solution that provides immutable sets as record fields is provided in the answer to:

However, the solution Set.copyOf(set), doesn't work with sorted sets as it just returns a generic Set type, and not a sorted set, so I ended up using code similar to that below:

record Foo(
        SortedSet<String> values
) {
    public Foo {
        values = Collections.unmodifiableSortedSet(
            new TreeSet<>(values)
        );
    }
}

It lets you know that the set is sorted but not that it is also immutable. This is a limitation I can live with in my code base and is preferable to me, than using the copied class approach in this answer or adding the Guava library to my code.