What's New in Java 1.5?

Java
Typography
  • Smaller Small Medium Big Bigger
  • Default Helvetica Segoe Georgia Times

Java 1.5 (codenamed "Tiger" and also referred to as J2SE 5.0) is more than just your typical Java release. Traditionally, each release of the Java programming language consists of new and deprecated APIs and modifications within the Java Virtual Machine's (JVM) internals (e.g., garbage collection), etc. While the 1.5 release surely has its fair share of such changes, it is also accompanied by a fundamental change in the language itself. That is, the language contains new syntactical structures aimed toward enhanced programmer experience and, even more importantly, more robust applications.

Sun Microsystems first released Java 1.5 in the latter part of 2004, so why discuss the enhanced language features now? Well, if you've been using the Java programming language prior to the 1.5 release, you've probably grown accustomed to what you originally learned; it's now time for you to see how the language has matured and evolved since its inception! Continue reading for a discussion of the language's enhancements and any potential pros and cons that accompany them.

Language Enhancements

The enhancements made to Java beginning with the 1.5 release come in two classifications: either "syntactical sugar" enhancements or application robustness enhancements. That is, some features are geared toward making the programmer's life easier, while others are aimed toward better software engineering practices. The following sections discuss each major language enhancement, state their respective classification(s), and potentially provide an example code snippet of the feature.

Generics

By far, the addition of generics presents the most significant change in the Java programming language. Prior to the availability of generics, when you took an element out of a collection, you had to cast it to the type of the element originally stored in the collection. In addition to the syntactical ugliness of this, it was also prone to runtime errors. Every seasoned Java programmer has (unnecessarily) experienced the elusive ClassCastException during some collection-related operation due to casting an object to the incorrect runtime object type. Generics provide a mechanism for you to declare typed collections for the compiler and let it check your object types at compile time. Using pre-1.5 syntax, here is an example of a method whose job is to determine if a certain customer exists in a list of customers:

public boolean
doesCustomerExistInList(List list, Customer c) {
      for(int i = list.size() – 1; i >= 0; --i) {
            if( ((Customer) list.get(i)).equals( c ))
                  return true;
      }
      return false;
}

The previous code snippet contains a cast to Customer within the for loop. While this clutters the code, it's a language necessity. It's also possible that the list contains objects that are not customers, which would cause a ClassCastException to surface at runtime, likely causing a fatal application error. Using generics, here's the same example revisited:

public boolean
doesCustomerExistInList(List list, Customer c) {
      for(int i = list.size() - 1; i >= 0; --i) {
            if(list.get(i).equals( c ))
                  return true;
      }
      return false;
}

This snippet has two primary differences. First of all, the method's prototype accepts List, which tells the compiler to accept only a list of customers. This prototype guarantees that any caller of our method will pass a list containing only customers; anything else will generate a compile-time error. It's worth noting that whenever the text is encountered within code or APIs, it effectively reads "of type T." For example, List cats says that cats is a list of Cat objects. Having said that, we can also see that the list in the above code snippet is by definition a list of customers, and as such, the cast to a customer object is no longer necessary. By employing the use of generics, we have made our code both more robust and readable.

The generics feature has been given mixed reviews from the field. Many developers argue that it feels awkward to write declarations like Map = new HashMap(); and that they've made the Java APIs harder to comprehend. While they do certainly take some getting used to (just as any other large change in life does), I firmly believe they're for the better. Besides, you're never forced to use generics (though the compiler will rightfully generate type-safety warnings), so you can continue in your old habits if you so choose. While writing your own generified libraries requires some expertise, actually using generified libraries is quite simple and intuitive after you get past the syntax.

There is much more to generics that is beyond the scope of this article; for the full generics overview, including information on writing your own generified libraries, please refer to an excellent overview of generics written by its lead designer, Gilad Bracha.

Enhanced for Loop

If you have been working with Java for some time, one of your pet peeves is surely the "ugliness" of iterating over a collection of items. Take the following example, which iterates over a set of users (which at least can capitalize on generics to eliminate some of the drudgery) and deletes them:

public void deleteUsers(List users) {
      for(int i = 0; i < users.size(); ++i)
            users.get(i).deleteUser();
}

Using the enhanced for loop (also referred to as the for-each loop), the above example can now be rewritten as such:

public void deleteUsers(List users) {
      for(User user : users)
            user.deleteUser();
}

Much cleaner, isn't it? The loop logic, in everyday language, reads "For each User user in users, do the following..." As you can see, the use of the for-each loop combined with the elegance of generics increases readability by making the loop's logic much cleaner than in the pre-1.5 days. In those days, you would find yourself trying to digest the loop's stopping condition and trying to match up parentheses in an effort to figure out which object was being typecast to what type. The for-each loop can be used to iterate over any implementation of the Collection interface as well as arrays of any Java type, so its use is available in a wide variety of contexts. Here's an example that sums an array of integers:

public int sum(int[] nums) {
      int sum = 0;

      for(int num : nums)
            sum += num;

      return sum;
}

Again, this demonstrates another very straightforward and easy-to-understand example. So what can't this new loop construct do? The for-each loop can't be used for filtering, nor can you replace elements in a collection as you traverse it. Lastly, it also can't be used to iterate over multiple collections in parallel. Even with these "shortcomings," use of the for-each loop can greatly reduce the complexity of your loop structure in terms of readability. While this enhancement isn't going to buy you much outside of increased readability, it is one feature whose use I strongly encourage exploring.

Autoboxing/Unboxing

If you're not already familiar with this feature, it will surely grow to be one of your most-liked enhancements. Every Java programmer knows that if you wanted to (logically) put an integer primitive into either a collection or a map, you needed to first box the integer in its wrapper class, Integer. Then, when you finally wanted to retrieve the value from the structure and reassign it to an integer primitive, you needed to unbox the Integer by invoking the intValue() method. The new autoboxing and unboxing language enhancement eliminates this convoluted mess and does the work for you. The following example demonstrates the practical use of generics, the for-each loop, and autoboxing:

List custIds = new LinkedList();

public void addCustomer(Customer c) {
      int custId = c.getCustomerId();
      // The following line demonstrates autoboxing
      custIds.add( custId );
}

public boolean customerExists(Customer c) {
      // The loop's logic reads "For each int custId in the List of
      // Integers, do the following..." which shows that unboxing
      // is happening implicitly
      for(int custId : custIds) {
            if(custId == c.getCustomerId())
                  return true;
      }
      return false;
}

As you can see in the addCustomer(...) method, no longer must we wrap the primitive custId in an Integer wrapper object before inserting it into the linked list of Integer wrapper types. Also notice in the customerExists(...) method the absence of any code calling the intValue() method on an Integer object. The elimination of this boilerplate code greatly enhances code readability as the programmer must no longer sift through code that effectively had no logical implications.

The primary advantage of this enhancement is that there is less of a distinction between a primitive integer and its wrapper equivalent. However, there are subtle differences worth noting. First off, an Integer expression can contain a null value (e.g., Integer x = null). If an application tries to unbox a null value, a NullPointerException will be thrown. Also, there is a small degradation in performance when using autoboxing and unboxing, so using it in performance-critical (e.g., in a scientific application) is strongly discouraged.

The Java language designers recommend using this feature when there is an "impedance mismatch" between reference types and primitives (e.g., when inserting numerical values into a collection, as shown in the example above).

Type-safe Enumerations

An enumeration type in Java...all I can say is, "It's about time!" This is one of my personal favorite enhancements of this release. Why type-safe enumerations were not included in the early days of Java is beyond me, but they've finally arrived. In the "old" days, representing an enumerated type looked much like the following:

public static final int DAY_SUNDAY = 0;
public static final int DAY_MONDAY = 1;
...
public static final int DAY_SATURDAY = 6;


While this appears to be all right at a first glance, it has a few fundamental problems in terms of application runtime robustness. Its two most crucial problems are these:

  1. No type-safety—Since each day is simply an integer type, it's impossible for the compiler to check that an argument to a method expecting a "day" is actually going to receive a day. The best a programmer could do is check the parameters' values and generate a runtime error if an acceptable value was not passed, but it's still too late in the game; this is an error that could (and should) have otherwise been caught at compile time.
  2. No namespace—Integer constants must be (or should be anyway) prefixed with a string (e.g., DAY_) to avoid logical collision with other integer types.

Java 1.5 introduces full language support for enumerated types that addresses the problems noted above. Rewriting the above example using the new enumerated types looks like this:

enum Day { SUNDAY, MONDAY, ..., SATURDAY };

To address the type-safety concern, method prototypes can now be declared to accept an argument of type Day, and any attempt to pass anything other than a Day will result in a compile-time error.

The namespace concern is addressed through the enumerated type's name, Day. The Day type can now be used just as any other type; it can even be used in a switch statement. The above example shows Java's enumerated types in their most simplistic (and most commonly used) form; however, they're even more powerful as they can even contain data and operations. For a more comprehensive look at Java's implementation of enumerated types, please have a look at Sun's official enumeration summary.

Varargs

A shortened name for variable arguments, varargs is an enhancement that renders itself virtually useless in most programming scenarios, but I'll mention it for the sake of completeness. In past releases, any method that accepted an arbitrary number of arguments required the creation of an array containing the various values to pass to the method. Now, methods can have prototypes such as the following example:

public static String format(String pattern, Object... arguments);

The three periods after the final parameter's type indicate that the argument can be passed either as an array of arguments or as separate arguments. This implies that varargs may only be used in the final argument position. It's likely the only time you will experience a need to be familiar with the varargs syntax is when using the PrintStream::printf(...) or MessageFormat::format(...) APIs.

Static Import

Prior to Java 1.5, if you wanted to use a constant defined in an interface, you were forced to fully qualify the static member by name. For example, to compute the circumference of a circle, you would write this:

double circumference = 2 * Math.PI * radius;

 

The new static import construct allows you to directly import static members without inheriting from the type containing the static members or fully qualifying their names. Revisiting the example above, you can write this:

import static java.lang.Math.PI;
double circumference = 2 * PI * radius;

Although its use is not typically encouraged, you can statically import all members by using the asterisk qualifier (e.g., import static java.lang.Math.*). The latter example gives the logic enhanced readability because the expression is not bloated with location qualifiers. In practice, the static import feature should be used sparingly. If used too much, it can make your code much more difficult to understand as readers of your code will not automatically know where certain constants are defined and they will be forced to continuously glance at your code's import section. It's excellent for well-known constants (e.g., pi or other globally recognizable constants within your application's team) because the constant's value is implicitly known by the majority of the code's reviewers.

Compatibility Issues

With the drastic changes in syntactical structures available beginning with the Java 1.5 release, it is undoubtedly obvious that there is the potential for JRE compatibility issues if you start adopting 1.5 syntax features within already-existing applications and libraries. For example, if you are writing library code that is shared between disparate applications (e.g., a JDBC driver) and you introduce 1.5 syntactical structures within your code, you implicitly require all consumers of your library code to run within 1.5 (or higher) runtimes. This of course stems from the fact that pre-1.5 runtimes can't understand the byte-code generated by newer compilers. Therefore, you will have to provide your consumers with sufficient information (e.g., "As of version X of library Y, the use of a 1.5 or higher JRE is required.") to update (i.e., switch to a newer runtime) their application(s) to work with your library code.

Are These New Features Worth a Try?

I wouldn't be writing about them if they weren't! All jokes aside, as someone who's been writing Java code for several years, many of these enhancements truly are long-awaited and useful. Will any of these enhancements make your applications run faster? Probably not. However, they will make your applications more robust (e.g., if you use generics, the compiler can catch many common mistakes that don't typically surface until runtime...when it's already too late) and easier for your colleagues to read.

The enhancements presenting the most widely usable situations are generics, autoboxing and unboxing, type-safe enumerations, and the enhanced for loop. As you introduce generics into "legacy" code, you may even be surprised to find that you were putting the wrong object type into a data structure and never knew it! To start taking advantage of the new features, simply download a 1.5 (or later) version of the Java Development Kit (JDK), which is accompanied by a compiler that can parse the new language constructs. I strongly recommend experimenting with these new features and using them as you maintain old code and as you write code in the future. Your life will surely be simplified.

Joe Cropper is a Software Engineer at IBM in Rochester, Minnesota. Joe works as part of the System i Software Final System Test team, and his areas of expertise include database, multi-tiered client-server technologies, and a variety of object-oriented programming languages. Joe can be reached via email at This email address is being protected from spambots. You need JavaScript enabled to view it..

BLOG COMMENTS POWERED BY DISQUS