Краткий ответ: добавление Peach
в эту коллекцию возможно, поскольку Groovy выполняет динамическое приведение типа Collection
к Set
, поэтому переменная fruitSet
имеет тип Collections$UnmodifiableCollection
, но LinkedHashSet
. * 1007. *
Взгляните на этот простой примерный класс:
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)
}
}
В статически скомпилированном языке, таком как Java, следующая строка выдаст ошибку компиляции:
Set<String> fruitSet = Collections.unmodifiableCollection(fruits)
Это потому, что Collection
нельзя привести к Set
(он работает в противоположном направлении, потому что Set
расширяет Collection
). Теперь, поскольку Groovy по своему дизайну является динамическим языком, он пытается привести к типу с левой стороны, если тип, возвращаемый с правой стороны, недоступен для типа с левой стороны. Если вы скомпилируете этот код, выполните файл .class
и декомпилируете его, вы увидите что-то вроде этого:
//
// 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);
}
}
Интересна следующая строка:
Set fruitSet = (Set)ScriptBytecodeAdapter.castToType(var1[4].call(Collections.class, fruits), Set.class);
Groovy видит, что вы указали тип fruitSet
как Set<String>
, и поскольку выражение в правой части возвращает Collection
, оно пытается привести его к нужному типу. Теперь, если мы проследим, что будет дальше, мы обнаружим, что ScriptBytecodeAdapter.castToType()
переходит к:
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);
}
// .....
}
Источник: src / main / org / codehaus / groovy / runtime / typehandling / DefaultTypeTransformation.java # L253
И вот почему fruitSet
- это LinkedHashSet
, а не Collections$UnmodifableCollection
.
Конечно, он отлично работает для Collections.unmodifiableSet(fruits)
, потому что в этом случае нет необходимости в приведении - Collections$UnmodifiableSet
реализует Set
, поэтому динамическое приведение не используется.
Как предотвратить подобные ситуации?
Если вам не нужны какие-либо динамические функции Groovy, используйте статическую компиляцию, чтобы избежать проблем с динамической природой Groovy. Если мы изменим этот пример, просто добавив к классу аннотацию @CompileStatic
, он не скомпилируется, и мы будем предупреждены заранее:
Во-вторых, всегда используйте допустимые типы. Если метод возвращает Collection
, присвойте его Collection
. Вы можете поиграть с динамическими приведениями во время выполнения, но вы должны знать о возможных последствиях.
Надеюсь, это поможет.