One of the primary design goals for Java is the idea of "write once, run anywhere." Java is therefore an ideal language choice when faced with the challenge of developing a platform-independent application.
Many articles have been written on applet development and the use of Java for bringing more interactivity to pages on the Web. While this is a valid use of Java, it's equally well suited for developing more traditional standalone or client/server applications that can run across multiple platforms. When Java is used to develop each of the tiers of an n-tier client/server application, its cross-platform nature offers additional benefits, including scalability (independent of hardware platform), flexibility and vendor independence.
One advantage of application development over applet development is that you have much more control over which version of the JDK to use. If you're developing a small application for deployment in the next couple of months, you should probably use JDK 1.1. However, you may want to consider using JDK 1.2 (also known as Java 2 SDK, version 1.2) for slightly longer-term projects. It offers many new features and improved performance.
Whichever version you decide to use, be sure to bundle the appropriate Java Runtime Environment when you distribute your application.
Application development, with its more relaxed security model, allows lower-level access to the operating system. This access, however, can result in your application's becoming platform-specific. In this article I'll look at five primary areas you should pay special attention to when developing cross-platform Java applications:
- Good programming practices
- OS differences and limitations
- Read/write files
- Graphical user interface design
- Other issues
Good Programming Practices
Depend only on the Core APIs.
The Java core API forms a standard foundation of classes that all Java Virtual Machines must implement to be considered "standard Java."
If you use any third-party class libraries, you'll need to distribute their runtime versions (which may require a separate license). It's also recommended that you verify that these third-party libraries make use of the core Java API only and don't use any native calls. Otherwise the code you write may be cross-platform. However, since your code depends on a third-party product that's not cross-platform, your resulting application won't be cross-platform either.
Enable all compiler warnings.
By default, the Java compiler generates warnings for code it considers ambiguous, platform-dependent or unclear. Although you can disable these warnings using the javac-nowarn option, it isn't recommended. Good programming practice suggests enabling all warnings - to their maximum level, if appropriate - in your compiler, then changing your code to eliminate all warnings that are produced.
Avoid deprecated methods
In Java application development it shouldn't be necessary to use deprecated methods. While they may work in the current release of the JDK, they're no longer the preferred method. Furthermore, since the plan is to remove them from the JDK, on some platforms they may have already been removed.
Avoid "undocumented features"
Almost any implementation of the core Java API will include supporting classes and/or packages that aren't themselves part of the core API. Such classes/packages are generally undocumented - although they may provide some quick-and-dirty functionality, they should be avoided since there's a good chance they won't be available on other platforms.
Similarly, your applications shouldn't depend on the implementation details of any one particular Java implementation. For example, use of the AWT component peer interfaces is documented as being "For use by AWT implementers." Peer classes are highly platform-specific. As a portable application makes use of the AWT rather than implementing it, you should avoid using peer classes in your application.
Follow any applicable API protocols.
Certain methods in the core API must be called or implemented in a certain pattern. By skipping over some methods or calling them out of sequence, you may write syntactically correct code, which is highly nonportable.
For example, the JDK 1.1 introduced a new AWT event model. According to the new documentation, the results may be unpredictable if you mix the new event model with the older 1.0 event model. (Refer to "Updating 1.0 source files to 1.1" in the JDK documentation download bundle for details.)
Several of the AWT methods (Component.Paint and Component.Update, in particular) accept a Graphics object as one of their arguments. While you can use and make changes to this object within the method to which it was passed, you shouldn't store a reference to it for later use. Since the AWT implementation is allowed to invalidate any Graphics object after the method returns, storing and later using a reference to it may work on one platform but isn't guaranteed to work on another. This type of problem is particularly difficult to debug.
If any of your classes override the Object.equals method, they should also override the Object.hashCode method so that for any two objects x and y:
x.equals(y) implies that a.hashCode()==b.hashCode()
See the JDK java.lang.Object.hashCode documentation for details.
Use System.exit method sparingly.
You should use this only for abnormal termination of your application. Generally, you should terminate your application by stopping all nondaemon threads. This is often as simple as returning from the main method in your application.
Since the System.exit method forces termination of all threads in the JVM, it may, for example, destroy windows containing user data without giving users a chance to save their work.
Adhere to good practices with thread scheduling and synchronization
As with any multithreaded language, don't rely on priorities or luck to synchronize threads. Thread scheduling may differ on different platforms, and even between different machines running the same platform.
Before using Java's multithreaded capabilities, read and understand the JDK documentation for the java.lang.Thread class and the java.lang.Runnable interface. Although Java does simplify multithreaded application development, it's still somewhat of an advanced topic and many of the thread synchronization issues still exist. Doug Lea's book, Concurrent Programming in Java, covers this subject in depth.
OS Differences and Limitations
Avoid Native Methods
Occasionally it may appear tempting to use native code to implement certain functionality. However, the native code is by its very nature platform-dependent. Although the rest of your Java application may be completely platform-independent, the fact that you're using native code means that your entire application is also platform-dependent.
Before using native code, consider the following alternatives:
You may think that confining the native methods to a single class and providing an implementation of that class for every Java platform is a sufficient workaround. This is actually a poor "solution" since the number of Java platforms is ever-increasing. In addition, some Java platforms have no ability to execute native code.
- Define a simple server that provides the necessary functionality, then write your Java application as a client of that server.
- Implement the functionality of the native method(s) in Java.
Exercise caution when using Runtime.exec
Although the java.lang.Runtime.exec methods provide a means to execute other applications on the system in a seemingly platform-independent way, you should use the Runtime.exec methods with caution. While these methods are part of the core API, the contents of the string arguments passed to these methods are generally platform-specific.
A sample dialog shown running under Windows NT 4.0 using JDK 1.1.5
If you intend to use the Runtime.exec methods in your application, then you should provide a way in which the user can specify the exact command strings. Ideally this would be done through a GUI. However, if the command(s) rarely need changing, you can prompt the user to enter them during installation, then store them in a properties file for later use.
Don't use hardwired platform-specific constants
The core Java API provides several ways to help you write a platform-independent application. For example, instead of printing strings with embedded carriage returns and/or line feeds, use the System.println method to print a string followed by an end-of-line character. Alternatively, use System.getProperty("line.separator") to retrieve the line separator for the current platform.
The AWT also provides ways to help you create a platform-independent GUI. Details of these features are described in a separate section below.
Issues particular to command line programs and processing
Not all Java platforms support the notion of standard input or standard output streams. Command line programs that use System.in, System.out or System.err may not run under all platforms. Consider implementing a GUI to provide the same functionality.
Even with those platforms that support command line programs, command line processing isn't consistent across these platforms. Although the most portable solution doesn't use the command line, this may not be acceptable for programs that need to be executed from within a script. In this case consider using the POSIX convention in which command line options are indicated with a leading dash. As an alternative, you may also want to consider reading the options from a properties file and/or providing a GUI.
Don't assume support for rendering unicode characters
Since not all platforms can display all Unicode characters, use only ASCII characters for the default text for messages, menus, buttons and labels. It's acceptable to use non-ASCII characters in localization resources and in text entered by the user.
Make no assumptions about the hostname format
The java.net.InetAddress.getHostName method returns the fully qualified host name for the InetAddress object. The format of the String returned by getHostName is, however, platform-dependent. On some platforms it'll contain a fully qualified domain name, yet in others it'll contain only the host part of that name.
If your application is merely displaying the resulting hostname, this is unlikely to cause a problem. However, when the name is passed to other systems or applications, it may be best to provide the IP address in addition to the hostname. Note that the IP address is available using the InetAddress.getAddress or InetAddress.getHostAddress methods.
Don't hard-code file paths
Hard-coded filenames, and especially hard-coded file paths (a directory name followed by the filename), frequently present portability problems in any language. You can address this problem in Java by using java.lang.System.getProperties("file.separator") to retrieve the file/path separator or by using a java.awt.FileDialog to prompt the user for a filename.
Alternatively, to dynamically construct a path and filename in a platform-independent way, use one of the java.io.File constructors. Be aware that even the format of an absolute path differs between platforms. For example, on DOS and Windows-based platforms, an absolute path typically begins with a letter followed by a colon. Under UNIX, absolute paths begin with a forward slash: "/".
A sample dialog shown running under KDE 1.0
(and Slackware Linux 3.6) using JDK 1.1:3
Don't hard-code line termination characters
A common problem when transferring text files between different operating systems occurs because of the different ways the platforms represent an end-of-line sequence. Some platforms, most notably DOS and Windows 3.1/95/98/NT, use the character sequence "\r\n" (a carriage return followed by a linefeed), whereas other systems - namely, most varieties of UNIX - use a single linefeed ("\n"). Others may use a single carriage return character ("\r").
Additionally, though Java internally uses Unicode characters for text, different platforms have different internal representations.
JDK 1.1 provides several methods to help address both of these problems. To output text in a platform-independent manner, use any of Java's println methods. These methods output the end-of-line character sequence appropriate for the platform on which your Java application is running. If you really need access to the appropriate end-of-line sequence for the current platform, use the value returned by java.lang.System.getProperty("line.separator").
Similarly, to input text files, use the java.io.BufferedReader.readLine method. This reads a line of text terminated by a line feed ("\n"), a carriage return ("\r") or a carriage return followed immediately by a linefeed, and returns the contents of the line, excluding any line-termination characters.
Be aware of the maximum length of filenames
All platforms have some maximum length on valid filenames. This may be more of an issue during installation of your application, especially when using the inner classes of JDK 1.1 (which concatenate the class and inner class names to come up with the resulting filename). However, it can also cause problems at runtime in that the JVM may have trouble locating some of the required classes.
One workaround to this is to package your classes into a Java archive (JAR). Alternatively, a ZIP file will work if you're developing for JDK 1.0.
Observe strict case distinctions on all platforms.
Some platforms - most notably DOS and the Microsoft Windows platforms - ignore case when comparing filenames. However, don't let this affect your development efforts. Always use the correct case for class names and filenames throughout your code. That way, your Java application will run as intended on a wider range of platforms.
Combining all your classes into a JAR or ZIP file, as discussed above, also helps preserve the case of filenames.
Avoid "special" filenames
Some filenames have a "special" meaning on certain platforms. For example, in DOS and Microsoft Windows platforms, "LPT1:" refers to the first printer port and "CON:" refers to the console. Similarly, under UNIX, /dev/null is the name of a special device that simply absorbs all output directed to it. In other words, don't try saving anything that you may later want to retrieve to a file with this name.
Be aware of these and other "special" filenames on the different Java platforms.
Don't mix event models
Don't mix the newer JDK 1.1 event model with the older JDK 1.0 event model - the results may be unpredictable. In particular, you may achieve the intended results on a single platform with a particular version of the JVM, but this same code may give different - or unexpected - results on another platform or with a different JVM.
The JDK 1.1 documentation download bundle contains details about upgrading in the section titled "Updating 1.0 source files to 1.1." Although it may be tempting when upgrading your application from JDK 1.0 to 1.1 to do this piece by piece, resist this temptation. Bite the bullet and do the entire upgrade in one step.
Use layout managers for sizing elements.
Don't hard-code the sizes or positions of any GUI components. Their exact size is almost guaranteed to differ between platforms, as will the size of the screen and the default windows.
Break the habit of laying out components by size and position, and learn how to use Java's layout managers effectively to achieve the desired results.
Blend your GUI with the desktop
The size of the screen and the number of colors available are likely to be different between platforms - even between users. Additionally, though you can use your own color scheme with particular RGB values, displays can vary considerably as to the exact color rendered by a given RGB value.
To make your colors blend in with the user's desktop, you can use colors from the java.awt.SystemColor class. Alternatively, you may want to provide the user some way of customizing the appearance - particularly the colors and fonts - of your application, either through an Options dialog, a properties file or both.
Rarely should you need to obtain the screen resolution, but if you really have to, you can use the java.awt.Toolkit.getScreenResolution method.
Don't assume existence of nonstandard fonts
The availability and size of fonts varies from platform to platform and even from machine to machine depending on installation.
Don't hard-code font sizes. Let text components take on their default size using an appropriate layout manager, and use java.awt.FontMetrics.stringWidth if you really need to determine the actual displayed width of a string.
Before selecting a nonstandard font, be sure to test for its availability and provide a suitable default font in the event that it doesn't exist. The default fonts supported by JDK 1.1 are: "Serif" (usually Times Roman), "SansSerif" (usually Helvetica) and "Monospaced" (usually Courier).
Consider international issues.
The JDK 1.1 provides extensive localization and internationalization features. It's worthwhile familiarizing yourself with them and using them where applicable in your Java application. When you first write your application, you may only expect users from one country, speaking one language, to use it. However, it's much easier to provide support for multiple locales at this point than to retrofit these changes at a later date.
Choosing a distributed framework
When you begin developing larger applications, you may soon realize the need and/or benefit of distributed objects, or at least the need to separate out parts of the application in more of a client/server role. There are various ways to do this, including writing your own application-specific protocol. The most widely supported distributed object frameworks include RMI, CORBA and DCOM.
CORBA, the most generic of these frameworks, supports not only a wide variety of platforms but also a variety of object development languages. This allows, for example, a Java client to communicate with a C++ CORBA server on a different machine, knowing only the interface published by that object.
RMI is specific to Java and makes communication between distributed Java objects fast and easy. It has a lot in common with CORBA, and there have been talks about possibly merging the RMI specification into CORBA sometime in the future.
Finally, DCOM is Microsoft's Distributed Common Object Model. Like CORBA, it's also language-independent but isn't platform-independent, being supported only on the newer Microsoft Windows operating systems. Furthermore, DCOM isn't supported in the standard Java API. This isn't recommended if you're aiming to develop a truly cross-platform solution.
Loading JDBC drivers
While Java Database Connectivity has many uses in standalone applications, it's most likely to be used when developing middleware that communicates client requests to a back-end database. It provides a way to allow flexibility in driver loading without having to recompile your code each time you want to change drivers. The java.sql.DriverManager class is responsible for providing this flexibility. The two ways in which a driver can be made available to the DriverManager class are through the jdbc.drivers system property or through a call to java.lang.Class.forName, which causes the driver to be explicitly loaded.
Using JDBC may restrict the portability of your application if not implemented with some forethought. To keep your application as portable as possible, provide a means for the user to specify the JDBC driver name. This can be done through either a GUI, the jdbc.drivers system property or a similar properties file.
Updating 1.0 Source Files to 1.1:
Internationalization support in JDK 1.1:
100% Pure Java home page:
InstallAnywhere by ZeroG Software:
InstallShield Java Edition 2 by InstallShield:
The Java Developer Connection (JDC):
D. Lea. Concurrent Programming in Java: Design Principles and Patterns. Addison Wesley.
About the Author
Steven Gould, a consultant for Deloitte & Touche Consulting Group/DRT Systems (www.drtsystems.com) in Dallas, develops primarily in C++ and Java under Windows NT and various UNIX platforms. He is a Sun Certified Java developer and a Microsoft Certified Solution Developer. Steven can be reached at