Краткий ответ: добавление Peach
в эту коллекцию возможно, потому что Groovy выполняет динамическое преобразование из типа Collection
в тип Set
, поэтому переменная fruitSet
имеет тип не Collections$UnmodifiableCollection
, а LinkedHashSet
.
Взгляните на этот простой примерный класс:
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
. Вы можете поиграться с динамическими приведениями во время выполнения, но вы должны знать о последствиях, которые это может иметь.
Надеюсь, поможет.
person
Szymon Stepniak
schedule
30.08.2018
Groovy
он компилируется. - person   schedule 30.08.2018unmodifiableCollection
(java.util.Collections$UnmodifiableCollection
, насколько мой тест идет) вSet
. Скорее всего, он создает новый модифицируемый набор (типjava.util.LinkedHashSet
был присвоенfruitSet
в моем тесте). - person ernest_k   schedule 30.08.2018unmodifiableCollection
создает модифицируемыйset
и возвращает его. - person   schedule 30.08.2018Set
... - person sp00m   schedule 30.08.2018unmodifiableSet
- лучший выбор? - person   schedule 30.08.2018unmodifiableCollection
, назначьте его переменнойCollection
в соответствии с типом возвращаемого метода (если только вы не проверите фактически возвращаемый тип данных, что было бы излишним, учитывая существованиеunmodifiableSet
) - person ernest_k   schedule 30.08.2018java.util.Collection
, и это не то, что внесло путаницу. Это поведение Groovy (неявное преобразование). Возможно, это описано в документации Groovy. Проверьте это: docs.groovy-lang.org/latest/html/documentation/ - person ernest_k   schedule 30.08.2018Set
взята из Groovy, но она ссылается на документациюCollections
из Java. Для любого читателя это сбивает с толку, если вы читаете документы и пытаетесь написать код. - person   schedule 31.08.2018