Bug Patterns In Java - Platform-Dependent Patterns - Online Article

Overview

When you detect any problem that can't be replicated across the vendor, version, or operating system spectrum, chances are the bug is linked to the platform in some way. We discuss the key to diagnosis with this family of patterns—recognition.

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

Unfortunately, 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, such as incorrect platform-specific separator characters in path names, are easy to spot and correct. 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.

The cost of writing cross-platform code is never zero!

About Platform Dependence

We'll break down our discussion of platform-dependent bugs into three separate patterns: vendor-, version-, and operating system–dependent bug patterns.

  • Pattern: Vendor-dependent bugs.
  • Symptoms: Errors may occur on some JVMs, but not on others.
  • Cause: There are some unspecified areas in the JVM specification (such as no required optimization of tail-calls, for example). This type of bug is less common than 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 versions of a particular vendor's JVM. 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 can be deleted; on Windows, they cannot).
  • Cures and Preventions: Varies for the problems encountered.

Vendor-Dependent Bugs

Of 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 there 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 a detail of the JVM is left open at the specification level, it can produce vendor-dependent behavior across JVMs.

For example, the JVM spec does not require optimization of tail calls. You may be familiar with tail-recursive calls, which 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:

A Tail-Recursive Factorial

public class Math
{
public int factorial(int n)
{
return _factorial(n, 1);
}
private int _factorial(int n, int result)
{
if (n <= 0)
{
return result;
}
else
{
return _factorial(n - 1, n * result);
}
}
}

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?

A Purely Recursive Factorial

public class Math
{
int factorial(int n)
{
if (n <= 0)
{return 1;}
else {return n * factorial(n-1);}
}
}

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 runtime, preventing stack overflows (especially if the tail calls are recursive).

Some JVMs implement this optimization; some don't. As a result, some programs will cause stack overflows on some platforms and not others. So, when we are concerned about the possibility of stack overflow, we will often have to write methods that are naturally expressed recursively using iterative control constructs such as for and while.

Version-Dependent Bugs

The platform dependence resulting from tail calls is a result of the nature 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.0 (before 1.4.0_b3) 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:

  1. Open a new JOptionPane.
  2. In the OptionPane, press the Enter key twice.
  3. Type "test."
  4. Press the up arrow key.
  5. Press the down arrow key.

Apparently, this sequence of operations, along with some similar sequences, put JOptionPanes into a strange state. If a user of your program discovers this bug, he might very well try to recover from it by frantically banging at his keyboard.

In fact, it's not hard to recover from such a state; pressing the right arrow key will do the trick. If, in the course of his banging, your user manages to hit the right arrow key, he will be able to move on as if nothing had happened. 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.0 for every platform we've tested—Windows, Solaris, and Linux. So it's likely an operating system–independent bug in Sun's JDK. After our team reported this problem, Sun eliminated the bug in the 1.4.0_b3 release.

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 under later versions of Java. 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 the company made in improving performance on that version.

Incidentally, Sun was not the only one dissatisfied with Swing's performance. Eclipse, an open-source project designed to deliver a robust, 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. Many aspects of the API are 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 to accompany it.

OS-Dependent Bugs

As the final example of some of the insidious forms of platform dependence you can experience on the Java platform, consider the following code, which at one point was used by the DrJava project editor for opening files and reading them into the editor window. As a first cut, we wrote the code as follows:

FileReader reader = new FileReader(file);_editorKit.read(reader, tempDoc, 0);

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

Now, you may have noticed that this code contains a great example of a Split Cleaner. 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 vestigial, unclosed FileReader won't cause any problems there. But open files can't be deleted on Windows, so a Windows user would encounter an exception at this point. 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:

FileReader reader = new FileReader(file);
_editorKit.read(reader, tempDoc, 0);
reader.close(); // win32 needs readers closed explicitly!

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 code is much lower in Java 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.

What We've Learned

In this articleon bugs that spring from platform dependencies, probably the most important thing we've learned is that the cost of cross-platform capabilities is never zero.

We've also learned the following:

  • There are many compatibility snags across different JVM versions, such as incorrect platform-specific separator characters in path names.
  • There are three patterns of platform-dependent bugs—those tied to the vendor, those tied to the version, and those tied to the operating system.
  • Vendor-dependent bugs, in which errors occur on only some JVMs, are caused by unspecified areas in the JVM's specification. This is a fairly uncommon problem.
  • Version-dependent bugs, in which errors occur on only some versions of a JVM, are caused by bugs in certain JVM implementations. This is seen more commonly.
  • OS-dependent bugs, in which errors occur only on some operating systems, are caused by the rules of system behavior being different for different OSes.
  • For implementation bugs, there is a great tool: Sun's Java Bug Parade, a list of JVM-specific implementation bugs.
  • An example: Tail calls allow us to replace the stack frame built for the calling method with that for the called method, which can decrease the depth of the stack at runtime, preventing stack overflows. However, only some JVMs implement this optimization; others don't.
  • Platform dependence is not just about OS dependence and vendor dependence—it's about JVM version dependence, both backward and forward. Teams are usually concerned about providing backward compatibility, and they often expect their code to maintain its behavior under later versions of Java. But in real life, this expectation doesn't always hold true.

About the Author:

No further information.




Comments

No comment yet. Be the first to post a comment.