he IT industry is synonymous with change. Every day sees some new software version or specification released, which necessitates constant upgrades. Programming professionals often must upgrade business applications to the new versions of the software upon which they are built. To accommodate these rapidly changing business requirements, Sun Microsystems releases a JDK version with some new capabilities, enhancements, and improvements nearly every year.
This article describes the process of porting an existing Java-based application to a new JDK version and prescribes a porting process that ensures the functionality of the ported application will remain unchanged (see Figure 1).
|Figure 1. Porting Process Diagram
Porting is the process of making software that was written for one operating environment work in another operating environment that offers new value-added features and improved performance. Porting requires changing the programming details, which can be done at the binary (application) level or the source code level. The target configuration may include a new operating system, compiler, database, and/or other third-party software that will be integrated with the base product.
Before making the decision to port, one must determine the why, what, and how of the task. Answering the following questions in the given order will help:
- Why migrate the existing application and/or product?
- What in the existing application and/or product has to be migrated?
- How do I migrate the application and/or product?
Why Port in the First Place?
What are the reasons or external events that necessitate the porting process? One good reason for porting is to take advantage of the new features and improved functionality of a newer JDK, but you still must determine whether that’s a real business requirement. If none of your business applications require the new features the newer JDK version introduces, then spending the time and money to port your existing application may not be a good idea. On the other hand, if you are in the business of developing software products, then it is mandatory to constantly upgrade and support the latest functionality on the market in order to give customers value for their money.
The following are some of the scenarios in which one should port an application or product:
- When an existing application/product stops running on the targeted JDK [The changes introduced in the JDK, which are mandatory for implementation, can cause this (e.g., changes in the existing interfaces, changes in exception handling, introduction of new keywords such as assert in JDK 1.4, etc.)]
- When you want to improve the performance of your existing application/product with the features introduced in the targeted JDK
- When you want to remove the application’s dependency on third-party products/APIs, which are now introduced as features of the targeted JDK (e.g., using of logging API instead of Log4j)
- When a new feature replaces an old one and the new feature will be used in all forthcoming releases
- When you want to use the new JDK compiler, and you want the program to compile cleanly
- When you are in the business of product development or producing APIs, then upgrading is a business need for client requirements as well as for competitive edge
You may be thinking: if Java truly delivers “Write Once, Run Anywhere” (WORA) functionality, porting applications between JDKs should be so trivial as to make this article moot. True, but WORA is not always the reality. Sure, when you move from a Windows to a Unix platform or vice-versa, your JDK version remains the same. However, you have to keep some key points in mind while writing the code, including:
- Naming conventions—Unix is case sensitive.
- Use of appropriate separators (File.pathSeparator and File.separator)—Unix uses ‘/’ and ‘:’ as file and path separators, while Windows uses “” and ‘;’, respectively.
- AWT GUI components—They need special attention. Nowadays, it is advised to use swings.
- Threading model—In this arena, Java’s WORA falls flat on its face. Making threading truly platform independent is a nightmare for programmers. For example, programmers are advised to break up thread-processing code written in the run method into smaller chunks and, if necessary, to call yield() at the end of a loop iteration. In a nutshell, they must keep the pre-emptive and co-operative models in mind while writing this code (Read this article for more information: An Introduction to Java Thread Programming)
- Internationalization—Character sets are different in Unix and Windows.
- Proper path usage—Avoid using absolute paths, try to use relative paths instead. If that’s not possible, try to fetch from properties files.
So, with all these factors to consider, you very well may run into problems when you try to run or compile your existing application on a newer JDK version.
What Exactly Do I Need to Port?
Once you decide to go ahead with porting, your next step is to finalize what all needs to be migrated and determine their priorities. In order to ensure a smooth porting process, you must decide this and finalize your approach during the early stages of the project.
To help sort out your priorities, JDK changes can be broadly classified into four categories:
- Deprecated API
- New features in JDK
These changes can be introduced in Sun APIs as well as JDK APIs. Ideally, you should not use sun.* packages because Sun intends them only for their own usage. If you are using or extending a sun.* API in your application, be ready to port the changes or incompatibles introduced in it.
Sun technically classifies the compatibility between two JDK release versions as either binary compatibilities or source compatibilities. Binary compatibilities exist when the compiled code can run with the other version of the JDK. Source compatibility means the source code can comply and run with the other version of the JDK.
Binary compatibility has two types:
- Upward—when compiled code can run with a higher JDK version
- Downward—when compiled code can run with a lower JDK version
Similarly, source compatibility also has two types:
- Upward—when source code can comply and run with a higher version the JDK
- Downward—when source code can comply and run with a lower version of the JDK
In general, maintenance releases (e.g., JDK 1.4.1 and 1.4.2) support both upward and downward compatibility at the binary and source levels. The functional releases (e.g., JDK 1.3 and 1.4) support upward compatibility at both the binary and source levels, except for Sun’s stated incompatibilities. However, they do not guarantee downward compatibility at either level.
Removing the incompatibilities in the targeted JDK release is the top priority when porting your application, and it qualifies as the only MUST DO activity. Some of the incompatibilities you’ll find are changes in the existing interfaces, constants, exception handling, introduction of keywords (e.g., JDK 1.4 introduced assert), and removal of some methods and constants (mainly in sun.* packages). In most cases, you can compile and run code written in JDK 1.1 with JDK 1.2, 1.3, 1.4, and 1.5, as long as you don’t use any statements/APIs listed as part of the incompatibilities.
For more details about incompatibilities visit java.sun.com. The following are some other helpful links:
- Incompatibilities in J2SE 1.4.1 (since 1.4.0)
- Incompatibilities in J2SE 1.4.0 (since 1.3)
- Incompatibilities in J2SE 1.3 (since 1.2)
- Incompatibilities in J2SE 1.2 (since 1.1)
JDK improvements could be lumped in the new features category, but for a clear distinction and a better understanding of tasks, it’s better to view them as a separate task. Although these activities aren’t mandatory for making your application portable, from the performance, optimization, and best practices points of view, all the changes in this category are required. Before performing all the improvement changes, however, you must weigh the trade-off between the time and effort they require and the improvement you expect to get out of them.
A few of the suggested improvements for different JDK versions are:
- With JDK 1.1, use GregorianCalendar instead of java.util.Date for wider acceptability.
- With JDK 1.2, use Arraylist instead of Vectors, if you are not required to synchronize.
- With JDK 1.3, use the Timer class for scheduling future execution in a background thread instead of your own implementation.
- With JDK 1.4, the Preference API is better than the properties file for managing user preferences and configuration data.
Deprecation APIs are those that have been restructured and modified with new classes and methods that provide similar functionality. In general, whenever an API is deprecated, an alternative implementation is provided and information about it is offered in the API’s javadoc. Sun warns its users to withdraw support for deprecated APIs in future releases. Whenever possible, you should modify your application to remove references to deprecated methods/classes and to use the new alternatives. Deprecated APIs support methods and classes only for backward compatibility, and your Java compiler will generate a warning whenever you use them.
Marking something as deprecated is only one aspect of documentation and is not part of the OO paradigm. Deprecation also is not inherited, so you can still override a deprecated method without treating the subclass methods as deprecated methods. For example, say Class X has a deprecated method called
getStringValue(), and Class Y extends Class X, overriding the
getStringValue () method. While compiling test client TestX, which creates an instance of Class X and calls
getStringValue(), the compiler generates a warning. However, while compiling test client TestY, which creates an instance of Class Y and calls
getStringValue (), it doesn’t generate a warning.
You could treat a deprecated API as part of source compatibilities, but you should consider it a separate activity. Currently, you don’t have to eliminate the usage of deprecated APIs in order to port applications. In fact, the deprecated APIs from JDK 1.1 are still available in JDK 1.5 (J2SE 5). However, Sun still may withdraw their support in future releases.
Click here for a complete list of deprecated APIs still available in JDK 1.5.
New Features in JDK
Every major JDK version release provides new features in the form of new APIs and/or extensions to existing APIs. The following are some of the major features introduced in respective JDK versions:
- JDK 1.1 introduced inner classes.
- JDK 1.2 introduced swings, JDBC 2.0, and changes in Java security.
- JDK 1.3 introduced JNDI, RMI-IIOP, Java Sound, enhanced the Collection framework, and completed swings.
- JDK 1.4 introduced the JAAS Preferance API, the Logging API, JDBC 3.0, the assertion facility, and Regular expression.
- JDK 1.5 introduces major changes at the language level including autoboxing/unboxing, enhanced for loop, static import, typesafe enums, and varargs.
|Figure 2. JDK Releases and the Major Feature They Introduced
For a list of new features in your targeted JDK, refer to its javadoc.
Effectively using the targeted JDK’s new features, as well as its enhanced existing features, in your existing application is part of the porting process. Sometimes, using newly added features ends your dependency on third-party software/APIs (e.g., companies that previously used log4j for logging can now use the logging facility in JDK 1.4 instead). However, if you use new features just to enhance your existing application, then it’s an enhancement activity rather than a porting activity. Enhancement activities should not be included as part of the porting process because they may lead to some regressions in existing functionality, and then determining whether these problems are because of the porting activity or because of the enhancement becomes very difficult. Still, whether or not to go for the new features is purely dependent on your business requirements. It is not a mandatory part of the porting task.
How Do I Port My Application?
Once you have identified the porting tasks that need to be performed, as well as their priorities, make an action plan to execute the porting process. Testing plays a very important role in this execution. Create base line results or use existing base line results for unit, system, performance, and acceptance testing of your current system for each platform it supports. Figure 3 shows a porting process flow diagram.
|Figure 3. Porting Process Flow Diagram
Before starting the porting process, take the following steps:
- Prepare a list of the following changes:
- Incompatibilities introduced in Java API and Sun API for targeted JDK version
- Deprecated APIs
- Suggested improvements
- Newly added features
Try to estimate the number of occurrences of each identified change in the entire application source code (i.e., what is the approximate number of places you have to make changes corresponding to each JDK change introduced). Also estimate the time required for each change. Table 1 offers a sample template for capturing these details.
Table 1. Template for Capturing Change Details
- Prepare baseline results.
Consider a scenario in which a CRM product supports the following:
- ATG and BEA WebLogic application servers
- Windows, Linux, and Solaris operating systems (OSs)
- JDK 1.3
The product needs to be ported to JDK 1.4. As discussed previously, comparing the test results on the ported platform with the original (baseline) test results is crucial. So the baseline results and actual results must be captured. Table 2 offers a template for capturing test results.
Table 2. Template for Capturing Test Results
Capture the test results for each of the supported OSs (Windows, Linux, and Solaris) separately. If all the baseline results match the new test results of the ported application, then your porting process is almost complete.
Another advisable practice is performing Java compatibility tests as per your requirements [e.g., Pure Java Check and J2EE CTS (Compatibility Test Suite)]. Such compatibility ensures that implementations of Java technology meet Java specifications. Table 3 presents some of the changes that you might have to perform while porting an application from JDK 1.3 to JDK 1.4.
The areas where you’ll notice the majority of changes across different JDK versions are security, AWT, swings, I/O, exception handling, RMI/CORBA, and the collection API.
A Systematic Approach Covers All the Bases
The process and strategy this article describes may not be the only way to handle JDK porting activities, but from my experience with these types of project, it provides a comprehensive, systematic approach that can ensure a smooth process.
Credits: The author wishes to thank his project manager Atul Jain and his colleague Saurabh Bhatnagar for their valuable contributions as reviewers for this article.