How to avoid potential pitfalls in your Java application development
Writing enterprise Java apps for Solaris and Windows NT? Here are 10 common weak points to consider when aiming for cross-platform compatibility
By Steven Gould
Can you really achieve platform independence when developing your Java applications? You've probably heard of at least one high-profile Java development project that has been cancelled for one reason or another. And you may even have been swayed by articles characterizing Java's cross-platform promise as marketing hype. It's easy to inadvertently develop platform-dependent applications. So you need to be aware of a few potential problem areas. In this article, SunWorld contributor Steven Gould identifies 10 weak points to watch out for and offers suggestions for handling them. (3,500 words)
ou may assume that Java, by design, supports the idea of write-once, run-anywhere portability, and to some extent you're correct. However, depending on how you use -- or abuse -- it, Java may not live up to this promise. In order to be considered a viable enterprise programming language, Java has to have the built-in ability to access system-specific functionality. Unfortunately, use of this functionality often (in fact, usually) sacrifices the cross-platform capability of Java. But with a little planning such system-specific functionality is generally avoidable.
You may have also heard about compatibility problems between the Java virtual machine (JVM) under Windows 95/98/NT and Sun's standard JVM running under Solaris. Such problems are only a concern when developing applets, where the JVM is provided by the browser. When developing Java applications, you no longer have to rely on the JVM provided by the user's Web browser.
This article will highlight and provide workarounds to the top 10 potential pitfalls that can render your applications platform-dependent, thereby priming you to fully exploit Java's cross-platform capabilities.
Applets vs. applications
Java has received much attention as a platform for developing and deploying applets. An applet is a small program that is downloaded automatically from the Web and run in your Java-capable Web browser as a result of viewing a Web page. Applets provide for much more interactive Web pages than are available from static HTML.
Java is equally well suited for use in developing more traditional standalone or client/server applications that can run across multiple platforms. In Java terminology, an application is a set of one or more classes, the main one of which contains a public static void method called main:
public static void main( String argv )
While an applet runs in the JVM of the user's Web browser, a Java application is unbound by such constraints. Sun provides Java developers with a license to freely distribute Sun's runtime JVM -- available for Windows 95/98/NT and Solaris -- for use with Java applications. This can be installed independently of (and coexist alongside) your browser's JVM. By distributing the Java Runtime Environment (JRE) with your application, you have much more control over the version of the JVM used by the end user when running your application.
When developing Java applications, you're free to develop for whichever JVM you want. Generally you want to choose the latest Java Development Kit (JDK) or, at least, the latest minor release of the chosen major release. At this point in time, you probably want to choose between the JDK 1.1.6 if the project is small and is to be delivered in the near future, or the newer JDK 1.2 (which is still currently in beta).
You generally will deliver the appropriate version of the JRE with your application, thereby ensuring that the user has the correct version. Note that some of the Java development environments are tightly integrated into a particular version of the JDK, and it may not always be possible to update just the JDK. You may, therefore, find your choice of JDK restricted by the development tool(s) you choose.
Top 10 potential pitfalls
Here's a list of what I consider to be the top 10 potential pitfalls when it comes to writing cross-platform enterprise applications in Java. They are listed in order of the likelihood of occurrence, and workarounds are suggested where appropriate.
1. Native calls and unsupported APIs
In an effort to make Java as flexible as possible and allow it to tie into legacy systems, the designers of Java allow you to load existing system libraries written in languages other than Java. This capability is encapsulated within the load and loadLibrary methods of the java.lang.Runtime and java.lang.System classes. While these methods are useful for integrating a new Java application with existing legacy systems, they should be used with caution. They will make your Java application system dependent. These libraries typically exist on one or two platforms, and the names given to shared libraries also tend to be platform specific.
As far as classes within the standard JDK are concerned, use only the core APIs. If you intend to have your application or product certified as 100% Pure Java (more on certification below), this is actually a requirement. If not, you may want to take advantage of some of the "standard extensions" to the Java language. These include packages such as the Java Cryptography Extension (JCE) in javax.crypto.*, the Java Communications Standard Extensions in javax.comm.*, the JavaHelp API in javax.javahelp.*, the Java Media Framework (JMF) in javax.media.*, and any other package with a name that begins with javax.
Finally, avoid using deprecated APIs. They may very well be supported on the platform on which you're developing, however, they aren't guaranteed to be supported in future releases of the JDK.
2. Poor programming practices
Avoid using the -nowarn compiler option when compiling your Java source code using javac. This disables all warnings. Good programming practice suggests that you turn on all compiler warnings to their maximum level and eliminate all such warnings by making changes to your code.
Don't depend on the internal workings of a particular implementation. For example, avoid the java.awt.Component.getPeer method to access the peer component on the current platform. This method has been deprecated in JDK 1.1.
Similarly, do not make any assumptions about how a control will be drawn. The java.awt.Component paint and update methods include a Graphics context object to use for drawing. Do not save this object in an instance variable for future use. It may work on some platforms, but it will become an "undocumented feature" of that platform. It will almost certainly fail on another platform, and may even fail in a minor update of the JDK on the same platform.
Do not use any of the sun.* packages directly. They are often platform specific and are intended to function as support classes for the rest of the JDK, usually on the current platform only. While you may in fact be accessing them indirectly through the core API on one platform, do not rely on this behavior being the same on other platforms. Only access the core API classes, methods, and variables.
The static java.lang.System.exit method is intended to terminate the currently running JVM. It is intended for situations requiring abnormal termination. Don't make a habit of using this throughout your code. Instead, make effective use of exception handling -- when you really want to terminate your application, stop all non-daemon threads. This is often as simple as returning from the application's main method. The System.exit method forces termination of all currently running threads of execution. This may result in the user losing some of his or her work without first being given the chance to save it.
Finally, here are a couple of key points for assembling a team of Java application developers:
3. Use of platform-dependent routines
When using the java.lang.Runtime.exec methods be aware that the string argument passed to these methods is platform specific. Use of the Runtime.exec methods is only portable when the user has explicitly specified the command to be executed. This can be done either during installation or through an Options dialog or similar mechanism. Clearly, hard coding a command string into a call to one of the Runtime.exec methods is platform specific and should be avoided.
In addition to the methods in java.lang.Runtime, many of the methods in java.lang.System are also platform dependent in the use of their arguments. The getenv (now deprecated) and the newer getProperty methods are examples of such functions. When using one of the getProperty methods, be sure you're using one of the "standard properties," which are defined for all implementations of the JVM. These include "file.separator" and "line.separator" (discussed below).
When using any of the classes in the java.lang.Runtime or java.lang.System packages, be very aware of any platform dependencies you may introduce, albeit innocently.
On a related note, avoid using platform-specific constants anywhere in your code. Do not hard code platform-specific constants such as '/' or '\' in filenames and pathnames. Use System.getProperty("file.separator") to determine what file and path separator to use for the current platform. Similarly, use System.getProperty("line.separator") to determine the correct line separator to use in text output.
When using System.getProperty to retrieve the version, vendor, installation directory, or classpath of the JVM, or the OS name, version, or "architecture," don't make any assumptions about the format of the returned string. These may appear consistent between versions of the JVM on a given platform and from a particular vendor, but they may very well be different on different platforms or different JVMs from various vendors.
4. File I/O
File input/output has always been an area of concern when developing cross-platform applications.
Perhaps one of the most frequently encountered issues when transferring files between Solaris (or any other flavor of Unix) and Windows NT (or DOS, Windows 3.1/95/98) is the line termination character sequence. Typically, Unix uses a single character, '\n', whereas the DOS and Windows-based platforms use the sequence "\r\n". Other systems use a single '\r'. Rather than hard coding any of these sequences into your application you should either use the println methods (which handle the end of line for you) or the System.getProperty("line.separator") method mentioned above.
When reading files, it is recommended that you use the readLine method in java.io.BufferedReader class to read complete lines of text. This, again, handles the end of line sequences for you.
Do not hard code file paths. These are frequently system dependent not only in the location of certain files but also in the format of filenames and pathnames. For example, Windows NT supports the idea of drives A through Z, whereas this would result in an error under Solaris. Similarly, file and directory separators differ between platforms. Use System.getProperty("file.separator") -- or alternatively, use the static variable java.io.File.separator to determine the file separator for the current platform.
Unless specified by the user (either during installation or through an Options dialog) it is often best to try to stick to relative directories as opposed to absolute directories. For example, when Java WorkShop stores a project's information including source files, it uses relative directories wherever possible. In this way, Java WorkShop is able to store all the project information in a single project file, which is the same for Solaris as for Windows NT. This feature makes it very easy for a developer to transfer a project from a Solaris machine to Windows NT and back again (provided the same directory structure is maintained).
Another platform-dependent issue to be aware of when dealing with files is that most platforms have a maximum length of a fully qualified path-plus-filename. This maximum length may become an issue when using inner classes where the parent class is already several "levels" deep and has been installed in a deep subdirectory. Some platforms (most notably those based using FAT 16/32 filesystems) are case insensitive when it comes to filenames, but others (most notably the Unix platforms) are case sensitive. To avoid problems, it's always best to write your applications as if the filesystem is case sensitive. It helps if you come up with a consistent naming convention here. Finally, some platforms treat a few "special" filenames differently than normal files. For example, under Windows NT, "con:" refers to the console, and "lpt1:" refers to the first printer port. Likewise, under Solaris, don't expect to save anything to the file "/dev/null"!
5. Graphical user interface (GUI) development
Java first really made a name for itself in its capability to display a GUI within a Web page. Even though it is very effective at doing this, there are still a number of important considerations when developing a cross-platform GUI in Java.
Effectively sizing AWT components using absolute coordinates can be difficult on one platform. Trying to do this in a generic, cross-platform way is near impossible. Except for special-purpose tasks, you should use a layout manager to lay out your screens and dialogs. Then allow the components to take on their default sizes within the constraints of the layout manager. If you feel the basic layout managers don't give you enough control, take the time to familiarize yourself with the java.awt.GridBagLayout layout manager. While this takes a little more time to understand and master than the others, it provides great flexibility and control over layout and does so in a cross-platform manner. Additionally, you may want to look at embedding panels within panels (each can have a different layout manager) to achieve the desired results. This technique, possibly combined with the GridBagLayout, should cater to all your GUI needs while maintaining cross-platform support.
Furthermore, don't make any assumptions about the size of the screen -- in pixels or in inches -- on which the user will be running your application. If you really need to, you can get the screen size using the java.awt.Toolkit.getScreenResolution method.
Before using a font (other than the components default), make sure it's available -- and available in the required size -- on the current platform. If not, provide a suitable alternative from the list of default fonts: Serif (typically TimesRoman), SanSerif (typically Helvetica), or Monospaced (typically Courier) in JDK 1.1.
Whereas Windows NT and Solaris support display of Unicode characters, not all platforms are able to do so. To work around this, be sure to use only ASCII characters for the default text of messages, buttons, labels, and menu items. Non-ASCII characters can be used in localization resources and in text obtained from the user.
6. API protocols and event models
The core API contains certain methods that must be called or implemented in a particular way for them to produce the required results. Don't try to shortcut these or use them in a way not described in the JDK documentation. If you do, the results may very well work on one platform but still be highly nonportable. For example, using a reference to a Graphics object outside of the original update or paint method for which it was created may work on some platforms but not on others.
Similarly, the Abstract Windowing Toolkit (AWT) event model in JDK 1.1 differs from the JDK 1.0 AWT event model. Programs written using the 1.0 event model will work under a 1.1 JVM. Mixing the event models in the same application, however, is not guaranteed to work. Rather than attempting to convert an application from JDK 1.0 to JDK 1.1 slowly, make a concerted effort to do it all in one go. The JDK 1.1 event model -- especially when combined with the use of inner classes -- provides a much more object-oriented solution to the AWT event handling. In writing a new application, where you have much more control over the version of the JVM used, there is relatively little need now to use the JDK 1.0 event model.
7. Command-line programs and command-line arguments
Java applications that make extensive use of System.in, System.out, or System.err may not port to all platforms because not all Java platforms have the concept of standard input or standard output streams. If your application is intended to run only on Solaris and Windows NT, this isn't an issue. However, you may still want to consider providing a GUI as an alternative.
For those platforms that do support command-line arguments, be aware of the different conventions on the different systems. For example, under Solaris the POSIX convention of a dash (-) is used to introduce command-line options. Under Windows NT, the forward-slash (/) is used. You may want to implement several methods for displaying online help. Under Solaris, --h, --help, or -? are common. Under Windows NT /? is most common. It would be best to support all of these on all platforms.
Finally, provide a way to specify any or all command-line arguments through a GUI, or read them from a property file. Java Foundation Classes (JFC) provide a clean way to handle property files. Be sure to document the use of the property file in your user documentation.
8. Java Database Connectivity (JDBC)
Using JDBC and the DriverManager class in your application provides for great flexibility in loading the actual JDBC driver. You can change drivers without having to change and recompile your code. The DriverManager class selects from among the available drivers when a connection is established. Drivers can be made available to the DriverManager in two ways:
To keep your application as portable as possible, allow the user to choose or enter the exact JDBC driver name. This can be done by relying on the jdbc.drivers system property, or by using a properties file to supply a name to pass to the java.lang.Class.forName method.
Be aware that some drivers may not be as portable as your application and may not be available on every platform your application runs on. You should gracefully handle this situation in your application.
9. Thread scheduling
Thread scheduling is often a complex topic for new developers. It can be complex on a single platform. Making a multithreaded application cross platform can be even more challenging. Fortunately, this challenge is reduced when using Java due to its built-in support for threads and thread synchronization. Be sure to understand Java's java.lang.Thread class, the java.lang.Runnable interface, and the different methods of thread synchronization before developing a multithreaded application in Java.
Do not simply ignore thread synchronization issues because your application may, by chance, happen to work on one platform, but it will almost definitely give different -- even unexpected -- results on another platform. Reliance on thread priorities and on when each thread was started to achieve synchronization is poor programming and should be avoided.
10. RMI vs. CORBA vs. DCOM
When you begin developing larger enterprise applications you may soon find yourself faced with the need for distributed objects. These can be implemented in any number of ways including writing your own protocol. For greatest portability, however, adopt one of the more widely used distributed object architectures. Currently, the main choices are RMI, CORBA, and DCOM.
RMI has various advantages in terms of ease of use, ease of implementation, and performance. Its main disadvantage is that, currently, it only supports Java-to-Java communication.
CORBA is the most widely supported standard in terms of its cross-platform capabilities. A variety of object request brokers are available for both Windows NT and Solaris. It is also language neutral, so a Java object can communicate with a C++ object on a different machine knowing only the interface published by that object. Java also provides strong support for CORBA.
DCOM is Microsoft's Distributed Common Object Model which is also language independent. DCOM is a newer technology than CORBA and is only widely supported in the newer Microsoft operating systems. Standard Java doesn't directly support DCOM; for such support you would need to use Microsoft's Visual J++.
In a mixed Windows and Unix environment the best choice seems to be CORBA. As an alternative, RMI could be used if you only plan on Java-to-Java communication. The good news is that there are ongoing discussions between the Object Management Group and Sun to merge the RMI and CORBA standards.
100% Pure Java
If you are planning on developing a Java application to run under Windows NT and Solaris and decide to follow these guidelines, you should have a highly portable application. You may want to test its "purity" using some of the testing tools available from the 100% Pure Java home page. If the application is a commercial product, or even a high-profile internal application, you may choose to go one step further and get your application certified 100% Pure Java. Details of this process are beyond the scope of this article but there is plenty of information available on Sun's 100% Pure Java site.
About the author
Steven Gould currently works as a consultant for Deloitte & Touche Consulting Group/DRT Systems (http://www.drtsystems.com) in Dallas, TX. He develops primarily in C++ and Java under Windows NT and various Unix platforms. He is a Sun Certified Java Developer and Microsoft Certified Solution Developer. Reach Steven at firstname.lastname@example.org.