I gave an introduction to Groovy at our local JUG this week and didn’t quite manage to make it through all the material I wanted to cover, so I decided to add the missing content here! The part I missed out in the meetup was how to extend a class at runtime via it’s metaClass.
If you add a named closure to the metaClass of an existing class, then it essentially becomes available as a method on that class. In this example, I’ve added a containsOnly(Collection anotherCollection) method to the Collection class, which returns true if two collections have the same content but not necessarily in the same order.
Collection.metaClass.containsOnly = { Collection otherCollection ->
delegate.containsAll(otherCollection) && delegate.size() == otherCollection.size()
}
a = [1, 2, 3, 4]
b = [2, 4, 3, 1]
c = [2, 4, 3, 5]
assert a.containsOnly(b)
assert !b.containsOnly(c)
delegate is an implicit argument referring to the object on which the containsOnly() method is being called.
In the following case, I have extended the Collection class again to add a choose(int numberOfElements) method, which selects a supplied number of elements from the collection, chosen at random. Note that the return keyword is optional, I’ve used it here for clarity.
Collection.metaClass.choose = { int numberOfElements ->
if (delegate.size() <= numberOfElements) {
return delegate
} else {
List previouslyUsed = []
List chosen = []
while (chosen.size() < numberOfElements) {
int index = new Random().nextInt(delegate.size())
if (!previouslyUsed.contains(index)) {
chosen << delegate[index]
previouslyUsed << index
}
}
return chosen
}
}
a = [1,2,3,4,5,6,7,8,9,10]
one = a.choose(1)
assert one.size() == 1
assert a.containsAll(one)
five = a.choose(5)
assert five.size() == 5
assert a.containsAll(five)
twelve = a.choose(12) // should only return 10!
assert twelve.size() == 10
assert a.containsAll(twelve)
My final example extends the String class so that you can easily cast it to a Date instance using a fixed conversion format. Groovy uses this syntax for casting (which is much nicer than Java):
"08/08/1988" as Date
Under the hood, Groovy calls the asType(Class targetType) method to perform the conversion, and that already supports casting to a number of different types. To add Date casting I had to replace the existing method definition. So that I didn’t lose the original functionality, I captured the original method and delegated to it if we’re not trying to cast to a Date.
def oldAsType = String.metaClass.getMetaMethod("asType", [Class] as Class[])
String.metaClass.asType = { Class targetType ->
if (targetType == Date.class) {
Date.parse("dd/MM/yyyy", delegate)
} else {
oldAsType.invoke(delegate, targetType)
}
}
// Now we can call...
"08/08/1988" as Date
This work is licensed under a Creative Commons Attribution 3.0 Unported License.
Copyright ©Craig Aspinall 2011