为什么 Collections.unmodifiableCollection 允许您更改集合?

Why does Collections.unmodifiableCollection allow you to change the collection?

假设我必须遵循 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() 不是这种情况。为什么?

根据 documentation 它应该抛出一个错误:

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.

所有代码都是使用Groovy2.5.2

编写的

简短回答:可以将 Peach 添加到此集合中,因为 Groovy 会从 Collection 动态转换为 Set 类型,因此 fruitSet 变量不是 Collections$UnmodifiableCollection 类型,而是 LinkedHashSet.

类型

看看这个简单的示例 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)
  }
}

在像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> 并且因为右侧表达式 returns 和 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);
    }

// .....
}

Source: src/main/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java#L253

这就是为什么 fruitSetLinkedHashSet 而不是 Collections$UnmodifableCollection

当然它对 Collections.unmodifiableSet(fruits) 工作得很好,因为在这种情况下不需要转换 - Collections$UnmodifiableSet 实现 Set 所以不涉及动态转换。

如何防止类似情况发生?

如果您不需要任何 Groovy 动态特性,请使用静态编译来避免 Groovy 的动态特性出现问题。如果我们只是通过在 class 上添加 @CompileStatic 注释来修改此示例,它将无法编译,我们会提前警告:

其次,始终使用有效类型。如果方法returnsCollection,赋值给Collection。您可以在运行时使用动态转换,但您必须了解它可能产生的后果。

希望对您有所帮助。