Groovy强制类型转换集合是否需要?

9

我有一些用Java编写的代码,其中使用了泛型。这是一个简单的版本:

// In Java
public interface Testable {
    void test();
}

public class TestableImpl implements Testable {
    @Override
    public void test(){
        System.out.println("hello");
    }
}

public class Test {
    public <T extends Testable> void runTest(Collection<T> ts){
        System.out.println("Collection<T>");
        for(T t: ts)
            t.test();
    }

    public void runTest(Object o){
        System.out.println("Object");
        System.out.println(o);
    }
}


// in Groovy - this is how I have to use the code
Test test = new Test()
test.runTest([new TestableImpl(), new TestableImpl()]) 
test.runTest([1,2,3]) //exception here

我很惊讶第二个方法调用被派发到了错误的方法(在我的Java理解中是错误的)。而不是调用Object重载,却调用了Collection

我正在使用Groovy 2.1.9,Windows 7。

异常信息如下:

Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: 
   Cannot cast object '1' with class 'java.lang.Integer' to class 'Testable'
org.codehaus.groovy.runtime.typehandling.GroovyCastException:
   Cannot cast object '1' with class 'java.lang.Integer' to class 'Testable'

为什么?如何解决这个问题?

如何让Groovy调用与Java相同的方法?


编辑:为了进一步解释这个案例,我想写一个Spock测试(想象一下该方法返回某些东西,比如一个字符串..):

def "good dispatch"(in,out) {
    expect:
    test.runTest(in) == out

    where:
    in                   | out
    new Object()         | "a value for Object"
    new Integer(123)     | "a value for Object"
    [1,2,3]              | "a value for Object"
    [new TestableImpl()] | "a value for Testable Collection"

}

1
也许相关:https://dev59.com/KGTWa4cB1Zd3GeqPF7Wn - tim_yates
4个回答

8

其他人已经提出了可能解决您问题的方法,但这里是为什么会发生这种情况。

Groovy是一种动态语言,使用运行时类型信息来调用正确的方法。另一方面,Java根据静态类型确定将使用哪种方法。

一个简单的示例,演示了JAVA和GROOVY之间的差异:

void foo(Collection coll) {
    System.out.println("coll")   
}

void foo(Object obj) {
    System.out.println("obj")   
}

GROOVY 中:

Object x = [1,2,3] //dynamic type at invocation time will be ArrayList
foo(x)
//OUT: coll

JAVA中:

Object x = Arrays.asList(1,2,3);
foo(x);
//OUT: obj
Collection x = Arrays.asList(1,2,3);
foo(x);
//OUT: coll

现在看看你的例子(它实际上与泛型的使用没有任何关系):
test.runTest([new TestableImpl(), ...]) //ArrayList --> collection method will be used
test.runTest([1,2,3]) //also ArrayList --> best match is the collection variant

1
你已经解释了问题,但没有提供如何避免它的答案。 - Leonard Brünings
1
true,如果你读了我的第一句话,那就是我写的。问题的一部分是“为什么?”我回答了这个问题。可以使用@CompileStatic或类型转换,如其他答案中所提到的 - 这两种方法都有自己的问题。简单地说,我认为没有一种简单的方法来实现这一点(至少我不知道)。这也有点与系统作斗争,因为毕竟Groovy是一种动态语言 :) 如果您想删除所有这些功能,那么您可能只需坚持使用Java(尽管我理解OP想要实现的内容,用Groovy编写测试非常好)。 - kmera
@tfeichtinger 谢谢。我确实想在Groovy中测试一些代码。我想要一个“数据提供程序”-多个输入到单个测试方法(无论是在TestNG还是Spock中)..整个重点是检查方法重载等。因此,似乎在Groovy中测试Java很危险(因为代码的工作方式不同),有时(比如这里)是不可能的(因为差异是测试的重点)。我仍在等待一些大师的发言 :-) 无论如何,感谢您的意见。 - Parobay

3
如果多重分派不是您想要的,您能否在测试脚本中转换参数?
test.runTest( (Object) [1,2,3] )

2
+1 这似乎是最好的解决方案,因为只需要调整调用站点。请注意,根据您提供的文章,test.runTest([1,2,3] as Object) 也可以使用。 - Paul Bellora

1
让我们从解决方案开始:
import groovy.transform.CompileStatic
import spock.lang.Specification
import spock.lang.Subject

class TestSpec extends Specification {

    @Subject
    Test test = new Test()

    def 'Invokes proper method from JAVA class'() {
        given:
        List input = [1,2,3]

        when:
        invokeTestedMethodJavaWay(test, input)

        then:
        noExceptionThrown()
    }

    @CompileStatic
    void invokeTestedMethodJavaWay(Test test, Object input) {
        test.runTest(input)
    }
}

首先,在JAVA中,您不能通过通用类型覆盖方法。例如,如果您尝试添加另一个具有相同合同但使用不同通用类型的方法,比如说public <P extends Printable> void runTest(Collection<P> ps),您将遇到歧义问题,因为两种方法将具有相同的擦除。
更重要的是,在您的问题中,已经在其他答案中提到了。由于我们正在分别进行JAVA和Groovy之间的编译时与运行时类型评估,因此您的期望未达到行为。如果人们意识到这一点,这可能非常有用。例如,在处理异常时。考虑以下示例。
JAVA:
public void someMethod() {
    try {
        // some code possibly throwing different exceptions
    } catch (SQLException e) {
        // SQL handle
    } catch (IllegalStateException e) {
        // illegal state handle
    } catch (RuntimeException e) {
        // runtime handle
    } catch (Exception e) {
        // common handle
    }
}

Groovy:

void someMethod() {
    try {
        // some code possibly throwing different exceptions
    } catch (Exception e) {
        handle(e)
    }
}

void handle(Exception e) { /* common handle */ }

void handle(IllegalStateException e) { /* illegal state handle */ }

void handle(RuntimeException e) { /* runtime handle */ }

void handle(SQLException e) { /* SQL handle */ }

我认为Groovy的方式比丑陋的try-catch多块要干净得多,特别是你可以将所有处理方法实现在单独的对象中并委托处理。所以这不是一个错误,而是一个特性 :)
回到解决方案。你已经知道不能用@CompileStatic注释整个Spock测试类。但是你可以使用单个方法(或单独的帮助类)来实现这一点。这将为从注释方法内部进行的任何调用带回预期的类似Java的行为(编译时类型评估)。
希望这有所帮助,干杯!
PS. @Subject注释仅用于可读性。它指出正在测试哪个对象(是规范的主题)。
编辑: 经过与问题作者的讨论,发现一个不太干净但可行的解决方案:
import groovy.transform.CompileStatic import spock.lang.Specification import spock.lang.Subject
class TestSpec extends Specification {

    @Subject Test test = new Test()
    TestInvoker invoker = new TestInvoker(test)

    def 'Invokes proper method from JAVA class'() {
        when:
        invoker.invokeTestedMethod(input)

        then:
        noExceptionThrown()

        where:
        input << [
                [1, 2, 3, 4, 5],
                [new TestableImpl(), new TestableImpl()]
        ]
    }
}

@CompileStatic
class TestInvoker {

    Test target

    TestInvoker(Test target) {
        this.target = target
    }

    void invokeTestedMethod(Object input) {
        target.runTest(input)
    }

    void invokeTestedMethod(Collection input) {
        if (input.first() instanceof Testable) {
            target.runTest(input)
        } else {
            this.invokeTestedMethod((Object) input)
        }
    }
}

如果您需要检查多个泛型集合类型,请注意在Groovy中可以在switch case语句中使用instanceof。

这很好。你能否从Spock中的数据表中调用一个@CompileStatic方法?(http://docs.spockframework.org/en/latest/data_driven_testing.html#data-tables)?这就是我满意的! - Parobay
1
你是指 where 块中的表格吗?是的,可以。我以为这种方法需要使用 static 修饰符,但似乎这并不必要。@CompileStatic 对助手方法的影响并不影响它是否可以从 where 块的数据表中调用。 - topr
嘿,但是你的invokeTestedMethodJavaWay始终调用Object重载版本,因为参数的静态类型是Object。我很想接受这个答案,但你必须让我的案例工作-在原始帖子中,我运行了2个调用来测试2个重载版本,在现实中,我想要测试大约5个重载版本和大约25个数据输入。 - Parobay
请问您能否提供一个在JAVA中的调用示例?如果您使用了JAVA等效的调用test.runTest(new ArrayList<Integer>() {{ add(1); add(2); add(3); }}),那么您也会遇到ClassCastException。这似乎是您试图通过方法参数的泛型类型进行重载,而这在JAVA中是不可能的。对于这个令人困惑的ArrayList实例化,我很抱歉,但我没有找到其他内联方式来完成它。 - topr
好的,我了解你想要实现什么。在你的情况下,Groovy动态类型评估确实非常有帮助,但是你试图通过方法参数泛型类型进行重载,这在JAVA或Groovy中都不可能。这就是为什么我很高兴看到如何在JAVA中解决这个问题,因为你说你不想改变你的类API,只是因为它们被Groovy测试过。 无论如何,我在我的答案中添加了一个不太直接但可行的解决方案。也许它能完成任务。 - topr

1
这是因为Java在编译时从代码中剥离了泛型信息导致的。
当Groovy在运行时尝试选择正确的方法时,它会得到一个ArrayList作为第二个调用的参数(注意:不再有泛型信息),这与runTest(Object o)相比更匹配runTest(Collection tx)
有两种方法可以解决这个问题:
  • 创建两个不同名称的方法
  • 删除runTest(Collection)。而是在runTest(Object)中使用instanceof来确定参数是否是正确类型的集合,并委托给一个新的内部方法runTestsInCollection()

这是个坏消息,我想要用Groovy来测试我编写的一些Java工具代码...而且我不想因为在Groovy中进行测试而改变API...我正在使用Spock框架。 - Parobay
创建一个帮助类GroovyTest,在进行检查后扩展或委托给Test。或者,使用Groovy 2,您可以尝试@CompileStatic。 - Aaron Digulla
你知道Spock框架吗?它有一些疯狂的功能,在静态编译时根本不起作用,实际上它们根本无法编译 :-( - Parobay
我对Spock有一点了解。如果使用@CompileStatic,则使用帮助类在Groovy和Java之间进行翻译。 - Aaron Digulla

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