Write once, run anywhere. That's the
promise, but sometimes the Java language doesn't deliver. Sure, the JVM
provides an unprecedented degree of cross-platform interoperability, but
minor glitches at both the specification and implementation levels often
prevent programs from behaving correctly on multiple platforms. In this
article, Eric Allen discusses some platform-dependent aspects of Java
programming to watch out for, such as tail-recursive calls, as well as
built-in vendor, version, and operating system dependence. Eric also
demonstrates some ways around this type of dependence. Share your
thoughts on this article with the author and other readers in the discussion forum by clicking
Discuss at the top or bottom of the
article.

One of the main advantages to programming in the Java language is the
tremendous degree of platform-independence it allows you. Rather than
having to form separate builds of your product for each target platform,
you can simply compile to bytecode and distribute to any platform with a
JVM. Or at least that's the way the story is supposed to go.

It's not quite that simple. Although Java programming can save untold
hours of developer support for multiple platforms, there are many
compatibility snags across different JVM versions. Some of these snags are
easy to spot and correct, such as using the platform-specific separator
character when constructing path names. But others can be difficult or
impossible to intercept.

For that reason, it's important to keep in mind the possibility that
some anomalous program behavior that defies explanation may be a bug in a
particular JVM.

Vendor-dependent bugsOf
course, if you want to see some of the many subtle platform-dependent bugs
that exist in JVMs, you need only make a casual inspection of Sun's Java
Bug Parade (see Resources).
Many of the bugs listed here are implementation bugs that apply
only to JVMs on one specific platform. If you don't happen to be
developing on that platform, you may not even know that your program trips
over it.

But not all Java platform dependence results from JVM implementation
bugs. Significant platform dependence is introduced by the JVM
specification itself. When the details of a JVM are left open at the
specification level, it can produce vendor-dependent behavior across
JVMs.

For example, as we saw back in "Improve
the performance of your Java code" (May 2001), the JVM spec does not
require optimization of tail-recursive calls. Tail-recursive calls
are recursive method invocations that occur as the very last operation in
a method. More generally, any method invocation, recursive or not, that
occurs at the end of a method is a tail call. For example, consider
the following simple code:

In this example, both the public factorial method and its
private helper method, _factorial, include tail calls;
factorial includes a tail call to _factorial, and
_factorial includes a recursive tail call to itself.

If that strikes you as a particularly convoluted way to write
factorial, you're not alone. Why not write it in the following,
much more natural form?

The answer is that tail calls allow for very powerful optimization --
they let us replace the stack frame built for the calling method
with that for the called method. This can drastically decrease the
depth of the stack at run time, preventing stack overflows (especially if
the tail calls are recursive, like the one for _factorial in
Listing 2).

Some JVMs implement this optimization; some don't. As a result, some
programs will cause stack overflows on some platforms and not others. If
only this optimization could be performed statically, we could simply
compile the bytecode to a tail-call optimized form and then enjoy the
optimization with platform independence. Unfortunately, as I explain in
the above-referenced article on the subject, this optimization can't be
performed statically.

Version-dependent bugsThe
platform dependence resulting from tail calls is a result of the JVM spec
itself. But the much more common causes of platform dependence are bugs in
JVM implementations. In the case of Swing, such bugs are widespread.

For example, the JOptionPane component in JDK 1.4 has an
associated bug. If a user adds text in a JOptionPane to a
line that comes immediately after a blank line, and then presses the DOWN
ARROW key, nothing happens. Try it for yourself:

Open a new JOptionPane.

In the OptionPane, then press the ENTER key twice.

Type "test".

Press the UP ARROW key.

Press the DOWN ARROW key.

Apparently, that sequence of operations (and similar sequences of
operations) put JOptionPanes into a strange state. If a user
of your program discovered this bug, he might very well recover from it by
frantically banging at his keyboard. (It's not hard to recover from such a
state; pressing the RIGHT ARROW key will do the trick.) Once he recovered,
he may no longer care much that things froze up, and may never even report
the bug. Users' standards of acceptability have been lowered substantially
by decades of buggy software.

Here's the kicker, though. This bug exists on all versions of Sun JDK
1.4 for every platform I tested -- Windows, Solaris, and Linux. So it's
likely an operating-system-independent bug in Sun's JDK.

This example illustrates that platform dependence is not just about OS
dependence, and it's not just about vendor dependence -- it's about JVM
version dependence, both backward and forward.

Teams are usually concerned about providing backward compatibility, but
they often expect their code to maintain its behavior on later versions.
Ideally, this expectation would be correct, but in reality it's not. In
fact, it's not so surprising that Sun introduced a bug in Swing on version
1.4 given the tremendous effort they made in improving performance on that
version.

Incidentally, Sun was not the only one dissatisfied with Swing's
performance. The Eclipse project, an open source project designed to
deliver a robust, open-source, full-featured, commercial-quality platform
for the development of highly integrated tools, implements an entirely new
widget toolkit, called the Standard Widget Toolkit (SWT). SWT is extremely
lightweight, because, unlike Swing, it leverages the underlying
platform-specific windowing system (see Resources).
The API is identical across the platforms on which it is implemented, but
the look-and-feel is entirely platform dependent. So we can expect a whole
new set of platform-dependent issues with that toolkit.

OS-dependent bugsAs our
final example of some of the insidious forms of platform dependence you
can experience on the Java platform, suppose we are writing code for an
editor that will open files and read them into an editor window. As a
first cut, we might write the code as follows:

The call to _editorKit.read reads the contents of the file
into a temporary document which is later added to the collection of open
documents. But after these two lines, we never refer to
reader again.

This code is taken from an early version of the DrJava IDE, Rice
University's free, open source Java IDE (see Resources).
Now, if you are familiar with the Split
Cleaner bug pattern, you may have noticed that this code provides a
great example of that pattern.

A FileReader is constructed to read the contents of the
file, but that FileReader is never closed. Of course, like
other instances of the Split Cleaner, this bug will not produce any
symptoms until some other attempt is made to access the file. But,
depending on the platform, it may not produce any symptoms even then!

Suppose the user later tries to delete this file. On UNIX, open files
may be deleted, so the vestigal, unclosed FileReader won't
cause any problems. But if the user is on Windows, open files cannot be
deleted, so an exception will be thrown. The bug in the previous code
listing was discovered when one of our unit tests managed to pass on UNIX
but not on Windows. Once the problem was diagnosed, it wasn't hard to
fix:

The cost of cross platform is not
zeroAs the examples in this column demonstrate, the Java
language is not immune to insidious platform-dependent bugs. The symptoms
of these bugs are quite varied, but you can expect some of them to bite
you at one time or another.

Although the cost of writing cross-platform is much less with the Java
language than in many other languages, it's not zero. The best advice I
can offer is to run your unit tests on as many platforms as possible,
using as many JVM versions as possible. And, as always, avoid writing
bug-prone code. Bug-prone code and platform dependence are a deadly
combination. Here's a look at what we covered this month:

Pattern: Vendor-dependent bugs.

Symptoms: Errors may occur on some JVMs, but not on others.

Cause: Some unspecified areas in the JVM specification (such as no
required optimization of tail-recursive calls, for example). This type
of cause is less common than with version-dependent bugs.

Cures and preventions: Varies for the problems encountered.

Pattern: Version-dependent bugs.

Symptoms: Errors may occur on some versions of a JVM, but not on
others.

Cause: Bugs in certain JVM implementations, such as Swing. This is a
more common cause than the vendor-dependent bugs.

Cures and preventions: Varies for the problems encountered.

Pattern: OS-dependent bugs.

Symptoms: Errors may occur on some operating systems, but not on
others.

Cause: Rules of system behavior are different on different operating
systems (for instance, on Unix, open files may be deleted; on Windows,
they may not).

Cures and preventions: Varies for the problems encountered.

I would like to thank DrJava developers Brian Stoler and John Garvin
for their assistance in identifying the latter two bugs discussed in this
article.

About the
authorEric Allen has a bachelor's degree in computer
science and mathematics from Cornell University and is a PhD
candidate in the Java programming languages team at Rice University.
Before returning to Rice to finish his degree, Eric was the lead
Java software developer at Cycorp, Inc. He has also moderated the
Java Beginner discussion forum at JavaWorld. His research concerns
the development of semantic models and static analysis tools for the
Java language, both at the source and bytecode levels. Eric is the
lead developer of Rice's experimental compiler for the NextGen
programming language, an extension of the Java language with added
language features, and is a project manager of DrJava, an
open-source Java IDE designed for beginners. Contact Eric at eallen@cs.rice.edu.