77.

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:

Listing 20-1: A Tail-Recursive Factorial

start example
 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);     }   } } 
end example

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?

Listing 20-2: A Purely Recursive Factorial

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

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, like the one for _factorial() in Listing 20-1).

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. (For more on the DrJava project, see 18, as well as Resources.)

Now, you may have noticed that this code contains a great example of a Split Cleaner (see Chapter 16). 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.



Bug Patterns in Java
Bug Patterns In Java
ISBN: 1590590619
EAN: 2147483647
Year: N/A
Pages: 95
Authors: Eric Allen

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net