Cross Compiling In Java

Can the latest tools added to the language be used without being limited to an old version?

A situation that occurs frequently is having to write code for a project that is in an old version of java. In Damavis, we always like to make use of the latest tools added to the language, so in these cases it can be frustrating to have to stick to an old version, especially when we know that with more modern versions, we could do the same thing in a more compact, more efficient way and even in less time.

It is normal that in these cases the doubt arises “can I write code in the most recent version of the language and when compiling it, the resulting bytecode is that of an older version of the JVM?”

How to cross-compile

Suppose, for example, that the application we are maintaining is written for java 8, and we want to use at least the latest LTS, java 11.

At first, it may seem that to achieve this is as easy as changing some parameters in the pom.xml file. In maven we have the properties maven.compiler.source and maven.compiler.target, and in the first one we indicate the source code version, and in the second one the bytecode version.

So it should be as simple as setting the source property to 11 and target to 8.

<properties>
   <maven.compiler.source>11</maven.compiler.source>
   <maven.compiler.target>8</maven.compiler.target>
</properties>

But if we try to compile like this, maven will report the following error:

source release 11 requires target release 11

What’s going on?

The properties source and target are not two independent variables. There are relationships between them that must be fulfilled. Specifically, the target property cannot be less than source: if we are writing java 11 code, the generated bytecode must be (at least) java 11 bytecode.

There are several reasons for this. First, there may be no way to translate code written in an earlier version of the JVM. Extensions and modifications to the semantics of the language generally require explicit knowledge on the Interpreter’s part. Therefore, an older version of the language will not be able to recognise the bytecode generated by a newer compiler.

But java is known to have very good backwards compatibility. Doesn’t this break that principle? No, because they are different concepts:


Backwards compatibility is when the JVM is able to execute code compiled in an earlier version of the language. This is a functionality of the JVM, and java is renowned for being a language that in its entire history has hardly ever introduced changes that would prevent this. But what we are trying to do here is to have the JVM execute code from a newer version, and this is not a supported feature.

The other reason that prevents this “backward compilation” is the libraries. For example, it is very likely that current code makes extensive use of the List.of() function. But this function was not added to the language until java 9. Therefore, the java 8 JVM will not be able to execute this code.

Therefore, when defining the maven attributes source and target, target must be greater than or equal to source.

Yes, it is possible for target to be greater than source. That is, we can write code using strictly java 8 syntax, and (using a java 11 or later compiler) compile it to java 11 bytecode. But in practice this is not very useful.

Cross-compiling with maven

Back to the initial question, we have our project in good old java 8. And no, we can’t use new language capabilities and keep the project running under the same environment. For that we have to do a migration.

This does not mean that we have to use the java 8 compiler. We can “cross-compile“: compile a java 8 project with our java 11 compiler. But to do this we do not have to use the source and target attributes. Instead, use the release property. So, in maven, instead of putting this:

<properties>
   <maven.compiler.source>8</maven.compiler.source>
   <maven.compiler.target>8</maven.compiler.target>
</properties>

we will put this:

<properties>
   <maven.compiler.release>8</maven.compiler.release>
</properties>

What’s the difference?

The release property is equivalent to defining the same value for source and target, but it adds something else: it indicates which version of the libraries should be used.

During a build, the libraries used are those that correspond to the JDK we are using to compile. Let’s assume the following mini-project example:

Example

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.damavis</groupId>
   <artifactId>release-usage</artifactId>
   <version>1.0-SNAPSHOT</version>

   <properties>
       <maven.compiler.source>8</maven.compiler.source>
       <maven.compiler.target>8</maven.compiler.target>
   </properties>

</project>

App.java:

package com;

import java.util.List;

public class App {

   public static void main(String[] args)  {
       List<String> myList = List.of("some", "new", "java", "code");
       /* ... */	 
   }
}

We compile this with JDK 11, and it doesn’t give any error. But when trying to run the generated jar with the java 8 interpreter, the following exception is thrown:

Exception in thread "main" java.lang.NoSuchMethodError: java.util.List.of(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;

This is because when compiling with java 11, java 11 uses the libraries of that version by default. The release attribute tells the compiler to use the libraries of the specified version.

We try replacing the previous pom with the following one (it is important to note that the release property is only recognised from maven-compiler-plugin version 3.7.0 onwards)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.damavis</groupId>
   <artifactId>release-usage</artifactId>
   <version>1.0-SNAPSHOT</version>

   <properties>
       <maven.compiler.release>8</maven.compiler.release>
   </properties>

   <build>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.7.0</version>
           </plugin>
       </plugins>
   </build>

</project>

Now the project does not compile anymore. And that’s right: we haven’t written valid java 8 code, so it doesn’t have to compile.

Conclusions

  • You cannot “compile backwards”. It is not possible to compile code written in a recent version of the language to work in a legacy JVM.
  • You can, however, use the JDK you have installed to compile projects made with an older JDK. For that, you have to use the release property instead of source and target.
If you found this post useful, we invite you to visit the Software category of our blog to see more articles like this one and to share it with your contacts so that they can also read it and give their opinion. See you in networks!

Pedro Riera
Pedro Riera
Articles: 2