VI Methods
54. Introduction
Methods (sometimes also called functions) are blocks of code that can be run more than once and encapsulate a segment of logic. We define a method by writing the code that will be run when the method is called. Calling a method is the process of your code asking the method to start.
Groovy, like Java, is object-oriented and works around classes. C and Pascal are procedural and work around functions. Whilst the methods described here may look a bit like C-style programming that lets you build libraries of functions, what is really happening is Groovy wraps your code in a class definition behind the scenes. You’re only likely to create “raw” methods, like the ones below, as a means to break up your scripts. More usually you’ll create methods within your classes.
Methods have a number of features:
- Methods have names
- this allows you to call your method in a meaningful way
- Methods can accept parameters
- these are inputs into your method that can affect how your method operates
- Methods can return a result value
- this can be captured by a variable from the code calling the method
- Methods have their own scope
- they can have their own variables and not inadvertently affect the rest of your program
We’ve already looked at various methods for use with variables such as lists and maps so you’ve seen methods being called throughout the previous chapters.
55. The Basics
Let’s start by examining a new method we’ll write to calculate the average of the numbers in a list:
def determineAverage(list) {
return list.sum() / list.size()
}
Breaking that code up we can see:
- The
defreserved word is used to commence the method declaration- Much like we use when defining a variable
-
determineAverageis the name of the method - The method accepts a single parameter,
list - A single value is returned using the
returnreserved word- In this case it’s the result of
list.sum() / list.size()
- In this case it’s the result of
The method name (determineAverage) may look a bit odd but it uses a naming strategy called “lower Camel Case”. The camel aspect is the use of upper-case letters to indicate individual words in the name (hence Average). The first word in the method name is a verb (determine) to indicate that a method “does” something.
Let’s return to that determineAverage method and get a complete script together - you can copy and paste this into groovyConsole and run it to experience the method first-hand:
def determineAverage(list) {
return list.sum() / list.size()
}
def scores = [2, 7, 4, 3]
def result = determineAverage(scores)
println result
Let’s look at the main components of that script:
- The
determineAveragemethod is defined as before- This can appear above or below the other code
- A new list of numbers is created:
def scores = [2, 7, 4, 3] - The method is called with the
scoresvariable passed as a parameter - The return value (result) of
determineAverageis stored in theresultvariable.
In the example I called the method using determineAverage(scores) but, in many cases, I don’t need to use the parentheses and determineAverage scores would have also worked. That’s why println 'hello, world' works just fine. This works really well when you start to use Groovy to construct domain-specific languages.
56. Parameters
Let’s look at the last example from the previous chapter:
def determineAverage(list) {
return list.sum() / list.size()
}
def scores = [2, 7, 4, 3]
def result = determineAverage(scores)
println result
You might be wondering what happened to the scores variable once it was passed to determineAverage as a parameter. Basically, Groovy gave it another name (list) for use within the method. Inside the method, list is just another variable. This means that if determineAverage changes list in some way, this is reflected in the scores variable used in the main script:
def scores = [2, 7, 4, 3]
def result = determineAverage(scores)
println result
println scores
def determineAverage(list) {
list << 9
return list.sum() / list.size()
}
The code above is very poorly behaved - it modifies list by adding a new item. Unless you provided documentation that explicitly states that you will change a parameter, most developers will assume that their parameters will be safely untouched by your method.
Declaring data types for parameters
Groovy lets you designate a data type for your parameters:
def determineAverage(List list) {
return list.sum() / list.size()
}
As you start to develop classes and larger programs, methods create your Application Programming Interface (API). Such methods can be called by other people’s code and they could be using another JVM language (such as Java). It can make their life a little easier if you indicate the data types you’re expecting for your parameters. Alternatively, you can stay true to dynamic typing and let people know through your documentation.
Multiple parameters
Let’s look at another method - one that needs several parameters:
def callFriend(name, phone, message) {
println "Dialling $name on $phone"
println "Hi, $name - $message"
}
Either of these calls would work - it just depends if you want to use the parentheses:
callFriend('Barry', '0400 123 456', 'Did you see that local sporting team?')
callFriend 'Alex', '07 3344 0000', 'Could you please check on my pets whilst I\'\
m away?'
Each parameter may be typed if needed:
def callFriend(String name, String phone, String message) {..}
You can provide a mix of typed and untyped parameters but this is a little messy and I think it’s bad form so I can’t be bothered encouraging such an action by providing an example.
57. Default Values for Parameters
One or more parameters can be defined with a default value. This can be really useful if most calls to the method will use the defaults:
def displayMessage (message,
title = 'Important message:',
border = true) {
def borderText = ''
if (border) {
borderText = '--------------------------'
}
println """\
$borderText
$title
\t $message
$borderText
"""
}
displayMessage 'Preparing to shut down. Please save your work'
The displayMethod can be called in a number of ways:
displayMessage 'Preparing to shut down. Please save your work'displayMessage 'The system appears to have crashed', 'Error!'displayMessage 'Be prepared for the happiness patrol', 'Public announcement:', false
When you get to method overloading and other object-oriented concepts you’ll see that default parameter values can aid in reducing the variations of a method that you might need to provide.
58. Named Arguments
You can use named arguments by having the first parameter be a Map. Consider the method below:
def displayMessage (options, message) {
def borderText = ''
if (! options.containsKey('border') || options.border) {
borderText = '--------------------------'
}
def title = 'Important message:'
if (options.title) {
title = options.title
}
println """\
$borderText
$title
\t $message
$borderText
"""
}
displayMessage(title: 'Canberra', border: true, 'The capital of Australia')
The options parameter is actually a Map and this lets the caller use an interesting Groovy syntax when calling the method. Instead of passing a Map ([:]) to the options parameter, the caller can use the key: value format in their method call. This lets us call displayMessage in many ways, including:
displayMessage(title: 'Canberra', border: true, 'The capital of Australia')displayMessage title: 'Time', "It is now ${new Date()}"displayMessage border: false, 'Hang in there little buddy!'
My recommendation is to use named parameters for non-essential parameters and to make sure that your method can operate correctly if a named parameter is not provided.
If others are to be using your method or you need to remember which named parameters are available, then you’ll make sure that you add some useful documentation to the method.
59. Variable Arguments (Varargs)
There are times where we want to pass a variable number of parameters to the method. However, the parameter list for a method is fixed.
One approach is to use a list for a catch-all parameter, such as items does in the code below:
def buyGroceries(store, items) {
println "I'm off to $store to buy:"
for (item in items) {
println " -$item"
}
}
buyGroceries 'The Corner Store', ['apples', 'cat food', 'cream']
Whilst the list path is an option, Groovy supports the use of variable arguments (varargs) using the “three-dot” (...) notation for the last (and only the last) parameter:
def buyGroceries(... items) {
for (item in items) {
println item
}
}
buyGroceries 'apples', 'cat food', 'cream'
We can set a specific data type for the items parameter by placing the type before the ...:
def buyGroceries(String... items) {
for (item in items) {
println item
}
}
buyGroceries 'apples', 'cat food', 'cream'
Let’s return to the first example in this chapter and rewrite it using varargs:
def buyGroceries(store, ... items) {
println "I'm off to $store to buy:"
for (item in items) {
println " -$item"
}
}
buyGroceries 'The Corner Store', 'apples', 'cat food', 'cream'
So the items parameter is actually a list inside buyGroceries but the caller just passes a series of values to the method.
60. Return value
When we started this tutorial I provided a very basic method for calculating averages. I’ve rewritten it slightly to use varargs and this is a good starting point into using the return statement:
println determineAverage(10, 20, 30, 40)
def determineAverage(... list) {
return list.sum() / list.size()
}
In the code above I return the average to the caller so, instead of printing out the result I could also assign it to a variable: def avg = determineAverage(10, 20, 30, 40).
Using the return reserved word isn’t required as Groovy will return the result of the last statement:
println determineAverage(10, 20, 30, 40)
def determineAverage(... list) {
list.sum() / list.size()
}
You can use return to explicitly exit a method. By itself, return actually returns null. In the useless method I provide below, I explicitly provide return:
def printer(message) {
println message
return
}
printer('hello, world')
That use of return in the last bit of code was redundant as the method would exit anyway (it had nothing left to do). However, this can be handy if the last expression to run in a method returns a value that we don’t want as the return value for our method.
Anything after a return is inaccessible, as illustrated by my even more useless method:
def printer(message) {
println message
return
println 'Help, I don\'t exist'
}
That last line in the method will never, ever, ever be called. But if I really wanted it to be called I can use the try-finally approach:
def printer(message) {
try{
println message
return
} finally {
println 'Help, I don\'t exist'
}
}
Now, that last bit of text will be displayed as it’s in a finally block. This example is rather daft but it serves to illustrate how finally can be used to clean up something like an open file before the return is actioned.
Multiple Returns
You can have more than one return statement in a method but only one will ever be evaluated. This is really handy as it localises the return and prevents the method from further evaluation. You can also place a return at the very end of the method block to ensure that the method always returns a value. In the code below I use two return statements in the switch but also have return false at the bottom of the method just in case something slips through (most likely when I add in code at a later date):
def checkAnimalAsPet(animal) {
switch(animal){
case 'dog':
case 'cat':
return true
default:
return false
}
return false
}
assert checkAnimalAsPet('cat')
assert checkAnimalAsPet('dog')
assert checkAnimalAsPet('lion') == false
assert checkAnimalAsPet('pterodactyl') == false
You’ll note that, in the checkAnimalAsPet method I have a switch with no breaks. Essentially, the return is breaking out of the switch and the method all at once.
Declaring data types for return values
A data type can be declared for the return value by replacing def with the class name:
e.g. Number determineAverage(... list){..}:
Number determineAverage(... list) {
return list.sum() / list.size()
}
println determineAverage(10, 20, 30, 40)
This is very handy if your method is to be accessed as part of an API, especially by Java programs.
You may notice some methods defined with a return type of void. This indicates that the method won’t return a value:
void displayText() {
println 'Hello, World'
}
I can still use return within the method - I just can’t return a value.
Sequential method calls
In many examples I have used a method’s returned value to set a value of a variable, in an assert or as the input to a println. As the return value has its own type, we can actually call a method straight from the method call. This can be useful if one method call is just an intermediary step towards our goal and we don’t want to explicitly store its return value.
In the example below I call the tokenize method which returns a List of the words in the poem I then call the size method for that list to determine how many words are in the poem:
def poem = '''\
Once a jolly swagman camped by a billabong
Under the shade of a coolibah tree,
And he sang as he watched and waited till his billy boiled:
"Who'll come a-waltzing Matilda, with me?"
'''
poem.tokenize().size()
61. Throwing an exception
A method is able to throw an exception just as we saw in the Exceptions tutorial. In the code below I throw an exception if the caller to determineAverage() tries to pass a String through as a parameter:
def determineAverage(...values) throws IllegalArgumentException {
for (item in values) {
if (item in String) {
throw new IllegalArgumentException()
}
}
return values.sum() / values.size()
}
//This works:
assert determineAverage(12, 18) == 15
//This does not work - we get an exception
determineAverage(5, 5, 8, 'kitten')
None of that code is new to you except for the throws IllegalArgumentException that forms part of the method’s signature1. The use of throws helps describe our method a little better by making callers aware of what to expect.
Multiple exceptions can be listed against throws, as seen in the example below:
def determineAverage(...values)
throws IllegalArgumentException, NullPointerException {
for (item in values) {
if (item in String) {
throw new IllegalArgumentException()
}
}
return values.sum() / values.size()
}
Groovy doesn’t require that you explicitly provide a throws listing if your method throws an exception or passes up an exception that it doesn’t handle. However, if your method is to be used by others, I’d suggest that including throws is worth the effort.
You may note that, in that last example, I placed throws on a second line - this helps readability as it breaks up the display of the signature just slightly.
- This is the section of the method definition contain the return type, method name, parameters and thrown exceptions. As always, Wikipedia has some further information↩
62. Documenting a method
The groovydoc command that comes with the Groovy installation can be used to generate HTML documentation from comments within your code. GroovyDoc is based on JavaDoc and uses much the same syntax.
Let’s look at a Groovy method that features GroovyDoc comments:
/**
* Returns the average of the parameters
*
* @param values a series of numerical values
* @throws IllegalArgumentException if a values parameter is a String
* @returns The average of the values
*/
def determineAverage(...values)
throws IllegalArgumentException {
for (item in values) {
if (item in String) {
throw new IllegalArgumentException()
}
}
return values.sum() / values.size()
}
Taking a look at the commenting:
- The multi-line comment block starts with
/**to indicate that this is a GroovyDoc - The first piece of text provides the summary of the method. It’s one line and meant to be terse.
- A set of
@paramtags can be provided to describe each parameter.- The format is
@param <parameter name> <summary> - You don’t provide the parameter type even if you declare one
- The format is
- Each exception that can be thrown by the method is listed against a
@throwstag and provides a summary as to when the exception may be thrown.- The format is
@throws <exception class> <summary>
- The format is
- The
@returnstag describes what the method willreturn- The format is
@returns <summary>
- The format is
If you copy the sample code into a file named Average.groovy you can then run the following command in your command line/terminal:
groovydoc -d doc Average.groovy
This will produce a directory named doc that contains a set of documentation files. Inside the doc directory you’ll see index.html - open this in a browser to view your documentation.
As you click through the various links you’ll find the documentation for the determineAverage() method. It’ll contain the following information (but look a lot prettier):
java.lang.Object determineAverage(java.lang.Object… values)
Returns the average of the parameters
- throws:
- IllegalArgumentException if a parameter is a String
- returns:
- The average of the values
- Parameters:
- values - a series of numerical values
If you keep clicking links in the html files but can’t find it, look in the DefaultPackage directory for a file name Average.html - that’ll be what you’re after.
63. Techniques
I’d like to tell you that your programming career will be all about writing perfect code that never has problems but I’d just be lying. Here are some techniques to help make sure your methods are more robust and helpful to other programmers.
Valid parameters
We understand that the method determineAverage(...values) is expecting a list of numbers and have used a reasonably clear naming strategy (determineAverage) to display that the method is number-oriented but what happens when our colleague gives us something like:
determineAverage(5, 5, 8, 'kitten')
Clearly, kittens aren’t something that the average calculation can understand1. If you try to run that code you’ll get a nasty error that basically says your code has failed. Don’t be too hard on your colleague though - perhaps they’ve loaded data from a file that’s become corrupted by felines.
Comment your method
Firstly, make sure that determineAverage has some useful documentation such as:
/**
* Returns the average of the parameters
*
* @param values a series of numerical values
* @returns The average of the values
*/
def determineAverage(... values) {
values.sum() / values.size()
}
In the example above I’ve just added a GroovyDoc comment block that describes what the method does, its parameter and what it will return. At the very least, other developers will see how they should be using my method.
Check the parameters
Next, I can be more defensive in my coding and make sure that the method has a prerequisite that needs to be met before it attempts to run.
/**
* Returns the average of the parameters
*
* @param values a series of numerical values
* @throws IllegalArgumentException if a parameter is a String
* @returns The average of the values
*/
def determineAverage(...values)
throws IllegalArgumentException {
for (item in values) {
if (item in String) {
throw new IllegalArgumentException()
}
}
values.sum() / values.size()
}
//This works:
assert determineAverage(12, 18) == 15
//This does not work - we get an exception
determineAverage(5, 5, 8, 'kitten')
The approach above checks to make sure that no parameter is a String - if you pass one to the method you’ll get an exception thrown back at you. In reality I should make sure that only numbers can be passed in and my check won’t pick up a Boolean value - more on this in a moment.
What do you think would happen if I called the method with no parameters - determineAverage()?
(pause)
Well, the division would attempt to divide by zero and that’s a fail so I need to also check that values isn’t empty (I’ll leave out the comments for brevity):
def determineAverage(...values)
throws IllegalArgumentException {
for (item in values) {
if (item in String) {
throw new IllegalArgumentException()
}
}
if (!values) {
return 0
}
values.sum() / values.size()
}
assert determineAverage() == 0
Note that if no parameters are passed, I return 0. I really don’t like returning null from methods as it makes other developers then have to check for null. I also don’t want to raise an exception - I’m happy enough to say that the average of no values is 0.
Get really typed
If I really want to get specific with the data types I’ll take as parameters and return from the method then I can switch to static typing. I can make sure that all my parameters are of type Number (or one of its subtypes) and that I will return a value of type Number. The code below really gets specific about data types:
/**
* Returns the average of the parameters
*
* @param values a series of numerical values
* @throws IllegalArgumentException if a parameter is a String
* @returns The average of the values
*/
Number determineAverage(Number...values) {
if (!values) {
return 0
}
values.sum() / values.length
}
The following two calls to the method would work:
assert determineAverage(2, 7, 4, 4) == 4.25
assert determineAverage() == 0
…but the following two calls won’t work:
determineAverage(2, 7, 4, 4, 'kitten')
determineAverage(2, 7, 4, 4, true)
If you are writing a method that needs to be very specific about data types for parameters and/or return values then this is the way to go.
Testing
I’d get into a lot of trouble from experienced developers if I just left this chapter without mentioning testing. So, here’s a little example using Spock!
Firstly, this won’t run in your groovyConsole. You need to copy the code into the online Spock web console2 and then click on “Run Script”:
import spock.lang.Specification
class MathDemo {
/**
* Returns the average of the parameters
*
* @param values a series of numerical values
* @throws IllegalArgumentException if a parameter is a String
* @returns The average of the values
*/
static determineAverage(... values)
throws IllegalArgumentException {
for (item in values) {
if (!(item instanceof Number)) {
throw new IllegalArgumentException()
}
}
if (!values) {
return 0
}
return values.sum() / values.size()
}
}
class AvgSpec extends Specification {
@Unroll
def "average of #values gives #result"(values, result) {
expect:
MathDemo.determineAverage(*values) == result
where:
values || result
[ 1, 2, 3 ] || 2
[ 2, 7, 4, 4 ] || 4.25
[ ] || 0
}
@Unroll
def "determineAverage called with #values throws #exception"(values, excepti\
on) {
setup:
def e = getException(MathDemo.&determineAverage, *values)
expect:
exception == e?.class
where:
values || exception
[ 'kitten', 1 ] || java.lang.IllegalArgumentException
[ 99, true ] || java.lang.IllegalArgumentException
[ 1, 2, 3 ] || null
}
Exception getException(closure, ... args) {
try {
closure.call(args)
return null
} catch (any) {
return any
}
}
}
When you run this in the Spock web console you should get:
AvgSpec
- average of [1, 2, 3] gives 2
- average of [2, 7, 4, 4] gives 4.25
- average of [] gives 0
- determineAverage called with [kitten, 1] throws class java.lang.IllegalArgume\
ntException
- determineAverage called with [99, true] throws class java.lang.IllegalArgumen\
tException
- determineAverage called with [1, 2, 3] throws null
So there’s a lot going on here that we haven’t covered in the tutorials so far but let’s try to break it down:
- I wrapped the
determineAverage()method in a class namedMathDemoand made it astaticmethod- I won’t explain this here - just trust me that you can call
MathDemo.determineAverage() - But do note that I’ve changed
determineAverage()to better check that the parameters are numbers.
- I won’t explain this here - just trust me that you can call
- I then created a spock test
Specificationsubclass calledAvgSpec- The first test is
def "average of #values gives #result"(values, result)- This runs a series of tests using the data table in the
where:block - Yes, that’s right, Groovy will let you use a string as the name of the method - that’s v.cool but you can’t use interpolation (
${}).
- This runs a series of tests using the data table in the
- The second test is
def "determineAverage called with #values throws #exception"(values, exception)- This checks to make sure that the
IllegalArgumentExceptionis thrown for incorrect parameters
- This checks to make sure that the
- The first test is
As I say, there are a number of topics such as classes and closures that I haven’t covered - this example was just a quick one and should make sense as you learn about those additional concepts.
- Pun intended↩
- I’ve published the code to make it easy for you but can’t promise that this link will always work.↩