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) method to the Collections 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) method, which selects a supplied number of entries from the collection, chosen at random. Note that the ‘return’ keyword is optional, I’ve used it here for clarity.
Collection.metaClass.choose = { int numberOfEntries ->
if (delegate.size() <= numberOfEntries) {
return delegate
} else {
List previouslyUsed = []
List chosen = []
while (chosen.size() < numberOfEntries) {
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) 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)
}
}
'08/08/1988' as Date