The pursuit of truth and beauty is a sphere of activity in which we are permitted to remain children all our lives. | ||
--Albert Einstein |
At the hardware level, computer systems use simple arithmetic and logical operations, such as jump to a new location if a memory value equals zero. Any complex flow of logic that a computer is executing can always be expressed in terms of these simple operations. Fortunately, languages such as Java raise the abstraction level available in programs we write so that we can express the flow of logic in terms of higher-level constructs—for example, looping through all of the elements in an array or processing characters until we reach the end of a file.
In this chapter, we explore the constructs Groovy gives us to describe logic flow in ways that are even simpler and more expressive than Java. Before we look at the constructs themselves, however, we have to examine Groovy’s answer to that age-old philosophical question: What is truth?[1]
In order to understand how Groovy will handle control structures such as if
and while
, you need to know how it evaluates expressions, which need to have Boolean results. Many of the control structures we examine in this chapter rely on the result of a Boolean test—an expression that is first evaluated and then considered as being either true or false. The outcome of this affects which path is then followed in the code. In Java, the consideration involved is usually trivial, because Java requires the expression to be one resulting in the primitive boolean
type to start with. Groovy is more relaxed about this, allowing simpler code at the slight expense of language simplicity. We’ll examine Groovy’s rules for Boolean tests and give some advice to avoid falling into an age-old trap.
The expression of a Boolean test can be of any (non-void) type. It can apply to any object. Groovy decides whether to consider the expression as being true or false by applying the rules shown in table 6.1, based on the result’s runtime type. The rules are applied in the order given, and once a rule matches, it completely determines the result.[2]
Table 6.1. Sequence of rules used to evaluate a Boolean test
Evaluation criterion required for truth | |
---|---|
| Corresponding Boolean value is true |
| The matcher has a match |
| The collection is non-empty |
| The map is non-empty |
| The string is non-empty |
| The value is nonzero |
| The object reference is non-null |
Listing 6.1 shows these rules in action, using the Boolean negation operator !
to assert that expressions which ought to evaluate to false
really do so.
These rules can make testing for “truth” simpler and easier to read. However, they come with a price, as you’re about to find out.
Before we get into the meat of the chapter, we have a warning to point out. Just like Java, Groovy allows the expression used for a Boolean test to be an assignment—and the value of an assignment expression is the value assigned. Unlike Java, the type of a Boolean test is not restricted to boolean
s, which means that a problem you might have thought was ancient history reappears, albeit in an alleviated manner. Namely, an equality operator ==
incorrectly entered as an assignment operator =
is valid code with a drastically different effect than the intended one. Groovy shields you from falling into this trap for the most common appearance of this error: when it’s used as a top-level expression in an if
statement. However, it can still arise in less usual cases.
Listing 6.2 leads you through some typical variations of this topic.
The equality comparison in is fine and would be allowable in Java. In , an equality comparison was intended, but one of the equal signs was left out. This raises a Groovy compiler error, because an assignment is not allowed as a top-level expression in an if
test.
However, Boolean tests can be nested inside expressions in arbitrary depth; the simplest one is shown at , where extra parentheses around the assignment make it a subexpression, and therefore the assignment becomes compliant with the Groovy language. The value 3
will be assigned to x
, and x
will be tested for truth. Because 3
is considered true
, the value 3
gets printed. This use of parentheses to please the compiler can even be used as a trick to spare an extra line of assignment. The unusual appearance of the extra parentheses then serves as a warning sign for the reader.
The restriction of assignments from being used in top-level Boolean expressions applies only to if
and not to other control structures such as while
. This is because doing assignment and testing in one expression are often used with while
in the style shown at . This style tends to appear with classical usages like processing tokens retrieved from a parser or reading data from a stream. Although this is convenient, it leaves us with the potential coding pitfall shown at , where x
is assigned the value 1
and the loop would never stop if there weren’t a break statement.[3]
This potential cause of bugs has given rise to the idiom in other languages (such as C and C++, which suffer from the same problem to a worse degree) of putting constants on the left side of the equality operator when you wish to perform a comparison with one. This would lead to the last while
statement in the previous listing (still with a typo) being
This would raise an error, as you can’t assign a value to a constant. We’re back to safety—so long as constants are involved. Unfortunately, not only does this fail when both sides of the comparison are variables, it also reduces readability. Whether it is a natural occurrence, a quirk of human languages, or conditioning, most people find while (x==3)
significantly simpler to read than while (3==x)
. Although neither is going to cause confusion, the latter tends to slow people down or interrupt their train of thought. In this book, we have favored readability over safety—but our situation is somewhat different than that of normal development. You will have to decide for yourself which convention suits you and your team better.
Now that we have examined which expressions Groovy will consider to be true and which are false, we can start looking at the control structures themselves.
Our first set of control structures deals with conditional execution. They all evaluate a Boolean test and make a choice about what to do next based on whether the result was true or false. None of these structures should come as a completely new experience to any Java developer, but of course Groovy adds some twists of its own. We will cover if
statements, the conditional operator, switch
statements, and assertions.
Our first two structures act exactly the same way in Groovy as they do in Java, apart from the evaluation of the Boolean test itself. We start with if
and if/else
statements.
Just as in Java, the Boolean test expression must be enclosed in parentheses. The conditional block is normally enclosed in curly braces. These braces are optional if the block consists of only one statement.[4]
A special application of the “no braces needed for single statements” rule is the sequential use of else if
. In this case, the logical indentation of the code is often flattened; that is, all else if
lines have the same indentation although their meaning is nested. The indentation makes no difference to Groovy and is only of aesthetic relevance.
Listing 6.3 gives some examples, using assert true
to show the blocks of code that will be executed and assert false
to show the blocks that won’t be executed.
There should be no surprises in the listing, although it might still look slightly odd to you that non-Boolean expressions such as strings and lists can be used for Boolean tests. Don’t worry—it becomes natural over time.
Groovy also supports the ternary conditional ?:
operator for small inline tests, as shown in listing 6.4. This operator returns the object that results from evaluating the expression left or right of the colon, depending on the test before the question mark. If the first expression evaluates to true
, the middle expression is evaluated. Otherwise, the last expression is evaluated. Just as in Java, whichever of the last two expressions isn’t used as the result isn’t evaluated at all.
Example 6.4. The conditional operator
def result = (1==1) ? 'ok' : 'failed' assert result == 'ok' result = 'some string' ? 10 : ['x'] assert result == 10
Again, notice how the Boolean test (the first expression) can be of any type. Also note that because everything is an object in Groovy, the middle and last expressions can be of radically different types.
Opinions about the ternary conditional operator vary wildly. Some people find it extremely convenient and use it often. Others find it too Perl-ish. You may well find that you use it less often in Groovy because there are features that make its typical applications obsolete—for example, GStrings (covered in section 3.4.2) allow dynamic creation of strings that would be constructed in Java using the ternary operator.
So far, so Java-like. Things change significantly when we consider switch
statements.
On a recent train ride, I (Dierk) spoke with a teammate about Groovy, mentioning the oh-so-cool switch
capabilities. He wouldn’t even let me get started, waving his hands and saying, “I never use switch
!” I was put off at first, because I lost my momentum in the discussion; but after more thought, I agreed that I don’t use it either—in Java.
The switch
statement in Java is very restrictive. You can only switch on an int
type, with byte
, char
, and short
automatically being promoted to int
.[5] With this restriction, its applicability is bound to either low-level tasks or to some kind of dispatching on a type code. In object-oriented languages, the use of type codes is considered smelly.[6]
The general appearance of the switch
construct is just like in Java, and its logic is identical in the sense that the handling logic falls through to the next case
unless it is exited explicitly. We will explore exiting options in section 6.4.
Listing 6.5 shows the general appearance.
Although the fallthrough is supported in Groovy, there are few cases where this feature really enhances the readability of the code. It usually does more harm than good (and this applies to Java, too). As a general rule, putting a break at the end of each case is good style.
You have seen the Groovy switch
used for classification in section 3.5.5 and when working through the datatypes. A classifier is eligible as a switch
case if it implements the isCase
method. In other words, a Groovy switch
like
switch (candidate) { case classifier1 : handle1() ; break case classifier2 : handle2() ; break default : handleDefault() }
is roughly equivalent (beside the fallthrough and exit handling) to
if (classifier1.isCase(candidate )) handle1() else if (classifier2.isCase(candidate )) handle2() else handleDefault()
This allows expressive classifications and even some unconventional usages with mixed classifiers. Unlike Java’s constant cases, the candidate may match more than one classifier. This means that the order of cases is important in Groovy, whereas it does not affect behavior in Java. Listing 6.6 gives an example of multiple types of classifiers. After having checked that our number 10
is not zero, not in range 0..9
, not in list [8,9,11]
, not of type Float
, and not an integral multiple of 3
, we finally find it to be made of two characters.
The new feature in is that we can classify by type. Float
is of type java.lang.Class
, and the GDK enhances Class
by adding an isCase
method that tests the candidate with isInstance
.
The isCase
method on closures at passes the candidate into the closure and returns the result of the closure call coerced to a Boolean
.
The final classification as a two-digit number works because ~/../
is a Pattern
and the isCase
method on patterns applies its test to the toString
value of the argument.
In order to leverage the power of the switch
construct, it is essential to know the available isCase
implementations. It is not possible to provide an exhaustive list, because any custom type in your code or in a library can implement it. Table 6.2 has the list of known implementations in the GDK.
The isCase
method is also used with grep
on collections such that collection
.grep(
classifier
)
returns a collection of all items that are a case of that classifier.
Using the Groovy switch
in the sense of a classifier is a big step forward. It adds much to the readability of the code. The reader sees a simple classification instead of a tangled, nested construction of if
statements. Again, you are able to reveal what the code does rather than how it does it.
As pointed out in section 4.1.2, the switch classification on ranges is particularly convenient for modeling business rules that tend to prefer discrete classification to continuous functions. The resulting code reads almost like a specification.
Look actively through your code for places to implement isCase
. A characteristic sign of looming classifiers is lengthy else if
constructions.
It is possible to overload the isCase
method to support different kinds of classification logic depending on the type of the candidate. If you provide both methods, isCase(String candidate)
and isCase(Integer candidate)
, then switch ('1')
can behave differently than switch(1)
with your object as classifier.
Our next topic, assertions, may not look particularly important at first glance. However, although assertions don’t change the business capabilities of the code, they do make the code more robust in production. Moreover, they do something even better: enhance the development team’s confidence in their code as well as their ability to remain agile during additional enhancements and ongoing maintenance.
This book contains several hundred assertion statements—and indeed, you’ve already seen a number of them. Now it’s time to go into some extra detail. We will look at producing meaningful error messages from failed assertions, reflect over reasonable uses of this keyword, and show how to use it for inline unit tests. We will also quickly compare the Groovy solution to Java’s assert
keyword and assertions as used in unit test cases.
When an assertion fails, it produces a stacktrace and a message. Put the code
a = 1
assert a==2
in a file called FailingAssert.groovy, and let it run via
> groovy Failing-Assert.groovy
It is expected to fail, and it does so with the message
Caught: java.lang.AssertionError: Expression: (a==2). Values: a = 1 at FailingAssert.run(FailingAssert.groovy:2) at FailingAssert.main(FailingAssert.groovy)
You see that on failure, the assertion prints out the failed expression as it appears in the code plus the value of the variables in that expression. The trailing stack-trace reveals the location of the failed assertion and the sequence of method calls that led to the error. It is best read bottom to top:
We are in the file FailingAssert.groovy.
From that file, a class FailingAssert
was constructed with a method main
.
Within main
, we called FailingAssert.run
, which is located in the file FailingAssert.groovy at line 2 of the file.[7]
At that point, the assertion fails.
This is a lot of information, and it is sufficient to locate and understand the error in most cases, but not always. Let’s try another example that tries to protect a file reading code from being executed if the file doesn’t exist or cannot be read.[8]
input = new File('no such file')
assert input.exists()
assert input.canRead()
println input.text
This produces the output
Caught: java.lang.AssertionError: Expression: input.exists() ...
which is not very informative. The missing information here is what the bad file name was. To this end, assertions can be instrumented with a trailing message:
input = new File('no such file') assert input.exists() , "cannot find '$input.name'" assert input.canRead() , "cannot read '$input.canonicalPath'" println input.text
This produces the following
... cannot find 'no such file'. Expression: input.exists()
which is the information we need. However, this special case also reveals the sometimes unnecessary use of assertions, because in this case we could easily leave the assertions out:
input = new File('no such file') println input.text
The result is the following sufficient error message:
FileNotFoundException: no such file (The system cannot find the file specified)
This leads to the following best practices with assertions:
Before writing an assertion, let your code fail, and see whether any other thrown exception is good enough.
When writing an assertion, let it fail the first time, and see whether the failure message is sufficient. If not, add a message. Let it fail again to verify that the message is now good enough.
If you feel you need an assertion to clarify or protect your code, add it regardless of the previous rules.
If you feel you need a message to clarify the meaning or purpose of your assertion, add it regardless of the previous rules.
Finally, there is a potentially controversial use of assertions as unit tests that live right inside production code and get executed with it. Listing 6.7 shows this strategy with a nontrivial regular expression that extracts a hostname from a URL. The pattern is first constructed and then applied to some assertions before being put to action. We also implement a simple method assertHost
for easy asserting of a match grouping.[9]
Reading this code with and without assertions, their value becomes obvious. Seeing the example matches in the assertions reveals what the code is doing and verifies our assumptions at the same time. Traditionally, these examples would live inside a test harness or perhaps only within a comment. This is better than nothing, but experience shows that comments go out of date and the reader cannot really be sure that the code works as indicated. Tests in external test harnesses also often drift away from the code. Some tests break, they are commented out of a test suite under the pressures of meeting schedules, and eventually they are no longer run at all.
Some may fear a bad impact on performance when doing this style of inline unit tests. The best answer is to use a profiler and investigate where performance is really relevant. Our assertions in listing 6.7 run in a few milliseconds and should not normally be an issue. When performance is important, one possibility would be to put inline unit tests where they are executed only once per loaded class: in a static initializer.
Java has had an assert
keyword since JDK 1.4. It differs from Groovy assertions in that it has a slightly different syntax (colon instead of comma to separate the Boolean test from the message) and that it can be enabled and disabled. Java’s assertion feature is not as powerful, because it works only on a Java Boolean test, whereas the Groovy assert takes a full Groovy conditional (see section 6.1).
The JDK documentation has a long chapter on assertions that talks about the disabling feature for assertions and its impact on compiling, starting the VM, and resulting design issues. Although this is fine and the design rationale behind Java assertions is clear, we feel the disabling feature is the biggest stumbling block for using assertions in Java. You can never be sure that your assertions are really executed.
Some people claim that for performance reasons, assertions should be disabled in production, after the code has been tested with assertions enabled. On this issue, Bertrand Meyer,[10] the father of design by contract, pointed out that it is like learning to swim with a swimming belt and taking it off when leaving the pool and heading for the ocean.
In Groovy, your assertions are always executed.
Assertions also play a central role in unit tests. Groovy comes with an included version of JUnit, the leading unit test framework for Java. JUnit makes a lot of specialized assertions available to its TestCase
s. Groovy adds even more of them. Full coverage of these assertions is given in chapter 14. The information that Groovy provides when assertions fail makes them very convenient when writing unit tests, because it relieves the tester from writing lots of messages.
Assertions can make a big difference to your personal programming style and even more to the culture of a development team, regardless of whether they are used inline or in separated unit tests. Asserting your assumptions not only makes your code more reliable, but it also makes it easier to understand and easier to work with.
That’s it for conditional execution structures. They are the basis for any kind of logical branching and a prerequisite to allow looping—the language feature that makes your computer do all the repetitive work for you. The next two sections cover the looping structures while
and for
.
The structures you’ve seen so far have evaluated a Boolean test once and changed the path of execution once based on the result of the condition. Looping, on the other hand, repeats the execution of a block of code multiple times. The loops available in Groovy are while
and for
, both of which we cover here.
The while
construct is like its Java counterpart. The only difference is the one you’ve seen already—the power of Groovy Boolean test expressions. To summarize very briefly, the Boolean test is evaluated, and if it’s true, the body of the loop is then executed. The test is then re-evaluated, and so forth. Only when the test becomes false does control proceed past the while
loop. Listing 6.8 shows an example that removes all entries from a list. We visited this problem in chapter 3, where you discovered that you can’t use each
for that purpose. The second example adds the values again in a one-liner body without the optional braces.
Example 6.8. Example while loops
def list = [1,2,3] while (list) { list.remove(0) } assert list == [] while (list.size() < 3) list << list.size()+1 assert list == [1,2,3]
Again, there should be no surprises in this code, with the exception of using just list
as the Boolean test in the first loop.
Note that there are no do {} while(
condition
)
or repeat {} until (
condition
)
Considering it is probably the most commonly used type of loop, the for
loop in Java is relatively hard to use, when you examine it closely. Through familiarity, people who have used a language with a similar structure (and there are many such languages) grow to find it easy to use, but that is solely due to frequent use, not due to good design. Although the nature of the traditional for
loop is powerful, it is rarely used in a way that can’t be more simply expressed in terms of iterating through a collection-like data structure. Groovy embraces this simplicity, leading to probably the biggest difference in control structures between Java and Groovy.
Groovy for
loops follow this structure:
for (variable in iterable) { body }
where variable may optionally have a declared type. The Groovy for
loop iterates over the iterable. Frequently used iterables are ranges, collections, maps, arrays, iterators, and enumerations. In fact, any object can be an iterable. Groovy applies the same logic as for object iteration, described in chapter 8.
Curly braces around the body are optional if it consists of only one statement. Listing 6.9 shows some of the possible combinations.
Example uses explicit typing for i
and no braces for a loop body of a single statement. The looping is done on a range of strings.
The usual for
loop appearance when working on a collection is shown in . Recall that thanks to the autoboxing, this also works for arrays.
Looping on a half-exclusive integer range as shown in is equivalent to the Java construction
which is referred to as the classic for
loop. It is currently not supported in Groovy but may be in future versions.
Example is provided to make it clear that is not the typical Groovy style when working on strings. It is more Groovy to treat a string as a collection of characters.
Using the for
loop with object iteration as described in section 9.1.3 provides some very powerful combinations.
You can use it to print a file line-by-line via
def file = new File('myFileName.txt')
for (line in file) println line
or to print all one-digit matches of a regular expression:
def matcher = '12xy3'=~/d/ for (match in matcher) println match
If the container object is null, no iteration will occur:
for (x in null) println 'This will not be printed!'
If Groovy cannot make the container object iterable by any means, the fallback solution is to do an iteration that contains only the container object itself:
for (x in new Object()) println "Printed once for object $x"
Object iteration makes the Groovy for
loop a sophisticated control structure. It is a valid counterpart to using methods that iterate over an object with closures, such as using Collection
’s each
method.
The main difference is that the body of a for
loop is not a closure! That means this body is a block:
for (x in 0..9) { println x }
whereas this body is a closure:
(0..9).each { println it }
Even though they look similar, they are very different in construction.
A closure is an object of its own and has all the features that you saw in chapter 5. It can be constructed in a different place and passed to the each
method.
The body of the for
loop, in contrast, is directly generated as bytecode at its point of appearance. No special scoping rules apply.
This distinction is even more important when it comes to managing exit handling from the body. The next section shows why.
Although it’s nice to have code that reads as a simple list of instructions with no jumping around, it’s often vital that control is passed from the current block or method to the enclosing block or the calling method—or sometimes even further up the call stack. Just like in Java, Groovy allows this to happen in an expected, orderly fashion with return
, break
, and continue
statements, and in emergency situations with exceptions. Let’s take a closer look.
The general logic of return
, break
, and continue
is similar to Java. One difference is that the return
keyword is optional for the last expression in a method or closure. If it is omitted, the return value is that of the last expression. Methods with explicit return type void
do not return a value, whereas closures always return a value.[11]
Listing 6.10 shows how the current loop is shortcut with continue
and prematurely ended with break
. Like Java, there is an optional label
.
In classic programming style, the use of break
and continue
is sometimes considered smelly. However, it can be useful for controlling the workflow in services that run in an endless loop. Similarly, returning from multiple points in the method is frowned upon in some circles, but other people find it can greatly increase the readability of methods that might be able to return a result early. We encourage you to figure out what you find most readable and discuss it with whoever else is going to be reading your code—consistency is as important as anything else.
As a final note on return handling, remember that closures when used with iteration methods such as each
have a different meaning of return
than the control structures while
and for
, as explained in section 5.6.
Exception handling is exactly the same as in Java and follows the same logic. Just as in Java, you can specify a complete try-catch-finally
sequence of blocks, or just try-catch
, or just try-finally
. Note that unlike various other control structures, braces are required around the block bodies whether or not they contain more than one statement. The only difference between Java and Groovy in terms of exceptions is that declarations of exceptions in the method signature are optional, even for checked exceptions. Listing 6.11 shows the usual behavior.
Example 6.11. Throw, try, catch
, and finally
def myMethod() { throw new IllegalArgumentException() } def log = [] try { myMethod() } catch (Exception e) { log << e.toString() } finally { log << 'finally' } assert log.size() == 2
Despite the optional typing in the rest of Groovy, a type is mandatory in the catch
expression.
There are no compile-time or runtime warnings from Groovy when checked exceptions are not declared. When a checked exception is not handled, it is propagated up the execution stack like a RuntimeException
.
We cover integration between Java and Groovy in more detail in chapter 11; however, it is worthwhile noting an issue relating to exceptions here. When using a Groovy class from Java, you need to be careful—the Groovy methods will not declare that they throw any checked exceptions unless you’ve explicitly added the declaration, even though they might throw checked exceptions at runtime. Unfortunately, the Java compiler attempts to be clever and will complain if you try to catch a checked exception in Java when it believes there’s no way that the exception can be thrown. If you run into this and need to explicitly catch a checked exception generated in Groovy code, you may need to add a throws
declaration to the Groovy code, just to keep javac
happy.
This was our tour through Groovy’s control structures: conditionally executing code, looping, and exiting blocks and methods early. It wasn’t too surprising because everything turned out to be like Java, enriched with a bit of Groovy flavor. The only structural difference was the for
loop. Exception handling is very similar to Java, except without the requirement to declare checked exceptions.[12]
Groovy’s handling of Boolean tests is consistently available both in conditional execution structures and in loops. We examined the differences between Java and Groovy in determining when a Boolean test is considered to be true. This is a crucial area to understand, because idiomatic Groovy will often use tests that are not simple Boolean expressions.
The switch
keyword and its use as a general classifier bring a new object-oriented quality to conditionals. The interplay with the isCase
method allows objects to control how they are treated inside that conditional. Although the use of switch
is often discouraged in object-oriented languages, the new power given to it by Groovy gives it a new sense of purpose.
In the overall picture, assertions find their place as the bread-and-butter tool for the mindful developer. They belong in the toolbox of every programmer who cares about their craft.
With what you learned in the tour, you have all the means to do any kind of procedural programming. But certainly, you have higher goals and want to master object-oriented programming. The next chapter will teach you how.
[1] Groovy has no opinion as to what beauty is. We’re sure that if it did, however, it would involve expressive minimalism. Closures too, probably.
[2] It would be rare to encounter a situation where more than one rule matched, but you never know when someone will subclass java.lang.Number
and implement java.util.Map
at the same time.
[3] Remember that the code in this book has been executed. If we didn’t have the break
statement, the book would have taken literally forever to produce.
[4] Even though the braces are optional, many coding conventions insist on them in order to avoid errors that can occur through careless modification when they’re not used.
[5] As of Java 5, enum
types can also be switched on, due to some compiler trickery.
[6] See “Replace Conditional with Polymorphism” in Refactoring by Martin Fowler (Addison Wesley, 2000).
[7] The main
and run
methods are constructed for you behind the scenes when running a script.
[8] Perl programmers will see the analogy to or die
.
[9] Please note that we use regexes here only to show the value of assertions. If we really set out to find the hostname of a URL, we would use candidate.toURL().host
.
[10] See Object Oriented Software Construction, 2nd ed., by Bertrand Meyer (Prentice Hall, 1997).
[11] But what if the last evaluated expression of a closure is a void method call? In this case, the closure returns null
.
[12] Checked exceptions are regarded by many as an experiment that was worth performing but which proved not to be as useful as had been hoped.
18.191.14.93