Mar 13, 2014 12:55 PM PT
Java 101: The Next Generation

Java 101: The essential Java language features tour, Part 3

Autoboxing and unboxing, enhanced for loops, static imports, varargs, and covariant return types

Thinkstock

Complete your tour of JDK 5's language features by exploring autoboxing and unboxing, the enhanced for loop, static imports, varargs, and covariant return types.

This article wraps up my three-part focus on new features introduced in JDK 5, this time looking at a number of features that have improved the overall productivity of coding in Java. Keep reading to learn more about autoboxing and unboxing, the enhanced for loop, static imports, varargs, and covariant return types.

As in previous articles, I'll start with a quick introduction, followed by at least one programming example and tips for making the most of these features in your programs. I also reveal some pitfalls that you'll want to avoid.


Source code for "Java 101: The Next Generation: The essential Java language features tour, Part 3." Created by Jeff Friesen for JavaWorld.

Autoboxing and unboxing

Java 5's autoboxing and unboxing language feature facilitates storing primitive type values in collections. A primitive value is stored in a collection by boxing (wrapping) the value in a suitable wrapper class object (e.g., an int is wrapped in a java.lang.Integer), which is stored. A primitive value is retrieved from a collection by retrieving the wrapper class object and unboxing (unwrapping) its value.

Behind the scenes, autoboxing places a primitive value into an object of the appropriate wrapper class type whenever a primitive type is specified but a reference is required. For example, the following code fragment boxes true and 27 into java.lang.Boolean and Integer wrapper objects, respectively, and assigns these objects' references to variables b and i:

Boolean b = true; // implemented as Boolean b = Boolean.valueOf(true);
Integer i = 27; // implemented as Integer i = Integer.valueOf(27);

Unboxing, meanwhile, obtains a primitive value from a wrapper object whenever a reference is specified but a primitive type is required. This task is accomplished by invoking a suitable wrapper method, such as Boolean's booleanValue() method and Integer's intValue() method. The code fragment below uses these methods to unbox a Boolean and an Integer:

boolean b2 = b; // implemented as boolean b2 = b.booleanValue();
int i2 = i; // implemented as int i2 = i.intValue();

The code fragments reveal that primitive values appear to be equivalent to objects. There is less source code, which is easier to read. These benefits are more pronounced when working with the Java Collections Framework. For example, consider Listing 1's word-counting application, which counts words read from the standard input stream and stores unique words and their frequency counts in a map.

Listing 1. BoxDemo.java (version 1)

import java.io.IOException;

import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class BoxDemo
{
   public static void main(String[] args) throws IOException
   {
      int ch;
      Map<String, Integer> m = new TreeMap<String, Integer>();

      // Read each character from standard input until a letter
      // is read. This letter indicates the start of a word.

      while ((ch = System.in.read()) != -1)
      {
         // If character is a letter then start of word detected.

         if (Character.isLetter((char) ch))
         {
             // Create StringBuilder object to hold word letters.

             StringBuilder sb = new StringBuilder();

             // Place first letter character into StringBuilder object.

             sb.append((char) ch);

             // Place all subsequent letter characters into StringBuilder
             // object.

             do
             {
                ch = System.in.read();
                if (Character.isLetter((char) ch))
                    sb.append((char) ch);
                else
                    break;
             }
             while (true);

             // Insert word into map.

             String word = sb.toString();
             if (m.get(word) == null)
                 m.put(word, 1);
             else
                 m.put(word, m.get(word)+1);
         }
      }

      // Output map in ascending order.

      Iterator i = m.entrySet().iterator();
      while (i.hasNext())
         System.out.println(i.next());
   }
}

Listing 1's Map<String, Integer> m = new TreeMap<String, Integer>(); statement uses generics to map String keys to Integer values, where a word is the key and a frequency count is the value. The code fragment that adds words to this java.util.Map instance is simplified because of autoboxing and unboxing:

if (m.get(word) == null)
   m.put(word, 1);
else
   m.put(word, m.get(word)+1);

The code fragment first determines if an instance of the word already exists in the map. This is the case when m.get(word), which returns the Integer value associated with the given word, returns the null reference. In this situation, m.put(word, 1); boxes 1 into an Integer object and creates a new Map entry that stores both the word and this object.

If m.get(word) returns an Integer, the code fragment invokes m.put(word, m.get(word)+1); to unbox the Integer that m.get(word) returns, add 1 to the resulting int, box the new sum into an Integer, and store this updated frequency count object in the appropriate Map entry.

Although you can largely ignore the distinction between a primitive type and its wrapper type, you need to remember these three things:

  • Unboxing throws java.lang.NullPointerException when the wrapper object is null.
  • The == and != operators perform reference identity comparisons on wrapper expressions and value equality comparisons on primitive type expressions.
  • There are performance implications to autoboxing and unboxing when used excessively.

Autoboxing/unboxing and arithmetic/comparison operators

Java automatically unboxes wrapper objects in expressions involving arithmetic and comparison operators (except for == and !=). This behavior can lead to confusion unless you know what's happening. Consider Listing 2.

Listing 2. BoxDemo.java (version 2)

public class BoxDemo
{
   public static void main(String[] args)
   {
      Long l1 = 127L;
      Long l2 = 127L;
      System.out.println(l1 == l2);
      System.out.println(l1 < l2);
      System.out.println(l1 > l2);
      System.out.println(l1+l2);
      l1 = 128L;
      l2 = 128L;
      System.out.println(l1 == l2);
      System.out.println(l1 < l2);
      System.out.println(l1 > l2);
      l2 = 129L;
      System.out.println(l1 < l2);
      System.out.println(l1+l2);
   }
}

Compile Listing 2 (javac BoxDemo.java) and run the application (java BoxDemo). You should observe the following output:

true
false
false
254
false
false
false
true
257

The false value that's output by System.out.println(l1 == l2); where each of l1 and l2 is assigned 128L is surprising. After all, true is output when these objects contain 127L.

The discrepancy arises because == compares references (whereas < and > compare primitive values) and the valueOf() method (used behind the scenes in the Long l1 = 127L; and Long l2 = 127L; assignments) employs caching.

Each of the java.lang.Byte, java.lang.Short, Integer, and java.lang.Long wrapper classes provides a valueOf() method that stores its primitive argument value in a wapper object of the appropriate type and returns a reference to this object.

To minimize object creation, valueOf() first examines its argument to see if it falls within a specified range (e.g., Long's valueOf() method checks its argument to see if it's greater than or equal to -128 and less than or equal to 127). If so, the wrapper object that wraps this value is returned from the cache. Otherwise, a new wrapper object is created that wraps the value and is returned. The following code fragment reveals this behavior for Long's valueOf() method (in the Java 7 update 6 implementation):

public static Long valueOf(long l)
{
   final int offset = 128;
   if (l >= -128 && l <= 127) { // will cache
      return LongCache.cache[(int)l + offset];
   }
   return new Long(l);
}

In Listing 2 (above), 127L is cached so l1 and l2 reference the same wrapper object, and l1 == l2 evaluates to true. In contrast, 128L isn't cached so l1 and l2 reference different wrappers objects, and l1 == l2 evaluates to false.

Enhanced for loop

Java 5 introduced the enhanced for loop as a more compact way to iterate over a collection. Instead of explicitly obtaining the collection's iterator and invoking its hasNext() and next() methods to perform the iteration, which clutters source code and introduces opportunities for errors, you can more easily iterate over a collection via the following syntax:

for (type id: c)

Reference type type matches the element type of collection c. Also, variable id receives the next element from c during each loop iteration. Because the colon can be interpreted as in, the syntax reads "for each id in c."

Listing 3 uses the enhanced for loop to iterate over a list of employees.

Listing 3. EnForLoopDemo.java (version 1)

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

class Employee
{
   private String name;
   private int age;

   Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   public String toString()
   {
      return name+" "+age;
   }
}

public class EnForLoopDemo
{
   public static void main(String[] args)
   {
      List<Employee> employees = new ArrayList<Employee>();
      employees.add(new Employee("John Doe", 25));
      employees.add(new Employee("Sally Smith", 32));

      // Traditional iteration.

      for (Iterator i = employees.iterator(); i.hasNext();)
           System.out.println(i.next());

      System.out.println();

      // Enhanced for loop iteration.

      for (Employee employee: employees)
           System.out.println(employee);
   }
}

Listing 3 reveals the verbosity of the traditional approach to iteration. It also reveals the three places where an error can be introduced: the assignment, the hasNext() method call, and the next() method call. Because the enhanced for loop hides the iterator, these three opportunities for error disappear.

Compile Listing 3 (javac EnForLoopDemo.java) and run the application (java EnForLoopDemo). You should observe the following output:

John Doe 25
Sally Smith 32

John Doe 25
Sally Smith 32

The enhanced for loop also can be used in an array context via a similar syntax to that shown earlier. In this context, you can specify a primitive type for the loop variable, and you can also specify the array to be iterated over. Listing 4 provides a simple demonstration.

Listing 4. EnForLoopDemo.java (version 2)

public class EnForLoopDemo
{
   public static void main(String[] args)
   {
      int[] x = { 10, 30, 500, -1 };

      for (int i: x)
           System.out.println(i);

      System.out.println();

      // The above code is implemented in
      // terms of a loop such as this loop:

      for (int j = 0; j < x.length; j++)
      {
         int i = x[j];
        System.out.println(i);
      }
   }
}

Compile Listing 4 (javac EnForLoopDemo.java) and run the application (java EnForLoopDemo). You should observe the following output:

10
30
500
-1

10
30
500
-1

The enhanced for loop has limitations. For one thing, it cannot be used where access to the iterator is required in order to remove an element from a collection. Also, it's not usable where you need to replace elements in a collection or an array during a traversal. Finally, you cannot use the enhanced for loop where you need to iterate over multiple collections or arrays in parallel.

Static imports

Item 17 in Joshua Bloch's Effective Java Programming Language Guide mentions that interfaces should only be used to declare types. They shouldn't be used to declare constant interfaces, which are interfaces that only exist to export constants. Listing 5's Switchable constant interface provides an example.

Listing 5. Switchable.java

public interface Switchable
{
   boolean OFF = false;
   boolean ON = true;
}

Developers resort to constant interfaces to avoid having to prefix the constant's name with the name of its class (e.g., Math.PI). For example, consider Listing 6's Light class, which implements the Switchable interface so that the developer is free to specify constants OFF and ON without having to include class prefixes (if they were declared in a class).

Listing 6. Light.java (version 1)

public class Light implements Switchable
{
   private boolean state = OFF;

   public void printState()
   {
      System.out.printf("state = %s%n", (state == OFF) ? "OFF" : "ON");
   }

   public void toggle()
   {
      state = (state == OFF) ? ON : OFF;
   }
}

A constant interface provides constants that are to be used in a class's implementation. These constants are an implementation detail that you shouldn't leak into the class's exported API because they could confuse the users of your class. Furthermore, in order to preserve binary compatibility, you're committed to support them even when they're no longer used by the class.

To satisfy the need for constant interfaces while avoiding the problems imposed by the use of these interfaces, Java 5 introduced static imports. This language feature can be used to import a class's static members. It's implemented via the import static statement whose syntax appears below:

import static packagespec . classname . (
staticmembername | * );

This statement's placement of static after import distinguishes it from a regular import statement. Its syntax is similar to the regular import statement in terms of the standard period-separated list of package and subpackage names. Either a single static member name or all static member names (thanks to the asterisk) can be imported. Consider the following examples:

import static java.lang.Math.*;   // Import all static members from Math.
import static java.lang.Math.PI;  // Import the PI static constant only.
import static java.lang.Math.cos; // Import the cos() static method only.

Once imported, static members can be specified without having to prefix them with their class names. For example, after specifying either the first or third static import, you could specify cos directly, as in double cosine = cos(angle);.

To fix Listing 6 so that it no longer relies on implements Switchable, we can insert a static import, which Listing 7 demonstrates.

Listing 7. Light.java (version 2)

package foo;

import static foo.Switchable.*;

public class Light
{
   private boolean state = OFF;

   public void printState()
   {
      System.out.printf("state = %s%n", (state == OFF) ? "OFF" : "ON");
   }

   public void toggle()
   {
      state = (state == OFF) ? ON : OFF;
   }
}

Listing 7 begins with a package foo; statement because you cannot import static members from a type located in the default package. This package name appears as part of the subsequent import static foo.Switchable.*; static import.

There are two additional cautions in regard to static imports:

  • When two static imports import the same named member, the compiler reports an error. For example, suppose package physics contains a Math class that's identical to java.lang's Math class in that it implements the same PI constant and trigonometric methods. When confronted by the following code fragment, the compiler reports errors because it cannot determine whether java.lang.Math's or physics.Math's PI constant is being accessed and cos() method is being called:

    import static java.lang.Math.cos;
    import static physics.Math.cos;
    
    double angle = PI;
    System.out.println(cos(angle));
    
  • Overuse of static imports can make your code unreadable and unmaintainable because it pollutes the code's namespace with all of the static members you import. Also, anyone reading your code could have a hard time finding out which class a static member comes from, especially when importing all static member names from a class.

Varargs

The variable arguments (varargs) feature lets you pass a variable list of arguments to a method or constructor without having to first box those arguments into an array.

To use varargs, you must first declare the method or constructor with ... after the rightmost parameter type name in the method's/constructor's parameter list. Consider the following example:

static void printNames(String... names)
{
   for (String name: names)
      System.out.println(name);
}

In this example, printNames() declares a single names parameter. Note the three dots after the String type name. Also, I use the enhanced for loop to iterate over the variable number of String arguments passed to this method.

When invoking the method, specify a comma-separated list of arguments that match the parameter type. For example, given the previous method declaration, you could invoke this method as follows:

printNames("Java", "JRuby", "Jython", "Scala");

Behind the scenes, varargs is implemented in terms of an array and ... is syntax sugar that hides the array implementation. However, you can access the array from within the method, as follows:

static void printNames2(String... names)
{
   for (int i = 0; i < names.length; i++)
      System.out.println(names[i]);
}

Also, you can create and pass the array directly, although you would typically not do so. Check out the example below:

printNames2(new String[] { "Java", "JRuby", "Jython", "Scala" });

Covariant return types

Java 5's final new language feature is the covariant return type, which is a method return type that, in the superclass's method declaration, is the supertype of the return type in the subclass's overriding method declaration. Consider Listing 8.

Listing 8. CovarDemo.java

class Paper
{
   @Override
   public String toString()
   {
      return "paper instance";
   }
}

class PaperFactory
{
   Paper newInstance()
   {
      return new Paper();
   }
}

class Cardboard extends Paper
{
   @Override
   public String toString()
   {
      return "cardboard instance";
   }
}

class CardboardFactory extends PaperFactory
{
   @Override
   Cardboard newInstance()
   {
      return new Cardboard();
   }
}

public class CovarDemo
{
   public static void main(String[] args)
   {
      PaperFactory pf = new PaperFactory();
      Paper paper = pf.newInstance();
      System.out.println(paper);
      CardboardFactory cf = new CardboardFactory();
      Cardboard cardboard = cf.newInstance();
      System.out.println(cardboard);
   }
}

Listing 8 presents Paper, PaperFactory, Cardboard, CardboardFactory, and CovarDemo classes. The key classes in this demo are PaperFactory and CardboardFactory.

PaperFactory declares a newInstance() method that CardboardFactory overrides. Notice that the return type changes from Paper in the PaperFactory superclass to Cardboard in the CardboardFactory subclass. The return type is said to be covariant.

Compile Listing 8 (javac CovarDemo.java) and run the application (java CovarDemo). You should observe the following output:

paper instance
cardboard instance

Covariant return types eliminate explicit casts. For example, if CardboardFactory's newInstance() method had been declared with a Paper return type, you would have to specify Cardboard cardboard = (Cardboard) cf.newInstance(); in the main() method.

In conclusion

Java 5 improved type safety mainly through generics but also through typesafe enums. It also improved developer productivity by introducing annotations, autoboxing and unboxing, the enhanced for loop, static imports, varargs, and covariant return types. Next up in the Java 101: The next generation series is a look at productivity-oriented language features introduced in Java 7.