【问题标题】:Groovy casting collection unasked for itGroovy 铸造集合不请自来
【发布时间】:2014-01-28 00:08:59
【问题描述】:

我有一些使用泛型的 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

我很惊讶第二个方法调用被分派给了错误的方法(在我的 Javish 理解中是错误的)。而不是调用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"

}

【问题讨论】:

标签: java generics groovy casting spock


【解决方案1】:

这是因为 Java 在编译时从代码中剥离了通用信息。

当 Groovy 尝试在 运行时 选择正确的方法时,它会获得一个 ArrayList 作为第二次调用的参数(注意:不再有通用信息),它比 runTest(Collection tx) 匹配 @987654323 更好@。

有两种方法可以解决这个问题:

  • 创建两个不同名称的方法
  • 删除runTest(Collection)。而是在runTest(Object) 中使用instanceof 来确定参数是否是正确类型的集合并委托给新的内部方法runTestsInCollection()

【讨论】:

  • 这是个坏消息,我想使用 Groovy 来测试我编写的一些 Java 实用程序的代码……我不想仅仅因为我在 Groovy 中测试就更改 API……我我正在使用 Spock 框架
  • 创建一个辅助类GroovyTest,在完成检查后扩展或委托给Test。或者,使用 Groovy 2,您可以尝试 @CompileStatic。
  • 你知道Spock框架吗?它有一些疯狂的功能,在静态编译时根本无法工作,它们实际上根本无法编译:-(
  • 我对 Spock 有点了解。如果@CompileStatic,则使用在 Groovy 和 Java 之间转换的帮助程序类。
【解决方案2】:

如果multiple dispatch不是你想要的,你可以在测试脚本中转换参数吗?

test.runTest( (Object) [1,2,3] )

【讨论】:

  • +1 这似乎是最好的解决方案,因为只需要调整呼叫站点。请注意,test.runTest([1,2,3] as Object) 也可以使用,具体取决于您链接的文章。
【解决方案3】:

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

Groovy - 作为一种动态语言 - 使用 runtime 类型信息来调用正确的方法。另一方面,Java 会根据 static 类型确定将使用哪种方法。

一个简单的例子,说明 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

【讨论】:

  • 你已经解释了这个问题,但没有提供如何避免它的答案。
  • 真的,如果你读到我的第一句话,那正是我写的。问题的一部分是WHY?。我这样回答。可以使用@CompileStatic 或其他答案中提到的类型转换——这两种方法都有自己的问题。简而言之,我认为没有一种简单的方法可以做到这一点(至少我不知道)。由于 Groovy 是一种动态语言,这也是一种与系统的斗争:) 如果您想删除所有这些功能,那么您最好还是坚持使用 java (尽管我了解 OP 想要实现的目标,但它写起来真的很整洁groovy 中的测试)。
  • @tfeichtinger 谢谢。我确实想在 Groovy 中测试一些代码。我想要一个“数据提供者”——一个测试方法的多个输入(无论是在 TestNG 还是 Spock 中).. 重点是检查方法重载等。因此,在 Groovy 中测试 Java 似乎很危险(因为代码的工作方式不同)有时(像这里)这是不可能的(因为不同之处是测试点)我仍在等待一些大师发言:-) 无论如何,谢谢你的意见。跨度>
【解决方案4】:

让我们从解决方案开始:

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 &lt;P extends Printable&gt; void runTest(Collection&lt;P&gt; 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
    }
}

时髦的:

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 注解仅用于可读性。它指出正在测试的对象(是规范的主题)。

编辑: 在与问题的作者讨论后,不是那么干净但可行的解决方案:

导入 groovy.transform.CompileStatic 导入 spock.lang.Specification 导入 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)
        }
    }
}

如果您需要检查一种以上的泛型集合,请注意 instanceof 可用于 Groovy 中的 switch case 语句。

【讨论】:

  • 这很好。你能从 spock 的数据表中调用@CompileStatic 方法吗? (docs.spockframework.org/en/latest/…)?这是我会满意的!
  • 你的意思是 where 块中的表格吗?是的你可以。我认为这种方法需要使用 static 修饰符,但似乎没有必要。 @CompileStatic over helper 方法不会影响它是否可以从 where 块的数据表中调用。
  • 嘿,但是您的invokeTestedMethodJavaWay总是调用Object-overload 版本,因为参数的静态类型是Object.. 我很乐意接受这个答案,但你必须让我的案例工作 - 在原始帖子中,我为 2 个重载运行了 2 个调用,实际上我想测试 ca. 5 次过载,约 5 次。 25 个数据输入。
  • 您能否提供一个示例,在普通 JAVA 中您想要的调用看起来如何?如果你会做 JAVA 等效调用 test.runTest(new ArrayList&lt;Integer&gt;() {{ add(1); add(2); add(3); }}) 你也会有 ClassCastException。似乎您正试图通过泛型类型的方法参数重载,这在 JAVA 中是不可能的。对于这个令人困惑的 ArrayList 实例化感到抱歉,但我没有找到其他内联方式。
  • 好吧,我知道你想达到什么目标。 Groovy 动态类型评估在您的情况下非常有用,除了您试图通过方法参数泛型类型重载的事实。这在 JAVA 或 Groovy 中都是不可能的。这就是为什么我很高兴看到如何在 JAVA 中解决这个问题,因为你说你不喜欢改变你的 clesses API只是因为它们是用 groovy 测试的。无论如何,我为我的答案添加了不那么直截了当但可行的解决方案。也许它会完成这项工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 2021-01-20
  • 2020-03-05
相关资源
最近更新 更多