Language improvements and models make great Java

Java enhancements, like generics and asserts, improve this already cool programming language

June 8, 2001-- As an object-oriented, network-enabled, platform-spanning language smart enough to include primitive data types for efficiency, Java has always been cool. But it just keeps getting better!

Once you code in a few different languages, you come to realize just how important the language is to solving a given problem. When you're using the right language, some puzzles just fall apart. But as someone who once tutored freshman who were ill-fated enough to take a class that forced them to solve real-world business problems using FORTRAN, I can tell you that the wrong language can turn a molehill of a problem into a nearly insurmountable mountain.

And, for all its beauty, Java experienced a few problems of its own. One of the major gaffes was the inconsistent, persnickety mechanisms for looping over arrays, vectors, and strings. Sun Microsystems Senior Staff Engineer Joshua Bloch came to the rescue on that front with the elegant Collections API. Just as Java itself is platform-spanning, Collections present a mechanism-spanning API, thereby creating uniform, consistent mechanisms for writing code.

The latest language improvements include generics and assertions. In addition, a variety of application templates and design patterns have become available to help you write better code more quickly. This article explores these enhancements:

  • Generics
  • Assertions
  • Effective Java
  • J2EE Blueprints
  • Common interface standards

Generics

Gilad Bracha, the specification lead and computational theologist for Sun, announced that the generics facility will arrive in the Tiger release of the Java platform (version 1.5). The bad news is that 1.5 is not due to be available until 2003. The good news is that an early-access version is available now, and you can actually make use of the technology in some practical ways.

It turns out that Merlin's (JDK 1.4) compiler is the generic-capable compiler, only with generics disabled. That means that the generic facility will undoubtedly be stable by the time it arrives. But it also means we'll have to wait a fairly long time for something that is (almost) here today.

(Note: For those familiar with the generics landscape, Bracha mentioned that Java's generics facility is based on Pizza and Generic Java, and that much of the documentation on those precursors (available on the Web) applies to generics.)

The advantages of generics

The ability to write generic classes, methods, and interfaces makes it possible to write general template classes. However, they significantly differ from C++ templates, as you'll see. Such template classes can be reused for multiple purposes without requiring general Object parameters that must be cast to the appropriate data type when the class is used.

For example, writing a generic data pool would make it possible to cache and deliver objects of type "X" to an application. One application could then create a pool of Car objects, while another application could create a pool of network Connectionobjects. Both applications would use the generic data pool facility. Neither application would have to write a data pool library, but both would be able to access Carobjects or Connection objects, as appropriate, as though they had written a custom library.

Prior to generics, the only way to write a generic data pool was to return objects of type Object. But that return value forces the application to cast the object to the real type, with code like this:

Car c = (Car) dataPool.get();

Such casts have two problems. First, they interfere with readability -- especially if you want to invoke a method on the returned object, because you wind up with a mess like this:

Car myCar = ((Car) dataPool.get()).initialize(make, model);

With generics, that code turns into something more readable:

Car myCar = dataPool.get().initialize(make, model);

But the second -- and larger -- issue with casts is that bugs in programs that use them show up as runtime errors. As Gilad said, a cast amounts to a declaration in which "we hope and pray" the variable contains an object of the indicated type at runtime. If it doesn't, the app is hosed.

On the other hand, with generics, the data pool's contents are documented, both for the human reader and the compiler. Because the compiler knows what the pool is supposed to contain, errors are discovered at compile time instead of runtime. That shift speeds application development and improves confidence in the final product.

How generics work

The declaration for a generic class, method, or interface uses angle brackets to indicate the generic part of the declaration:

interface List<E> {
      ...
    }

The above code defines an interface for a generic list class. Typically, single-character names indicate the generic component, but any valid identifier could be used.

Within the declaration, the angle brackets are no longer needed, so the identifier E would refer to the generic data type within the body of the declaration. For example:

interface List<E> {
      void add(E x);
      ...
    }

When the generic definition is referenced, the angle-bracket syntax is once again used to indicate the actual data type, like so:

List<Integer> myList = new LinkedList<Integer>();

(Note: In 1.5, the Collections API will be fully genericized, so you'll be able to use the code above. On the other hand, the generics facility has been cleverly designed so that it is totally backward compatible. As a result, old code of the form List myList = new LinkedList() will continue to run exactly as it did before. That code will still need to cast the data types, but it will run unchanged against the libraries' generic version.)

The data type named in a usage statement could itself be a generic type too. The declaration for a list of lists, for example, would look like this:

List<List<Integer>> myList;

(Note: Type aliases are currently being considered as an extension to the language in order to simplify such declarations. But the expert group is looking for a way of doing so that avoids adding (abusable) C-style typedefs to the language.)

A generic declaration can also name multiple generic types:

interface Function<A, B> { B value(A arg); }

The declaration above specifies that a class that implements the generic Function interface will include a value()method that takes an argument of type A, and returns an argument of type B.

One important difference between Java generics and C++ templates is that in Java, generic declarations are compiled and type-checked into classes that can be reused directly, instead of expanded into additional classes.

That fact has several important advantages for the Java developer:

  • If a generic class is used multiple times in an application, only one copy of it exists, rather than one copy for each data type. Consequently, program size is reduced.
  • When a bug is found and a correction made, there is no need to recreate dependent classes that were expanded from the template -- the fix automatically applies to all uses of the generic class.
  • Since the generic code is used as a class, rather than expanded from a source code template, the source code does not need to be present to use the generic code. That means third-party library providers can distribute generified components in the same way that they deliver other classes. (And, since a generic class can be used as though it weren't generic at all, a developer who is unfamiliar with generics can use the libraries as they normally would.)

Compilation also reduces application size since the template isn't expanded into a different class each time it is used. Finally, since generics are compiled into classfiles, the source code is not required to use a generic library. And since the compiled classes are totally backward compatible, they can be used with code that employs older code like LinkedList()instead of LinkedList<Integer>().

After the presentation, Ed Hansen, a senior application architect for Vision Service Plan and an experienced C++ template developer, had this to say: "It's a great API. And the fact that generic declarations are compiled and type-checked instead of expanded at runtime (like C++ templates) makes debugging a lot easier."

Using generics today

Despite the fact that generics are not yet a standard part of the Java platform, the generics facility can still be used today! You can download and use the generic compiler with the resulting classes running in Java 1.4.

At the moment, the best way to use the generics facility is to create a generic API for your existing library classes. For example, you could add generic declarations to an interface, or you could copy a class, remove the code, and add generics to the resulting stub.

You would then compile the generic stub with the generic compiler and import that stub when you compile the code that uses the library (with the generic compiler, once again). That code remains unchanged -- it still references List, for example, instead of List<Integer>.

When you compile, you turn on unchecked warnings, which warn you when a cast may not be valid -- a warning you cannot get today in any other way. However, you'll receive many meaningless warnings as well. It remains to be seen how easily you can filter out the warnings you don't care about so you can find the potential bugs in your app.

When you run the program, you run with the original classes -- the nongeneric versions. That way, you use the stable 1.4 versions at runtime, but you will have had the advantage of the additional compile-time error detection.

Since the 1.4 compiler is essentially identical to the generic compiler, you shouldn't have to recompile the code that uses the libraries. The resulting classes should be identical with either compiler. You might try a binary diff, to be sure. If you're the adventurous type, you can try creating a totally generic version of your library classes and compile that. The generic compiler can then type-check the implementation for consistency.

However, keep in mind that generic classes produced by the generic compiler have not been subjected to the same rigorous testing as classfiles produced by the production compiler. As a result, they could still harbor errors; treat this process as an experiment in progress.Do it for a stable library that won't be seeing much change and see how the classes run. If they perform, great; if not, drop back to the production compiler, report the bug, and wait for a new version of the generic compiler.

Assertions

Unlike generics, assertions are ready in Java 1.4. Assertions, according to Sun's Joshua Bloch, are "statements the programmer believes to be true" at any given point in the program. The assertion facility adds a new keyword to the language, assert, which captures those assumptions.

Adding assertions to a program documents what you believe to be true, making your assumptions clear to others. More importantly, assertion statements let the runtime engine (the VM) verifyyour assumptions.

The basic syntax for an assert statement is:

assert expression ;

Assertions speed up debugging because they immediately identify the differences between your expectations and reality. You don't have to backtrack from the point of the error to identify the cause.

You can also add information to the message generated when an assertion fails, like this:

assert expression : data ;

For example, if an index's value is calculated from some base value, then the following statement would validate the index and also indicate the value that caused the problem:

assert index > 0 : baseValue ;

To enable assertions, you compile with the -source 1.4 switch, which is necessary for backward compatibility. It alerts the compiler that assert is a reserved keyword. Without it, a class is free to use the word assertfor a variable or method name. (However, going forward, that is clearly not a good idea!)

Assertions are then compiled into the classfiles and enabled at runtime with the -ea (enable asserts) switch (for assertions in your code) or the -esa (enable system asserts) switch (for assertions in the runtime libraries). Or you can use both switches for the maximum runtime checking of assumptions.

The great news is that you can enable assertions in the field. You'll test with them enabled, of course, to speed the debugging process. And for maximum speed, normal production runs will disable the assertions. But when something goes wrong, you can enable them with a runtime switch to help isolate the error.

Bloch advised that you monitor your internal dialogue to write assertions. Every time you think to yourself, "At this point, I know that..." or write a comment to that effect, capture that assumption with an assertion. He mentioned four cases where asserts are particularly helpful:

Related:
1 2 Page 1
Page 1 of 2