在Groovy中,List和List<String>是否相同?

19

问题1

在Groovy中使用List(对象列表)还是List<String>(字符串列表)是否无关紧要?

在下面的代码示例中,这两个列表最终都变成了一个ArrayList(对象的ArrayList)。本来期望第二个列表会是ArrayList<String>(字符串的ArrayList)。

Groovy 是否在编译类时丢失类型信息,并在执行编译后的类时推断类型?

示例 1

List untypedList = ["a", "b", "c"]
List<String> typedList = ["a", "b", "c"]

println "Untyped list List:       ${untypedList.getClass()}"
println "Typed list List<String>: ${typedList.getClass()}"

输出1

Untyped list List:       class java.util.ArrayList
Typed list List<String>: class java.util.ArrayList // Would have expected ArrayList<String>

问题2

在下面的示例中,我希望行typedList << new Integer(1)会因为我试图将一个int放入一个字符串列表中而失败。有人能解释一下为什么我可以将int添加到String类型的List中吗?

输出结果显示它仍然是一个Integer,即它没有即时转换为一个String"1"。

示例2

List untypedList = ["a", "b", "c"]
List<String> typedList = ["a", "b", "c"]

untypedList << new Integer(1)
typedList << new Integer(1) // Why does this work? Shouldn't an exception be thrown?

println "Types:"
println "Untyped list List:       ${untypedList.getClass()}"
println "Typed list List<String>: ${typedList.getClass()}"

println "List contents:"
println untypedList
println typedList

println "Untyped list:"
untypedList.each { println it.getClass() }
println "Typed list:"
typedList.each { println it.getClass() }

输出 2

Types:
Untyped list List:       class java.util.ArrayList
Typed list List<String>: class java.util.ArrayList
List contents:
[a, b, c, 1]
[a, b, c, 1]
Untyped list:
class java.lang.String
class java.lang.String
class java.lang.String
class java.lang.Integer
Typed list:
class java.lang.String
class java.lang.String
class java.lang.String
class java.lang.Integer

1
https://dev59.com/o3DXa4cB1Zd3GeqP_2Ui - tim_yates
@tim_yates:所以根据您发布的链接,答案是“[...]这意味着类型参数在编译时不会被检查,并且在运行时不可用。”这意味着我无法阻止任何人将int放入List<String>中,这反过来又意味着我只能确定List<String>包含类型为object的对象。正确吗? - Lernkurve
3
好的,除非你使用@CompileStatic,否则是这样的。 - tim_yates
3个回答

28

当以Groovy“正常”方式运行时,在编译之前会丢弃泛型,因此只存在于源代码中作为对开发人员的有用提示。

但是,您可以使用@CompileStatic@TypeChecked来使Groovy尊重这些泛型并在编译时检查事物的类型。

举个例子,假设我有以下项目结构:

project
 |---- src
 |      |---- main
 |             |---- groovy
 |                    |---- test
 |                           |---- ListDelegate.groovy
 |                           |---- Main.groovy
 |---- build.gradle

使用以下代码:

build.gradle

apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.2.1'
}

task( runSimple, dependsOn:'classes', type:JavaExec ) {
    main = 'test.Main'
    classpath = sourceSets.main.runtimeClasspath
}

ListDelegate.groovy

package test

class ListDelegate<T> {
    @Delegate List<T> numbers = []
}

主要.groovy

package test

class Main {
    static main( args ) {
        def del = new ListDelegate<Integer>()
        del << 1
        del << 'tim'
        println del
    }
}

现在,运行gradle runSimple命令会得到以下输出:

:compileJava UP-TO-DATE
:compileGroovy
:processResources UP-TO-DATE
:classes
:runSimple
[1, tim]

BUILD SUCCESSFUL

Total time: 6.644 secs

所以您可以看到,泛型被丢弃了,只是将IntegersStrings添加到了我们原本只希望包含Integers的列表中,但它却奇迹般地工作了。

现在,如果我们将 ListDelegate.groovy 更改为:

package test

import groovy.transform.*

@CompileStatic
class ListDelegate<T> {
    @Delegate List<T> numbers = []
}

并再次运行:

:compileJava UP-TO-DATE
:compileGroovy
:processResources UP-TO-DATE
:classes
:runSimple
[1, tim]

BUILD SUCCESSFUL

Total time: 6.868 secs

我们得到了相同的输出!!这是因为虽然ListDelegate现在已经静态编译了,但我们的Main类仍然是动态的,在构建ListDelegate之前仍会丢弃泛型...所以我们也可以将Main.groovy更改为:

package test

import groovy.transform.*

@CompileStatic
class Main {
    static main( args ) {
        def del = new ListDelegate<Integer>()
        del << 1
        del << 'tim'
        println del
    }
}

现在重新运行gradle runSimple,我们得到:

:compileJava UP-TO-DATE
:compileGroovy
startup failed:
/Users/tyates/Code/Groovy/generics/src/main/groovy/test/Main.groovy: 10:
    [Static type checking] - Cannot find matching method test.ListDelegate#leftShift(java.lang.String).
    Please check if the declared type is right and if the method exists.
 @ line 10, column 9.
           del << 'tim'
           ^

1 error

:compileGroovy FAILED

正如你所预期的,这会导致我们无法将一个String添加到我们声明的Integer列表中。

实际上,你只需要对Main.groovy类进行CompileStatic编译,就可以发现这个错误,但我总是喜欢在可以使用它的地方使用它,而不仅仅是在需要的地方。


耶茨先生,回答太棒了! - mikemil
非常有用,如果我的方法返回一个 [boolean, String] 列表,在方法定义中我应该省略返回类型还是将其放置为 List - b.b3rn4rd
我会将其封装在一个新类中,该类具有布尔值和字符串。 - tim_yates

4
正如@tim_yates所指出的,可以通过@TypeChecked/@CompileStatic注解启用编译时检查。
另一种选择是通过使用Collections.checkedList()包装集合来启用运行时类型检查。虽然这不使用泛型或声明类型,但有时在松散类型的动态代码中强制执行它更符合逻辑。这是一个Java平台特性,不特定于Groovy。
示例:
// no type checking:
list1 = ["a", "b", "c"]
list1 << 1
assert list1 == ["a", "b", "c", 1]
// type checking
list2 = Collections.checkedList(["a", "b", "c"], String)
list2 << 1
// ERROR java.lang.ClassCastException:
// Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String

3

来自维基百科,针对Java:

泛型在编译时进行类型正确性检查。然后使用一种称为类型擦除的过程删除泛型类型信息。例如,List将转换为非泛型类型List,该类型通常包含任意对象。编译时检查确保生成的代码是类型正确的。

此类型信息适用于编译器和IDE。Groovy基于Java并继承相同的泛型原则。

另一方面,Groovy是一种更动态的语言,因此可能是它不在编译时检查类型的原因。在我看来,对于Groovy来说,这是某种代码注释,有时非常有用。

PS @tim_yates建议一个关于Generics的Groovy文档链接,证实了这一点:

Groovy目前会进一步丢弃泛型信息“在源级别”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接