madbean

Don't be fooled by javac -target 1.4

15 Sep 2006

In the last couple of weeks, I've been pinged a few times by cow orkers in the FishEye development team who were complaining about a mysterious JDK1.5 dependency in our code. Their development builds were breaking on a JDK1.4 JVM with an error like:

java.lang.NoSuchMethodError: java.lang.StringBuffer.append(Ljava/lang/CharSequence;)Ljava/lang/StringBuffer;

The sweat started pouring the first time I got that complaint; FishEye requires 1.4, we compile with -target 1.4 -source 1.4, and that method doesn't exist in 1.4, it was added in 1.5.

Have production builds been going out like this?!

I knew the answer to that question was "no", because we have a check like this in our Ant build (this uses some ant-contrib tasks):

<target name="check-prod-build">
    <propertyregex property="build.jdk.version" input="${java.version}" regexp="^(1\.\d).*" select="\1"/>
    <if><not><equals arg1="1.4" arg2="${build.jdk.version}"/></not>
        <then>
            <fail>Build aborting
====================================
A PROD BUILD REQUIRES JDK1.4.x ONLY!
====================================
You are using ${build.jdk.version} (${java.version}).
            </fail>
        </then>
    </if>

</target></pre>

Then the reason for putting that check in came back to me: because -target 1.4 -source 1.4 can still produce class files that won't run under 1.4. How? Because neither of those options change what rt.jar you are linking against.

This is best illustrated with a little example. Take this simple file:

public class Test {
    public static void main(String[] args) {
    StringBuffer b = new StringBuffer("Hello, ");
    CharSequence s = "world!";
    b.append(s);
    }
}

When compiled with JDK1.4 it will run under both JDK1.4 and JDK1.5, but not when compiled under JDK1.5:

$ export JDK14_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/1.4/Home
$ export JDK15_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/1.5/Home
$ $JDK14_HOME/bin/javac -target 1.4 -source 1.4 Test.java 
$ $JDK14_HOME/bin/java -cp . Test
$ $JDK15_HOME/bin/javac -target 1.4 -source 1.4 Test.java 
$ $JDK15_HOME/bin/java -cp . Test
$ $JDK14_HOME/bin/java -cp . Test
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.StringBuffer.append(Ljava/lang/CharSequence;)Ljava/lang/StringBuffer;
        at Test.main(Test.java:5)

The culprit is the call to append(). In JDK1.4, the only call that matches is append(Object) and you end up with bytecode like:

invokevirtual   #6; //Method java/lang/StringBuffer.append:(Ljava/lang/Object;)Ljava/lang/StringBuffer;

But with JDK1.5's rt.jar it matches append(CharSequence):

invokevirtual   #6; //Method java/lang/StringBuffer.append:(Ljava/lang/CharSequence;)Ljava/lang/StringBuffer;

Our Test.java is source-compatible with 1.4, the resulting Test.class as the correct class version for 1.4, but it is not link-compatible with 1.4 when compiled from 1.5. Have we learnt out lesson yet? Don't be fooled by javac -target 1.4 -source 1.4: it can still produce class files that won't run under 1.4.


Update Via Michael S and Chris N, you can also use the [code]-bootclasspath[/code], as described in the javac documentation.

  • Home
  • Blog