Почему Collections.unmodifiedCollection позволяет изменять коллекцию?

Предположим, мне нужно выполнить 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.


person Community    schedule 30.08.2018    source источник
comment
Это дело.   -  person Louis Wasserman    schedule 30.08.2018
comment
@ sp00m - В Groovy он компилируется.   -  person    schedule 30.08.2018
comment
Я подозреваю, что Groovy выполняет какое-то неявное преобразование из типа, возвращаемого unmodifiableCollection (java.util.Collections$UnmodifiableCollection, насколько мой тест идет) в Set. Скорее всего, он создает новый модифицируемый набор (тип java.util.LinkedHashSet был присвоен fruitSet в моем тесте).   -  person ernest_k    schedule 30.08.2018
comment
@ernest_k - Это вводит в заблуждение. В документации четко указано Возвращает неизменяемое представление указанной коллекции. Похоже, это не так.   -  person    schedule 30.08.2018
comment
Подождите, вы правы: ideone.com/qIzCCm!   -  person sp00m    schedule 30.08.2018
comment
Что ж, это могло бы ввести в заблуждение только в том случае, если бы неявные преобразования где-то не были указаны ;-)   -  person GhostCat    schedule 30.08.2018
comment
@sp00m - ;Д. Я смотрю на это в Eclipse IDE прямо сейчас.   -  person    schedule 30.08.2018
comment
@GhostCat - я не смог его найти, но, может быть, я ищу не в том месте, но где-нибудь сказано, что unmodifiableCollection создает модифицируемый set и возвращает его.   -  person    schedule 30.08.2018
comment
Ну, не совсем так, там написано немодифицируемая [...] коллекция. Мне интересно, как Groovy справляется с этим странным неявным преобразованием в Set...   -  person sp00m    schedule 30.08.2018
comment
@ernest_k - Довольно интересно и неожиданно при чтении документации.   -  person    schedule 30.08.2018
comment
@Света согласна. Но на вашем месте я бы постарался избегать преобразований типов, которые я явно не контролирую (лично я считаю это рискованным).   -  person ernest_k    schedule 30.08.2018
comment
@ernest_k - В этом случае unmodifiableSet - лучший выбор?   -  person    schedule 30.08.2018
comment
@ Света В вашем случае это явно лучший вариант из двух. В случае, если вы должны использовать unmodifiableCollection, назначьте его переменной Collection в соответствии с типом возвращаемого метода (если только вы не проверите фактически возвращаемый тип данных, что было бы излишним, учитывая существование unmodifiableSet)   -  person ernest_k    schedule 30.08.2018
comment
@ernest_k - Согласен. Было бы хорошей идеей уведомить авторов документов? Может быть, дать им понять, что это заблуждение?   -  person    schedule 30.08.2018
comment
Документация, на которую вы ссылаетесь, предназначена для java.util.Collection, и это не то, что внесло путаницу. Это поведение Groovy (неявное преобразование). Возможно, это описано в документации Groovy. Проверьте это: docs.groovy-lang.org/latest/html/documentation/   -  person ernest_k    schedule 30.08.2018
comment
@ernest_k - документация Set взята из Groovy, но она ссылается на документацию Collections из Java. Для любого читателя это сбивает с толку, если вы читаете документы и пытаетесь написать код.   -  person    schedule 31.08.2018


Ответы (1)


Краткий ответ: добавление 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
comment
Итак, лучше использовать Collections.unmodifiableSet? Учитывая, что я знаю, что моя коллекция представляет собой набор, это самый безопасный метод для вызова, верно? - person ; 31.08.2018
comment
@Света Правильно. Статическая компиляция не позволит вам присвоить Collection Set, поэтому всегда лучше быть как можно более явным. - person Szymon Stepniak; 31.08.2018
comment
Было бы неплохо уведомить авторов документации о том, что на странице Collections.unmodifiableCollection нет точного описания того, что делает метод? Страница Set взята из Groovy, но ссылается на Collections.unmodifiableCollection из Java. Любому читателю это может показаться запутанным. - person ; 31.08.2018
comment
Документация Collections.unmodifiableCollection() в порядке, и она возвращает то, что описывает. Проблема, с которой вы столкнулись, — это функция Groovy для преобразования несовместимых типов. Правостороннее выражение Collections.unmodifiableCollection() вернуло неизменяемую коллекцию, затем Groovy сравнил тип с левой стороны и нашел Set<String>, а поскольку Collection нельзя преобразовать в Set, он применил ScriptBytecodeAdapter.castToType() для создания Set из заданного Collection. Это просто ненормальное использование, основанное на динамическом преобразовании типа Collection в Set. - person Szymon Stepniak; 31.08.2018
comment
Проблема заключается в том, что Set документация Groovy ссылается на документацию Java о Collections.unmodifiableCollection(), что может дать читателям представление о том, что Collections.unmodifiableCollection() как в Groovy, так и в Java работает одинаково, но, как вы продемонстрировали, это не так. В Java Collections.unmodifiableCollection() выдает исключение, когда вы пытаетесь вызвать add(), как объясняется в документации. В Groovy он этого не делает, как вы продемонстрировали, он создал Set из данной коллекции. - person ; 31.08.2018
comment
Было бы неплохо получить уведомление о поведении перед использованием Collections.unmodifiableCollection(). - person ; 31.08.2018