【问题标题】:Changing Object.toString() dynamically in Groovy has no effect when calling Integer.toString()调用 Integer.toString() 时,在 Groovy 中动态更改 Object.toString() 无效
【发布时间】:2018-03-26 14:05:34
【问题描述】:

我将重写的方法toString注入Object.metaClass

Object.metaClass.toString ={
   System.out.println("the string is $delegate")
}

我认为下面的代码会执行这个方法:

1500.toString()

但事实并非如此,控制台上没有打印任何内容。这正是让我感到困惑的地方:如果出现问题,就会抛出错误;如果找到并调用了Object.metaClass.toString,则消息会出现,但为什么它不起作用?里面发生了什么?

【问题讨论】:

  • 试试Object.metaClass.toString={->...}
  • 嗯,完全没有变化......

标签: groovy metaprogramming


【解决方案1】:

这种行为是正确的,因为java.lang.Integer 用自己的实现覆盖了Object.toString()。如果您的假设是正确的,那么这意味着您可以通过强制使用父类的实现来破坏被覆盖的方法。

考虑使用 Groovy 脚本:

Object.metaClass.toString = {
    System.out.println("the string is $delegate")
}

class GroovyClassWithNoToString {}

class GroovyClassWithToString {
    @Override
    String toString() {
        return "aaaa"
    }
}

new GroovyClassWithNoToString().toString()

new GroovyClassWithToString().toString()

1500.toString()

Runtime.runtime.toString()

当你运行它时,你会看到类似的东西:

the string is GroovyClassWithNoToString@3a93b025
the string is java.lang.Runtime@128d2484

您可以看到GroovyClassWithNoToString.toString() 调用了Object.toString() 方法及其修改后的版本,Runtime.toString() 也调用了Object.toString() - 我选择了这个类作为不覆盖toString() 方法的纯Java 类的示例。

正如您所见,从Object 级别覆盖toString() 方法对于基于Object.toString() 实现的类是有意义的。提供自己的toString() 实现的类不会使用您动态修改的方法。它还解释了为什么以下代码有效:

Object.metaClass.printMessage = {
    System.out.println("Hello!")
}

1500.printMessage()

在这个例子中,我们将一个名为printMessage() 的新方法添加到Object 类中,所有不覆盖此方法的类都将使用我们刚刚创建的这个动态方法。 Integer 类没有类似的方法,所以会打印出来:

Hello!

正如预期的那样。

还请记住,toString() 应该返回一个 String,并且最好不要在此方法中打印任何要输出的内容 - 您最终可能会因循环调用 toString() 方法而导致讨厌的 StackOverflowError

更新:Groovy 运行时如何选择toString() 方法?

让我向您展示当我们调用以下脚本时会发生什么:

Object.metaClass.toString = {
    System.out.println("Hello!")
}

1500.toString()

让我们看看 Groovy 在运行时做了什么。 Groovy 使用元对象协议 (MOP),例如调用在 Groovy 代码中调用的任何方法。简而言之,当您调用任何 Java 或 Groovy 方法时,它使用 MOP 作为中间层来查找方法的执行计划 - 直接调用它或使用例如动态注入的方法。

在我们的例子中,我们使用纯 Java 类 - Integer。在这种情况下,Groovy 将为 Java 类创建一个 PojoMetaMethodSite 类到元类实现的实例 - 一个 Integer。每个元方法都使用 Groovy groovy.lang.MetaClass 实现之一执行。在这种情况下,groovy.lang.MetaClassImpl 被使用。选择要执行的方法的最后一个方法是MetaClassImpl.getMethodWithCachingInternal(Class sender, CallSite site, Class [] params)。如果你在这个方法的开头放一个断点,然后用调试器运行一个脚本,你会看到这个方法是用以下参数执行的:

在第 1331 行,您可以看到正在使用名为 chooseMethod(e.name, methods, params) 的辅助方法:

cacheEntry = new MetaMethodIndex.CacheEntry (params, (MetaMethod) chooseMethod(e.name, methods, params));

当我们尝试在Integer 对象上调用toString() 时,该方法负责选择正确的方法来执行。让我们到那里看看会发生什么。下面是这个方法实现的样子:

/**
 * Chooses the correct method to use from a list of methods which match by
 * name.
 *
 * @param methodOrList   the possible methods to choose from
 * @param arguments
 */
protected Object chooseMethod(String methodName, Object methodOrList, Class[] arguments) {
    Object method = chooseMethodInternal(methodName, methodOrList, arguments);
    if (method instanceof GeneratedMetaMethod.Proxy)
        return ((GeneratedMetaMethod.Proxy)method).proxy ();
    return method;
}

来源:https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/MetaClassImpl.java#L3158

现在让我们看看调用脚本时接收到哪些参数:

在我们的例子中最有趣的是methodOrList.data 的第一个元素。它是一个方法对象:

public java.lang.String java.lang.Integer.toString()

这是Integer 类从其父类覆盖的方法toString()。 Groovy 运行时选择了这个方法,因为从运行时的角度来看它是最准确的——它是为Integer 类提供的最具体的方法。如果在类级别没有覆盖toString() 方法(例如我之前提到的Runtime 类示例),那么调用toString() 方法的最佳候选者是我们在Object.metaClass.toString = ... 中提供的ClosureMetaMethod。我希望它能让您更好地了解幕后发生的事情。

【讨论】:

  • @Kylin.this Integer.toString() 返回 String 表示 Integer 值 - 这是这个方法应该做的。 ObjectInteger 的父类,Object.metaClass.toString = {}Object 类级别上更改此方法,Integer 覆盖 toString() 以提供自己的实现。这就是为什么当您执行 Integer.metaClass.toString = {} 时,您在 Integer 类级别上替换 toString() 方法并且它适用于它。另外请记住,toString() 应该只返回一个字符串并且它不应该引起任何副作用(比如打印到控制台)。
  • @Kylin.this 我更新了一个答案,更详细地解释了 Groovy 运行时引擎盖下发生的事情。希望它能让您更好地了解 Groovy 如何挑选最佳候选方法。
  • 你的意思是toStringInteger实现。不是Object.metaClass.toString,是在执行1500.toString()时调用的?那么1500.toString() 必须将1500 打印到控制台似乎是有道理的,无论我在Object.metaClass.toString 上做什么,因为后者对前者没有影响,不是吗?
  • 其实如果我定义Object.metaClass.toString={ "the string is $delegate" }并执行println 1500.toString(),它会在屏幕上生成1500。可能是体内的println导致了nothing output的副作用.....
  • toString() 从不向控制台打印任何内容,它总是返回一个对象的 String 表示。在这种情况下1500.toString() == '1500'。当您执行println 1500.toString() 时,您将值'1500' 传递给println 方法,它将字符串'1500' 显示为1500 到控制台。替换 Object.toString()Integer 类没有影响,因为 Integer 覆盖 toString() 并且这个被覆盖的方法在 Groovy 运行时具有最高优先级。您只能通过替换Integer.metaClass.toString = ...来替换它。
【解决方案2】:

我认为你不能那样覆盖 Object.toString()。

但这有效:

Integer.metaClass.toString = { ->
   System.out.println("the string is $delegate")
}

https://groovyconsole.appspot.com/script/5077208682987520

【讨论】:

  • 感谢您的回答。对,我知道 Integer.metaClass.toString 工作正常。但我只是想知道为什么当我调用 1500.toString() 时什么都没有打印?事实上,如果我定义 Object.metaClass.printMessage={out.println("the string is $delegate")},那么 1500.printMessage() 确实会打印消息。为什么 toString 不行?
  • 正如我所说,我认为 Object.toString 不能在对象级别被覆盖。
  • 抱歉,请稍等。假设我们不能在 Object 级别覆盖 toString,如果它在子类级别怎么办?其实我把Object换成Number,也就是Number.metaClass.toString={...}还是不行。看来only整数就可以了。
  • 但是现在还有一个问题,如果我想让任何类的所有实例都匹配我定义的toString,即1500.toString()"msg".toString(),我如何有效地设计代码, 3.14D.toString() 等等,都将打印相同格式的消息。定义Integer.metaClass.toString,String.metaClass.toString,Doble.metaClass.toString.....当然是噩梦不是吗?
  • 数字是抽象的,在具体的子类上尝试 metaClass,它应该可以工作。顺便说一句,toString() 必须返回 String。
猜你喜欢
  • 1970-01-01
  • 2011-12-12
  • 1970-01-01
  • 1970-01-01
  • 2017-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多