Search code examples
groovycollectionsunmodifiable

Why does Collections.unmodifiableCollection allow you to change the collection?


Suppose I have to following set:

Set<String> fruits = new HashSet<String>()
fruits.add("Apple")
fruits.add("Grapes")
fruits.add("Orange")

Set<String> unmodifiableFruits = Collections.unmodifiableSet(new HashSet<String>(fruits))
unmodifiableFruits.add("Peach") // -- Throws UnsupportedOperationException

Set<String> fruitSet = Collections.unmodifiableCollection(fruits)
fruitSet.add("Peach")
println(fruitSet)

If I were to use Collections.unmodifiableSet() it throws an exception when I attempt to use the add() method, but that isn't the case for Collections.unmodifiableCollection(). Why?

According the the documentation it should throw an error:

Returns an unmodifiable view of the specified collection. This method allows modules to provide users with "read-only" access to internal collections. Query operations on the returned collection "read through" to the specified collection, and attempts to modify the returned collection, whether direct or via its iterator, result in an UnsupportedOperationException.

All the code is written using Groovy 2.5.2


Solution

  • Short answer: adding Peach to this collection is possible, because Groovy does dynamic cast from Collection to Set type, so fruitSet variable is not of type Collections$UnmodifiableCollection but LinkedHashSet.

    Take a look at this simple exemplary class:

    class DynamicGroovyCastExample {
    
      static void main(String[] args) {
        Set<String> fruits = new HashSet<String>()
        fruits.add("Apple")
        fruits.add("Grapes")
        fruits.add("Orange")
    
        Set<String> fruitSet = Collections.unmodifiableCollection(fruits)
        println(fruitSet)
        fruitSet.add("Peach")
        println(fruitSet)
      }
    }
    

    In statically compiled language like Java, following line would throw compilation error:

    Set<String> fruitSet = Collections.unmodifiableCollection(fruits)
    

    This is because Collection cannot be cast to Set (it works in opposite direction, because Set extends Collection). Now, because Groovy is a dynamic language by design, it tries to cast to the type on the left hand side if the type returned on the right hand side is not accessible for the type on the left side. If you compile this code do a .class file and you decompile it, you will see something like this:

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    import groovy.lang.GroovyObject;
    import groovy.lang.MetaClass;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
    import org.codehaus.groovy.runtime.callsite.CallSite;
    
    public class DynamicGroovyCastExample implements GroovyObject {
        public DynamicGroovyCastExample() {
            CallSite[] var1 = $getCallSiteArray();
            MetaClass var2 = this.$getStaticMetaClass();
            this.metaClass = var2;
        }
    
        public static void main(String... args) {
            CallSite[] var1 = $getCallSiteArray();
            Set fruits = (Set)ScriptBytecodeAdapter.castToType(var1[0].callConstructor(HashSet.class), Set.class);
            var1[1].call(fruits, "Apple");
            var1[2].call(fruits, "Grapes");
            var1[3].call(fruits, "Orange");
            Set fruitSet = (Set)ScriptBytecodeAdapter.castToType(var1[4].call(Collections.class, fruits), Set.class);
            var1[5].callStatic(DynamicGroovyCastExample.class, fruitSet);
            var1[6].call(fruitSet, "Peach");
            var1[7].callStatic(DynamicGroovyCastExample.class, fruitSet);
        }
    }
    

    The interesting line is the following one:

    Set fruitSet = (Set)ScriptBytecodeAdapter.castToType(var1[4].call(Collections.class, fruits), Set.class);
    

    Groovy sees that you have specified a type of fruitSet as Set<String> and because right side expression returns a Collection, it tries to cast it to the desired type. Now, if we track what happens next we will find out that ScriptBytecodeAdapter.castToType() goes to:

    private static Object continueCastOnCollection(Object object, Class type) {
        int modifiers = type.getModifiers();
        Collection answer;
        if (object instanceof Collection && type.isAssignableFrom(LinkedHashSet.class) &&
                (type == LinkedHashSet.class || Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers))) {
            return new LinkedHashSet((Collection)object);
        }
    
    // .....
    }
    

    Source: src/main/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java#L253

    And this is why fruitSet is a LinkedHashSet and not Collections$UnmodifableCollection.

    enter image description here

    Of course it works just fine for Collections.unmodifiableSet(fruits), because in this case there is no cast needed - Collections$UnmodifiableSet implements Set so there is no dynamic casting involved.

    How to prevent similar situations?

    If you don't need any Groovy dynamic features, use static compilation to avoid problems with Groovy's dynamic nature. If we modify this example just by adding @CompileStatic annotation over the class, it would not compile and we would be early warned:

    enter image description here

    Secondly, always use valid types. If the method returns Collection, assign it to Collection. You can play around with dynamic casts in runtime, but you have to be aware of consequences it may have.

    Hope it helps.