3

I injected overridden method toString into Object.metaClass

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

and I thought that following code will execute this method:

1500.toString()

But it didn't not,nothing was printed to the console. That is what exactly confuses me: if something goes bad, then an error is to throw out; if Object.metaClass.toString is found and invoked, then the message will turn up, but why it is not working? What happened inside?

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
KyrinWoo
  • 327
  • 1
  • 3
  • 9

2 Answers2

4

This behavior is correct, because java.lang.Integer overrides Object.toString() with its own implementation. If your assumption was correct then it would mean that you can break overridden method by forcing to use an implementation from parent class.

Consider following Groovy script:

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()

When you run it you will see something like:

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

You can see that GroovyClassWithNoToString.toString() called Object.toString() method and its modified version, also Runtime.toString() calls Object.toString() - I picked this class as an example of pure Java class that does not override toString() method.

As you can see overriding toString() method from Object level makes sense for classes that base on Object.toString() implementation. Classes that provide their own implementation of toString() wont use your dynamically modified method. It also explains why following code works:

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

1500.printMessage()

In this example we are adding a new method called printMessage() to Object class and all classes that don't override this method will use this dynamic method we just created. Integer class does not have method like that one so it's gonna print out:

Hello!

as expected.

Also keep in mind that toString() should return a String and it's better to not print anything to output inside this method - you can end up with nasty StackOverflowError caused by circular calls to toString() method.

UPDATE: How toString() method is being picked by Groovy runtime?

Let me show you under the hood what happens when we call following script:

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

1500.toString()

and let's see what does Groovy during the runtime. Groovy uses Meta Object Protocol (MOP) to e.g. invoke any method called in a Groovy code. In short, when you call any Java or Groovy method it uses MOP as an intermediate layer to find an execution plan for a method - call it directly or use e.g. a method that was injected dynamically.

In our case we use plain Java class - Integer. In this case Groovy will create an instance of PojoMetaMethodSite class to meta class implementation for Java class - an Integer. Every meta method is executed using one of the Groovy groovy.lang.MetaClass implementation. In this case groovy.lang.MetaClassImpl is being used. One of the last methods that picks a method to execute is MetaClassImpl.getMethodWithCachingInternal(Class sender, CallSite site, Class [] params). If you put a breakpoint in the beginning of this method and run a script with a debugger, you will see that this method is executed with following parameters:

enter image description here

In line 1331 you can see that helper method called chooseMethod(e.name, methods, params) is being used:

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

This method is responsible for picking the right method to execute when we try to invoke toString() on Integer object. Let's get there and see what happens. Here is what this method implementation looks like:

/**
 * 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;
}

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

Now let's see what parameters are received when we call our script:

enter image description here

What is most interesting in our case is the first element of methodOrList.data. It's a method object of:

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

which is the method toString() that Integer class overrides from its parent class. Groovy runtime picks this method, because it is the most accurate from the runtimes point of view - it is the most specific method for Integer class provided. If there is no toString() method overridden at the class level (e.g. Runtime class example I mentioned earlier) then the best candidate for invoking toString() method is a ClosureMetaMethod provided by us in Object.metaClass.toString = .... I hope it gives you a better understanding of what happens under the hood.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
  • @Kylin.this `Integer.toString()` returns a `String` representation of `Integer` value - this is what this method suppose to do. `Object` is a parent class for `Integer`, `Object.metaClass.toString = {}` changes this method on a `Object` class level and `Integer` overrides `toString()` to provide its own implementation. That's why when you do `Integer.metaClass.toString = {}` you replace `toString()` method on a `Integer` class level and it works for it. Also please keep in mind that `toString()` should only return a String and it should not cause any side effects (like printing to console). – Szymon Stepniak Oct 15 '17 at 08:59
  • @Kylin.this I've updated an answer with more detailed explanation of what happens under the hood of Groovy runtime. Hope it gives you a better understanding how Groovy picks the best method candidate. – Szymon Stepniak Oct 15 '17 at 09:25
  • Do you mean that it is `toString` implemented by `Integer`. not `Object.metaClass.toString`,that is invoked when executing `1500.toString()`? Then it seemingly makes sense that `1500.toString()` must print `1500` to Console whatever I do on `Object.metaClass.toString` in that the latter has no effect on the former, doesn't it? – KyrinWoo Oct 15 '17 at 09:36
  • In fact, it generates `1500` on the screen if I define `Object.metaClass.toString={ "the string is $delegate" }` and execute `println 1500.toString()`. Maybe it's `println` in the body that causes the side effects of nothing output..... – KyrinWoo Oct 15 '17 at 09:42
  • `toString()` never prints anything to console, it always returns a `String` representation of an object. In this case `1500.toString() == '1500'`. When you do `println 1500.toString()` you pass a value `'1500'` to println method and it displays String `'1500`' to console as `1500`. Replacing `Object.toString()` has no effects for `Integer` class because `Integer` overrides `toString()` and this overridden method has highest priority in Groovy runtime. You can replace it only by replacing `Integer.metaClass.toString = ...`. – Szymon Stepniak Oct 15 '17 at 09:51
  • Thank you very much! It's very kind of you! – KyrinWoo Oct 15 '17 at 09:54
1

I don't think you can override the Object.toString() that way.

But this works:

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

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

Pablo Pazos
  • 3,080
  • 29
  • 42
  • Thanks for your answer.Right, I know Integer.metaClass.toString works fine. But I just wonder why nothing is printed when I call 1500.toString()? In fact,if I define Object.metaClass.printMessage={out.println("the string is $delegate")},then 1500.printMessage() does print the message. Why toString not? – KyrinWoo Oct 14 '17 at 17:11
  • as I said, I don't think Object.toString can be overridden that at the Object level. – Pablo Pazos Oct 14 '17 at 17:47
  • Sorry, but wait a minute. Provided that we cannot override toString at the Object level, what if it's at subclass level? In fact, it still can't work if I replace `Object` with `Number`,that is,`Number.metaClass.toString={...}`.It seems that **only** Integer is Ok. – KyrinWoo Oct 14 '17 at 18:19
  • But now there is another problem that how I design the code effectively if I prefer to make all instances of any class match the `toString` I define,that is, `1500.toString()`,`"msg".toString()`,`3.14D.toString()` and so on, all of which are gonna print message of the same format. It is of course nightmare to define `Integer.metaClass.toString`,`String.metaClass.toString`, `Doble.metaClass.toString`..... isn't it? – KyrinWoo Oct 14 '17 at 18:20
  • Number is abstract, try metaClass on concrete subclasses, it should work. By the way, toString() MUST return String. – Pablo Pazos Oct 14 '17 at 18:36