Groovy supports a limited set of datatypes at the language level; that is, it offers means for literal declaration and specialized operators. This set contains the simple datatypes for strings, regular expressions, and numbers, as well as the collective datatypes for ranges, lists, and maps. This chapter covers the simple datatypes; the next chapter introduces the collective datatypes.
In Groovy, everything is an object. In order to explain the choices made by Groovy’s designers, we’ll first go over some basics of Java’s type system. We will then explain how Groovy addresses the difficulties presented, and finally examine how Groovy and Java can still interoperate with ease due to automatic boxing/unboxing.
Java’s type system—primitives and references:
Java distinguishes between primitive types (such as int , double , char , and byte) and reference types (such as Object and String). There is a fixed set of primitivetypes, and these are the only types that have value semantics—where the value of a variable of that type is the actual number (or character, or true/false value). You cannot create your own value types in Java.
Reference types (everything apart from primitives) have reference semantics—the value of a variable of that type is only a reference to an object. Readers with a C/C++background may wish to think of a reference as a pointer—it’s a similar concept. If you change the value of a reference type variable, that has no effect on the object it was previously referring to—you’re just making the variable refer to a different object, or to no object at all.
You cannot call methods on values of primitive types, and you cannot use them where Java expects objects of type java.lang.Object. This is particularly painful when working with collections that cannot handle primitive types, such as java.util.ArrayList. To get around this, Java has a wrapper type for each primitive type—a reference type that stores a value of the primitive type in an object. For example, the wrapper for int is java.lang.Integer.
On the other hand, operators such as * in 3*2 or a*b are not supported for arbitrary reference types, but only for primitive types (with the exception of + , which is also supported for strings). As an example of why this causes pain, let’s consider a situation where you have two lists of integers, and you want to come up with a third list where the first element is the sum of the first elements of the other two lists, and so on. The Java code would be something like this:
- // Java code!
- ArrayList results = new ArrayList();
- for (int i=0; i < listOne.size(); i++)
- {
- Integer first = (Integer)listOne.get(i);
- Integer second = (Integer)listTwo.get(i);
- int sum = first.intValue()+second.intValue();
- results.add (new Integer(sum));
- }
Looking at the code block in the previous section, you can see that the problem is in the last two lines of the loop. To add the numbers, you must convert them fromInteger s into int s. In order to then store the result in another list, you have to create a new Integer . Groovy adds the plus method to java.lang.Integer , letting you write this instead:
- results.add (first.plus(second))
- // Java
- int sum = first.intValue()+second.intValue();
- results.add (new Integer(sum));
- results.add (first + second)
In order to make Groovy fully object-oriented, and because at the JVM level Java does not support object-oriented operations such as method calls on primitive types, the Groovy designers decided to do away with primitive types. When Groovy needs to store values that would have used Java’s primitive types, Groovy uses the wrapper classes already provided by the Java platform. Table 3.1 provides a complete list of these wrappers.
While we have the Java primitives under the microscope, so to speak, it’s worth examining the numeric literal formats that Java and Groovy each use. They are slightly different because Groovy allows instances of java.math.BigDecimal and java.math.BigInteger to be specified using literals in addition to the usual binary floating-point types. Table 3.2 gives examples of each of the literal formats available for numeric types in Groovy.
Interoperating with Java—automatic boxing and unboxing
Converting a primitive value into an instance of a wrapper type is called boxing in Java and other languages that support the same notion. The reverse action—taking an instance of a wrapper and retrieving the primitive value—is called unboxing. This automatic boxing and unboxing is known as autoboxing.
You’ve already seen that Groovy is designed to work well with Java, so what happens when a Java method takes primitive parameters or returns a primitive return type? How can you call that method from Groovy? Consider the existing method in the java.lang.String class: int indexOf(int ch) . You can call this method from Groovy like this:
- assert 'ABCDE'.indexOf(67) == 2
No intermediate unboxing:
If in 1+1 both numbers are objects of type Integer , are those Integers unboxed to execute the plus operation on primitive types? No. Groovy is more object-oriented than Java. It executes this expression as 1.plus(1) , calling the plus() method of the first Integer object, and passing the second Integer object as an argument. The method call returns a new Integer object of value 2 .
This is a powerful model. Calling methods on objects is what object-oriented languages should do. It opens the door for applying the full range of object-oriented capabilities to those operators. Let’s summarize. No matter how literals (numbers, strings, and so forth) appear in Groovy code, they are always objects. Only at the border to Java are they boxed and unboxed. Operators are a shorthand for method calls. Now that you have seen how Groovy handles types when you tell it what to expect, let’s examine what it does when you don’t give it any type information.