Почему Collections.unmodifiableCollection позволяет изменять коллекцию? - PullRequest
0 голосов
/ 30 августа 2018

Предположим, я должен следовать 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)

Если бы я использовал Collections.unmodifiableSet(), он выдает исключение, когда я пытаюсь использовать метод add(), но это не относится к Collections.unmodifiableCollection(). Зачем?

Согласно документации должно выдаваться сообщение об ошибке:

Возвращает неизменяемое представление указанной коллекции. Этот метод позволяет модулям предоставлять пользователям доступ «только для чтения» к внутренним коллекции. Запрос операций над возвращенной коллекцией через "к указанной коллекции, и пытается изменить возвращенная коллекция, прямая или через ее итератор, приводит к UnsupportedOperationException.

Весь код написан с использованием Groovy 2.5.2

1 Ответ

0 голосов
/ 30 августа 2018

Краткий ответ: добавление 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.

enter image description here

Конечно, он отлично работает для Collections.unmodifiableSet(fruits), потому что в этом случае нет необходимости в приведении - Collections$UnmodifiableSet реализует Set, поэтому динамическое приведение не используется.

Как предотвратить подобные ситуации?

Если вам не нужны какие-либо динамические функции Groovy, используйте статическую компиляцию, чтобы избежать проблем с динамической природой Groovy. Если мы изменим этот пример, просто добавив к классу аннотацию @CompileStatic, он не скомпилируется, и мы будем предупреждены заранее:

enter image description here

Во-вторых, всегда используйте допустимые типы. Если метод возвращает Collection, присвойте его Collection. Вы можете поиграть с динамическими приведениями во время выполнения, но вы должны знать о возможных последствиях.

Надеюсь, это поможет.

...