What version is your Java code?

Write and compile code targeting different Java versions

May 23, 2003

Q:

A:

public class Hello
{
    public static void main (String [] args)
    {
        StringBuffer greeting = new StringBuffer ("hello, ");
        StringBuffer who = new StringBuffer (args [0]).append ("!");
        greeting.append (who);
        
        System.out.println (greeting);
    }
} // End of class

At first the question seems rather trivial. So little code is involved in the Hello class, and whatever there is uses only functionality dating back to Java 1.0. So the class should run in just about any JVM with no problems, right?

Don't be so sure. Compile it using javac from Java 2 Platform, Standard Edition (J2SE) 1.4.1 and run it in an earlier version of the Java Runtime Environment (JRE):

>...\jdk1.4.1\bin\javac Hello.java
>...\jdk1.3.1\bin\java Hello world
Exception in thread "main" java.lang.NoSuchMethodError
        at Hello.main(Hello.java:20)

Instead of the expected "hello, world!," this code throws a runtime error even though the source is 100 percent Java 1.0 compatible! And the error isn't exactly what you might expect either: instead of a class version mismatch, it somehow complains about a missing method. Puzzled? If so, you will find the full explanation later in this article. First, let's broaden the discussion.

Why bother with different Java versions?

Java is quite platform-independent and mostly upwards compatible, so it is common to compile a piece of code using a given J2SE version and expect it to work in later JVM versions. (Java syntax changes usually occur without any substantial changes to the byte code instruction set.) The question in this situation is: Can you establish some kind of base Java version supported by your compiled application, or is the default compiler behavior acceptable? I will explain my recommendation later.

Another fairly common situation is to use a higher versioned compiler than the intended deployment platform. In this case, you do not use any recently added APIs, but merely want to benefit from tool improvements. Look at this code snippet and try to guess what it should do at runtime:

public class ThreadSurprise
{
    public static void main (String [] args)
        throws Exception
    {
        Thread [] threads = new Thread [0];
        threads [-1].sleep (1); // Should this throw?
    }
} // End of class

Should this code throw an ArrayIndexOutOfBoundsException or not? If you compile ThreadSurprise using different Sun Microsystems JDK/J2SDK (Java 2 Platform, Standard Development Kit) versions, behavior will not be consistent:

  • Version 1.1 and earlier compilers generate code that does not throw
  • Version 1.2 throws
  • Version 1.3 does not throw
  • Version 1.4 throws

The subtle point here is that Thread.sleep() is a static method and doesn't need a Thread instance at all. Still, the Java Language Specification requires the compiler to not only infer the target class from the left-hand expression of threads [-1].sleep (1);, but also evaluate the expression itself (and discard such an evaluation's result). Is referencing index -1 of the threads array part of such an evaluation? The wording in the Java Language Specification is somewhat vague. The summary of changes for J2SE 1.4 implies that the ambiguity was finally resolved in favor of evaluating the left-hand expression fully. Great! Since the J2SE 1.4 compiler seems like the best choice, I want to use it for all my Java programming even if my target runtime platform is an earlier version, just to benefit from such fixes and improvements. (Note that at the time of writing not all application servers are certified on the J2SE 1.4 platform.)

Although the last code example was somewhat artificial, it served to illustrate a point. Other reasons to use a recent J2SDK version include wanting to benefit from javadoc and other tool improvements.

Finally, cross-compilation is quite a way of life in embedded Java development and Java game development.

Hello class puzzle explained

The Hello example that started this article is an example of incorrect cross-compilation. J2SE 1.4 added a new method to the StringBuffer API: append(StringBuffer). When javac decides how to translate greeting.append (who) into byte code, it looks up the StringBuffer class definition in the bootstrap classpath and selects this new method instead of append(Object). Even though the source code is fully Java 1.0 compatible, the resulting byte code requires a J2SE 1.4 runtime.

Note how easy it is to make this mistake. There are no compilation warnings, and the error is detectable at runtime only. The correct way to use javac from J2SE 1.4 to generate a Java 1.1-compatible Hello class is:

>...\jdk1.4.1\bin\javac
    -target 1.1
    -bootclasspath ...\jdk1.1.8\lib\classes.zip
    Hello.java

The correct javac incantation contains two new options. Let's examine what they do and why they are necessary.

Every Java class has a version stamp

You may not be aware of it, but every .class file you generate contains a version stamp: two unsigned short integers starting at byte offset 4, right after the 0xCAFEBABE magic number. They are the major/minor version numbers of the class format (see the Class File Format Specification), and they have utility besides just being extension points for this format definition. Every version of the Java platform specifies a range of supported versions. Here is the table of supported ranges at this time of writing (my version of this table differs from data in Sun's docs slightly—I chose to remove some range values only relevant to extremely old (pre-1.0.2) versions of Sun's compiler):

Java 1.1 platform: 45.3-45.65535
Java 1.2 platform: 45.3-46.0
Java 1.3 platform: 45.3-47.0
Java 1.4 platform: 45.3-48.0

A compliant JVM will refuse to load a class if the class's version stamp is outside of the JVM's support range. Note from the previous table that later JVMs always support the entire version range from the previous version level and also extend it.

What does this mean to you as a Java developer? Given the ability to control this version stamp during compilation, you can enforce the minimum Java runtime version required by your application. This is precisely what the -target compiler option does. Here is a list of version stamps emitted by javac compilers from various JDKs/J2SDKs by default (observe that J2SDK 1.4 is the first J2SDK where javac changes its default target from 1.1 to 1.2):

JDK 1.1: 45.3
J2SDK 1.2: 45.3
J2SDK 1.3: 45.3
J2SDK 1.4: 46.0

And here is the effect of specifying various -targets:

-target 1.1: 45.3
-target 1.2: 46.0
-target 1.3: 47.0
-target 1.4: 48.0

As an example, the following uses the URL.getPath() method added in J2SE 1.3:

    URL url = new URL ("http://www.javaworld.com/columns/jw-qna-index.shtml");        
    System.out.println ("URL path: " + url.getPath ());

Since this code requires at least J2SE 1.3, I should use -target 1.3 when building it. Why force my users to deal with java.lang.NoSuchMethodError surprises that occur only when they have mistakenly loaded the class in a 1.2 JVM? Sure, I could document that my application requires J2SE 1.3, but it would be cleaner and more robust to enforce the same at binary level.

I don't think the practice of setting the target JVM is widely used in enterprise software development. I wrote a simple utility class DumpClassVersions (available with this article's download) that can scan files, archives, and directories with Java classes and report all encountered class version stamps. Some quick browsing of popular open source projects or even core libraries from various JDKs/J2SDKs will show no particular system for class versions.

Bootstrap and extension class lookup paths

When translating Java source code, the compiler needs to know the definition of types it has not yet seen. This includes your application classes and core classes like java.lang.StringBuffer. As I am sure you are aware, the latter class is frequently used to translate expressions containing String concatenation and the like.

A process superficially similar to normal application classloading looks up a class definition: first in the bootstrap classpath, then the extension classpath, and finally in the user classpath (-classpath). If you leave everything to the defaults, the definitions from the "home" javac's J2SDK will take effect—which may not be correct, as shown by the Hello example.

To override the bootstrap and extension class lookup paths, you use -bootclasspath and -extdirs javac options, respectively. This ability complements the -target option in the sense that while the latter sets the minimum required JVM version, the former selects the core class APIs available to the generated code.

Remember that javac itself was written in Java. The two options I just mentioned affect the class lookup for byte-code generation. They do not affect the bootstrap and extension classpaths used by the JVM to execute javac as a Java program (the latter could be done via the -J option, but doing that is quite dangerous and results in unsupported behavior). To put it another way, javac does not actually load any classes from -bootclasspath and -extdirs; it merely references their definitions.

With the newly acquired understanding for javac's cross-compilation support, let's see how this can be used in practical situations.

Scenario 1: Target a single base J2SE platform

This is a very common case: several J2SE versions support your application, and it so happens you can implement everything via core APIs of a certain (I will call it base) J2SE platform version. Upward compatibility takes care of the rest. Although J2SE 1.4 is the latest and greatest version, you see no reason to exclude users who can't run J2SE 1.4 yet.

The ideal way to compile your application is:

\bin\javac
    -target 
    -bootclasspath \jre\lib\rt.jar
    -classpath 
    
    

Yes, this implies you might have to use two different J2SDK versions on your build machine: the one you pick for its javac and the one that is your base supported J2SE platform. This seems like extra setup effort, but it is actually a small price to pay for a robust build. The key here is explicitly controlling both the class version stamps and the bootstrap classpath and not relying on defaults. Use the -verbose option to verify where core class definitions are coming from.

As a side comment, I'll mention that it is common to see developers include rt.jar from their J2SDKs on the -classpath line (this could be a habit from the JDK 1.1 days when you had to add classes.zip to the compilation classpath). If you followed the discussion above, you now understand that this is completely redundant, and in the worst case, might interfere with the proper order of things.

Scenario 2: Switch code based on the Java version detected at runtime

Here you want to be more sophisticated than in Scenario 1: You have a base-supported Java platform version, but should your code run in a higher Java version, you prefer to leverage newer APIs. For example, you can get by with java.io.* APIs but wouldn't mind benefiting from java.nio.* enhancements in a more recent JVM if the opportunity presents itself.

In this scenario, the basic compilation approach resembles Scenario 1's approach, except your bootstrap J2SDK should be the highest version you need to use:

\bin\javac
    -target 
    -bootclasspath \jre\lib\rt.jar
    -classpath 
    
    

This is not enough, however; you also need to do something clever in your Java code so it does the right thing in different J2SE versions.

One alternative is to use a Java preprocessor (with at least #ifdef/#else/#endif support) and actually generate different builds for different J2SE platform versions. Although J2SDK lacks proper preprocessing support, there is no shortage of such tools on the Web.

However, managing several distributions for different J2SE platforms is always an additional burden. With some extra foresight you can get away with distributing a single build of your application. Here is an example of how to do that (URLTest1 is a simple class that extracts various interesting bits from a URL):

public class URLTest1 implements IJREVersion
{
    public static void main (String [] args)
        throws MalformedURLException
    {
        URL url = new URL (args [0]);
        
        final String path, reference, query;
        
        // getRef() has been available since Java 1.1:
        reference = url.getRef ();
        
        if (JRE_1_3_PLUS)
        {
            // In Java 1.3+ everything is easy:
            query = url.getQuery ();
            // [Note: J2SDK javadocs fail to mention that getPath() was added
            // in version 1.3]
            path = url.getPath ();
        }
        else
        {
            // Prior to Java 1.3 I have to do extra work:
            final String file = url.getFile ();
            
            final int qindex = file.indexOf ('?');
            final int pindex = file.indexOf ('#');
            
            if (qindex >= 0)
            {
                path = file.substring (0, qindex);
                
                if (pindex >= 0)
                    query = file.substring (qindex + 1, pindex);
                else
                {
                    if (qindex < file.length () - 1)
                        query = file.substring (qindex + 1);
                    else
                        query = "";
                }
            }
            else
            {
                query = null;
                
                if (pindex >= 0)
                    path = file.substring (pindex);
                else
                    path = file;
            } 
        }
        
        System.out.println ("path: " + path);
        System.out.println ("ref: " + reference);
        System.out.println ("query: " + query);
    }
} // End of class

Instead of being preprocessed prior to compilation, URLTest1 code is compiled against J2SDK 1.4 bootstrap classes with -target set to 1.1. Furthermore, main() selects different execution paths at runtime based on the values of several static constants defined in this interface:

public interface IJREVersion
{
    /** 'true' iff the current runtime version is 1.2 or later */
    boolean JRE_1_2_PLUS = _JREVersion._JRE_1_2_PLUS;
    /** 'true' iff the current runtime version is 1.3 or later */
    boolean JRE_1_3_PLUS = _JREVersion._JRE_1_3_PLUS;
    /** 'true' iff the current runtime version is 1.4 or later */
    boolean JRE_1_4_PLUS = _JREVersion._JRE_1_4_PLUS;
    
    /*
     * Use a dummy nested class to fake a static initializer for the outer
     * interface (I want IJREVersion as an interface and not a class so that
     * all JRE_XXX constants could be imported via "implements").
     */
    abstract class _JREVersion
    {
        private static final boolean _JRE_1_2_PLUS, _JRE_1_3_PLUS, JRE_1_4_PLUS;
        
        static
        {
            _JRE_1_2_PLUS = ((SecurityManager.class.getModifiers () & 0x0400) == 0);
            boolean temp = false;            
            if (_JRE_1_2_PLUS)
            {
                try
                {
                    StrictMath.abs (1.0);
                    temp = true;
                }
                catch (Error ignore) {}
            }
            _JRE_1_3_PLUS = temp;
            
            if (temp)
            {
                temp = false;
                try
                {    
                    " ".subSequence (0, 0);
                    temp = true;
                }
                catch (NoSuchMethodError ignore) {}
            }
            _JRE_1_4_PLUS = temp;
        }
    } // End of nested class
} // End of interface

(My comments in the downloadable version of IJREVersion explain why I chose not to rely on java.version and other system properties.)

The java.net.URL API has suffered from poor design since Java's beginning and has undergone substantial changes in just about every major J2SE release so far. But thanks to runtime version switching, the above code parses a URL in the same fashion in all Java versions 1.1 through 1.4 (except certain esoteric edge cases). Although the above code is just a demo, it shows how you can deal with such situations in general.

At this point you might be wondering, "URL.getPath() and URL.getQuery() are not available prior to J2SE 1.3. Wouldn't this cause class linkage errors in earlier versions?" As it turns out, in most cases this will not be a problem. If you read the JVM and Java language specifications where they describe class loading and linking, you will see that in general, a JVM is not allowed to complain about a nonexistent method or class unless it is actually needed at runtime. (The process of checking symbolic references from a given class to other classes and interfaces it might depend on is called resolution. Resolution can be eager or lazy, but the specifications require that all errors resulting from this process be thrown only at a point where a symbolic reference is actively used.)

So, in J2SDK 1.2, the code never executes the getPath()/getQuery() part and everything works out nicely. This works even if you declare instances of unavailable classes as long as you don't initialize them. In general, just referring to a class in code will not necessarily require the class's definition at runtime. Certain constructs (calling a class method, referencing a field, creating an array with the class as the component type, and a few others) that do require the definition are known as active uses of a class. You should carefully steer around them as you code various version-specific chunks of your classes. In fact, this is how I coded IJREVersion itself.

Dynamically loading J2SE version-specific class versions

The above IJREVersion solution is great when you need it. But as dear as it might be to any former C/C++ programmer's heart, occasionally it just won't work. This happens, for instance, when you need to subclass a class only available in a J2SE version that is more recent than your base version. To load a class, a JVM always needs to load its superclass first, and, if it is not found, it will cause loading errors. And of course you can't put the extends declaration in a version-dependent if/then block. There are other, more obscure, cases: There appears to be a verification bug in all Sun-compatible JVMs that is an exception to the lazy resolution rule. When an astore instruction embedded inside a try/catch block references a nonexistent class, loading of the current class fails with a NoClassDefFoundError even if the instruction never executes. You can prove that this failure is caused by faulty verification by adding the class in question to the bootstrap classpath (bootstrap classes are not verified and the error goes away). At the source code level, this problem typically exhibits itself when you assign an instance of the nonexistent class to a variable or when the nonexistent class is a thrown exception.

In such a case, a more radical approach is guaranteed to work:

  1. Factor out the necessary methods into a version-neutral interface
  2. Implement this API in several version-specific and independent classes (it is safe to use nested classes here too)
  3. At runtime, instantiate the API implementation via a Factory class that uses IJREVersion internally to figure out which version to load

This approach is illustrated by the following reimplementation of URLTest1:

public class URLTest2
{
    public static void main (String [] args)
        throws MalformedURLException
    {
        URL url = new URL (args [0]);
        
        // This is where the version check takes place: 
        final IURLParser parser = URLParserFactory.newURLParser ();
        System.out.println ("got IURLParser implementation: " + parser);
        
        final String [] split = parser.splitURL (url);
        
        System.out.println ("path: " + split [0]);
        System.out.println ("ref: " + split [1]);
        System.out.println ("query: " + split [2]);
    }
} // End of class
public interface IURLParser
{
    String [] splitURL (URL url); 
} // End of interface
public abstract class URLParserFactory implements IJREVersion
{
    /**
     * Creates and returns a new instance of {@link IURLParser} implementation
     */
    public static IURLParser newURLParser ()
    {
        if (JRE_1_3_PLUS)
            return new JRE13URLParser ();
        else
            return new JRE11URLParser ();
    }    
        
    /*
     * The pre J2SE 1.3 implementation of IURLParser
     */
    private static class JRE11URLParser implements IURLParser
    {
        public String [] splitURL (final URL url)
        {
            final String path, reference, query;
            reference = url.getRef ();
            
            final String file = url.getFile ();
            
            final int qindex = file.indexOf ('?');
            final int pindex = file.indexOf ('#');
            
            if (qindex >= 0)
            {
                path = file.substring (0, qindex);
                
                if (pindex >= 0)
                    query = file.substring (qindex + 1, pindex);
                else
                {
                    if (qindex < file.length () - 1)
                        query = file.substring (qindex + 1);
                    else
                        query = "";
                }
            }
            else
            {
                query = null;
                
                if (pindex >= 0)
                    path = file.substring (pindex);
                else
                    path = file;
            }
            
            return new String [] {path, reference, query};
        }
        
    } // End of nested class
    
    /*
     * The Java 1.3+ implementation of IURLParser
     */
    private static class JRE13URLParser implements IURLParser
    {
        public String [] splitURL (final URL url)
        {
            return new String [] {url.getPath (), url.getRef (), url.getQuery ()};
        }
        
    } // End of nested class
    
} // End of class

Although the code above does not use Class.forName(), it effectively does the same kind of dynamic loading of different IURLParser implementations on an as-needed basis. This Factory approach ensures lazy resolution of all dynamic class dependencies and will work even when inline preprocessor-like usage of JRE_XXX constants (as in URLTest1) won't.

If you use a combination of this idea with correct -target and -bootclasspath compiler options, you will have a robust Java application supported on the largest possible set of J2SE platforms.

Code for version variation

Whew! Congratulations if you got this far. I have delved into Java details that are definitely not pretty. Coding for several Java versions in the same application is not common or enjoyable. But when it is a requirement or design choice, the ideas I outlined will serve you in good stead. Additionally, setting explicit compilation targets and knowing how a compiler locates class definitions will help you avoid accidentally building backwards-incompatible classes.

Vladimir Roubtsov has programmed in a variety of languages for more than 13 years, including Java since 1995. Currently, he develops enterprise software as a senior engineer for Trilogy in Austin, Texas.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies