Get Familiar with J2SE 5.0 Collections

he collections API has always been one of the most important aspects of the Java Development Kit (JDK). Nearly every Java program makes use of collection classes such as HashMap, ArrayList, TreeSet and many others, each of which stores data in a variety of ways. Therefore, nearly every Java programmer must have a good understanding of how these classes work and when each is appropriate. But with the release of J2SE 5.0, everything about the way you use these classes has just changed!

The good news is that the learning curve isn’t steep. J2SE 5.0 actually simplifies the collections API in many ways. All the changes introduced by J2SE 5.0 require you to create less code than previous versions of Java.

What’s New for Collections in J2SE 5.0
This article focuses on new features that affect the collections API. Specifically, the features that this article covers include:

  • Generics
  • An enhanced For loop
  • Autoboxing

Each of these technologies affects the collections API. In this article, you’ll see a set of downloadable sample classes that tie all three technologies together. But before digging into the new collections API features, here’s a short primer on generics.

Understanding Generics
Generics are the most important feature added to the collections API. Both the enhanced for loop and autoboxing depend heavily on generics. If you’re familiar with C++, in one sense, generics are Java’s answer to C++ templates, but in many ways they are much more. Generics let you associate a specific type with collections. Prior to J2SE 5.0 Java collections were never directly associated with specific types, and that often caused problems. To see why, consider the following code.

   import java.util.*;      public class BasicCollection {     public static void main(String args[]) {       ArrayList list = new ArrayList();          list.add( new String("One") );       list.add( new String("Two") );       list.add( new String("Three") );          Iterator itr = list.iterator();       while( itr.hasNext() ) {         String str = (String)itr.next();         System.out.println( str );       }     }   }

The preceding code shows how a typical collection class might look prior to J2SE 5.0. The class creates an ArrayList and adds three strings to it. Note that there’s no specific type associated with this ArrayList; you can add any class that inherits from Object to the ArrayList.

Look at the while loop that retrieves the strings from the ArrayList.

while( itr.hasNext() ) {   String str = (String)itr.next();   System.out.println( str );}

The loop iterates over every item in the ArrayList. But notice what you have to do for each element retrieved?typecast each element. The typecast is required because the ArrayList does not know what types it stores. This is the problem that generics solve in Java. Generics allow you to associate a specific type with the ArrayList. So when you upgrade this BasicCollection class to use generics, the problem disappears. The GenericCollection class shown below constitutes an upgraded version

   import java.util.*;      public class GenericCollection {     public static void main(String args[]) {       ArrayList list = new ArrayList();          list.add( new String("One") );       list.add( new String("Two") );       list.add( new String("Three") );       //list.add( new StringBuffer() );          Iterator itr = list.iterator();       while( itr.hasNext() ) {         String str = itr.next();         System.out.println( str );       }     }   }

As you can see, only a few lines change when you upgrade the class to J2SE 5.0. The first is the line of code that declares the ArrayList; this version declares the ArrayList using a type of String.

   ArrayList list = new ArrayList();

Notice how the code combines the type with the name of the collection. This is exactly how you specify all generics, delimiting the name of the type with angle brackets (<>), placed immediately after the end of the collection name.

Next, when declaring the Iterator, you declare it as an Iterator for the String type. You specify the generic type for the iterator exactly as you did for the ArrayList, for example:

   Iterator itr = list.iterator();

That line specifies the Iterator’s type as String. Now, when you call the next method for the iterator, you no longer need the typecast. You can simply call the next method and get back a String type.

   String str = itr.next();

Although that’s a nice code-saving feature, generics does more than just save you from having to do a typecast. It will also cause the compiler to generate a compile error when you try to add an “unsupported” type to the ArrayList. What is an unsupported type? Because the ArrayList was declared to accept strings, any class that is not a String or a String subclass is an unsupported type.

The class StringBuffer is good example of an unsupported type. Because this ArrayList accepts only Strings, it will not accept a StringBuffer object. For example, it would be invalid to add the following line to the program.

   list.add( new StringBuffer() );

Here’s the real beauty of generics. If someone attempts to add an unsupported type to the ArrayList you won’t get a runtime error; the error will be detected at compile time. You will get the following compile error.

   c:collectionsGenericCollection.java:12:       cannot find symbol   symbol  : method add(java.lang.StringBuffer)   location: class java.util.ArrayList       list.add( new StringBuffer() );

Catching such errors at compile time is a huge advantage for developers trying to write bug-free code. Prior to J2SE 5.0 this error would have likely shown up later as a ClassCastException at runtime. Catching errors at compile time is always preferable to waiting for the right conditions at runtime to cause the bug, because those “right conditions” may very well appear only after deployment, when your users are running the program.

Using the Enhanced For Loop
A for each looping construct is something that Java lacked for some time. Other languages such as Visual Basic and C# provide that construct, allowing programmers to loop over all of the contents of a collection easily. A for each loop gives you the ability to access the contents of a collection without the need for an iterator. Now (finally) Java has a for each looping construct, in the form of the enhanced for loop.

The enhanced for loop was one of the most anticipated features of J2SE 5.0. The feature lets you simplify the code you’ve seen so far even further by eliminating the Iterator altogether. Here’s a version upgraded to use the enhanced for loop.

   import java.util.*;      public class EnhancedForCollection {     public static void main(String args[]) {       ArrayList list = new ArrayList();          list.add( new String("One") );       list.add( new String("Two") );       list.add( new String("Three") );       //list.add( new StringBuffer() );          for( String str : list  ) {         System.out.println( str );       }     }   } 

The preceding code eliminates the Iterator and while loop completely, leaving just these three lines of code.

      for( String str : list  ) {         System.out.println( str );      }

The format for the enhanced for loop is as follows.

   for( [collection item type] [item access variable] :       [collection to be iterated] )

The first parameter, the collection item type, must be a Java type that corresponds to the generic type specified when the collection was created. Because this collection is type ArrayList , the collection item type is String.

The next parameter provided to the enhanced for loop is the name of the access variable, which holds the value of each collection item as the loop iterates through the collection. That variable must also be the same as that specified for the collection item type.

For the last parameter, specify the collection to be iterated over. This variable must be declared as a collection type, and it must have a generic type that matches the collection item type. If these two conditions are not met, a compile error will result.

Primitive Data Types and Autoboxing
One more feature of considerable importance was added to the J2SE 5.0 implementation?the ability to automatically box and unbox primitive data types. This greatly reduces the complexity normally associated with the code used to add and access primitive data types in the collections API.

If you’re wondering what boxing and unboxing are, you must first look at how Java handled primitive data types prior to J2SE 5.0. Here’s a simple Java application that makes use of primitive data types with the collections API, prior to J2SE 5.0.

   import java.util.*;      public class PrimitiveCollection {     public static void main(String args[]) {       ArrayList list = new ArrayList();          // box up each integer as they are added       list.add( new Integer(1) );       list.add( new Integer(2) );       list.add( new Integer(3) );       //list.add( new StringBuffer() );          // now iterate over the collection and unbox       Iterator itr = list.iterator();       while( itr.hasNext() ) {         Integer iObj = (Integer)itr.next();         int iPrimitive = iObj.intValue();         System.out.println( iPrimitive );      }     }   }

The preceding code illustrates two distinct steps that had to occur when using primitives with the collections API. Because the collection types hold Objects rather than primitive types, you had to box primitive item types into a suitable wrapper object. For example, the preceding code boxes the int primitive data type into an Integer object using the following lines of code:

   list.add( new Integer(1) );   list.add( new Integer(2) );   list.add( new Integer(3) );

But the boxing requirement presents a problem when later code tries to access the primitive int variables stored in the collection. Because the primitive data types were stored as Integer objects they will be returned from the collection as Integer objects, not as int variables, forcing yet another conversion to reverse the boxing process:

   Iterator itr = list.iterator();   while( itr.hasNext() ) {   Integer iObj = (Integer)itr.next();     int iPrimitive = iObj.intValue();     System.out.println( iPrimitive );   }

In this case, the code first retrieves each item as an Integer object, named iObj. Next, it converts the Integer object into an int primitive by calling its intValue method.

Primitive Data Types and Autoboxing (continued)
Using autoboxing, storing primitive types in collections and retrieving them becomes far simpler. Here’s an example.

   import java.util.*;      public class AutoBoxCollection {     public static void main(String args[]) {       ArrayList list = new          ArrayList();          // box up each integer as it's added       list.add( 1 );       list.add( 2 );       list.add( 3 );       //list.add( new StringBuffer() );          // now iterate over the collection       for( int iPrimitive: list  ) {         System.out.println( iPrimitive );      }     }   } 

This autoboxing example begins by creating an ArrayList with the generic type of Integer.

   ArrayList list = new ArrayList();

After creating the list with the wrapper type of Integer, you can add primitive ints to the list directly.

   list.add( 1 );   list.add( 2 );   list.add( 3 );

Now, you no longer need to wrap each integer in an Integer object. In addition, accessing the individual list members is also considerably easier.

   for( int iPrimitive: list  ) {     System.out.println( iPrimitive );   }

The preceding example shows how you can use the enhanced for loop to iterate over a collection of primitive data types: in this case, retrieving each primitive variable directly as an int with no type conversion required. In other words, autoboxing and unboxing let you use primitive data types with collections as easily as objects.

The addition of generics to J2SE 5.0 give you the ability to specify exactly what type of data a collection can store. That, in turn, enables the compiler to ensure that the collection accepts only the correct type of object and eliminates the need to typecast items stored to or retrieved from the collection.

Generics also allow you to use the enhanced for loop, which provides the same functionality as the for each construct found in many other programming languages. This enhanced for loop” eliminates the need for iterators and greatly simplifies the process of iterating through a collection of items.

Finally, adding primitive data types to collections has always been a tedious process. Developers have had to box primitive types such as int into Integer objects and later unbox them back into int primitive types. Autoboxing and unboxing allows Java to handle this overhead for you, resulting in much clearer source code when using collections with primitive data types.

These changes necessarily alter the techniques you use to access collection data in J2SE 5.0?greatly simplifying Java code used to access the collections API.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: