4

假设我必须遵循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 编写

4

1 回答 1

2

简短的回答:添加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(它以相反的方向工作,因为Setextends 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 看到您指定了fruitSetas类型,Set<String>并且因为右侧表达式返回 a 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是 aLinkedHashSet而不是Collections$UnmodifableCollection

在此处输入图像描述

当然,它适用于Collections.unmodifiableSet(fruits),因为在这种情况下不需要强制转换 -Collections$UnmodifiableSet实现Set,因此不涉及动态强制转换。

如何预防类似情况?

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

在此处输入图像描述

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

希望能帮助到你。

于 2018-08-30T19:25:42.133 回答