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