In its 30 years of life, the Java language has earned a reputation for being forward compatible. This means that code written for an older version of Java can be generally compiled and run on a newer version of the Java runtime. This characteristic of the language makes it very appropriate for enterprise software, where applications tend to have long lifetimes and are often maintained for many years. It is also appreciated by library developers, who can target a wide range of versions of Java without having to change the code, contrary to the dramas that other languages have suffered. This stability comes at a price, namely the slow pace of language evolution. The Java designers have been very cautious when introducing new features to the language to ensure that new features do not break existing code.

But what about the other way around? Can code written for a newer version of Java run on an older version of the Java runtime? And why would you want to do that? The answer to the second question is that developers of libraries and frameworks often want to use the latest language features, but they also want their code to be usable by as many developers as possible.

For many years, the answer to the first question was that it is possible to compile Java code into bytecode that can be run with an older version of the JVM using the -target option of the javac compiler. By default, javac compiles code into bytecode that is compatible with the version of the JVM with the same version number as the compiler. For example, if we compile a program with javac version 21, the default target version is also 21, and the bytecode will be rejected by older versions of the JVM:

# When the shell is configured to use Java 21
$ javac -version
javac 21

$ javac HelloWorld.java

$ java -version
openjdk version "21.0.5" 2024-10-15 LTS

$ java HelloWorld
Hello World!

# Now we switch to Java 11
$ java -version
openjdk version "11.0.25" 2024-10-15 LTS

$ java HelloWorld
Error: LinkageError occurred while loading main class H
	java.lang.UnsupportedClassVersionError: HelloWorld has been compiled by a more recent version
	of the Java Runtime (class file version 65.0), this version of the Java Runtime only
	recognizes class file versions up to 55.0

We can instruct the javac compiler to produce bytecode compatible with the JVM 11 by specifying the -target option:

# When the shell is configured to use Java 21
$ javac -target 11 HelloWorld.java
warning: target release 11 conflicts with default source release 21

That “warning” is actually a show-stopper. The -target option is not enough to make the bytecode compatible with an older version of the JVM. We also need to downgrade the source version to 11:

# When the shell is configured to use Java 21
$ javac -source 11 -target 11 HelloWorld.java

# Now we switch to Java 11
$ java HelloWorld
Hello World!

That works, because the Hello World program is very simple and does not use any new features introduced since Java 11. Let’s see now what happens when we use some modern Java. We will use the following file:

public class CoordinatesMain {
    record Coordinates(int x, int y) {
    }
    public static void main(String[] args) {
        System.out.println(new Coordinates(1, 2));
    }
}

This code uses record, a feature introduced in Java 14 (preview) and Java 16 (final version). Therefore, the program will not compile with javac version 21 if we provide the -source 11 parameter, since records were not available in Java 11:

# When the shell is configured to use Java 21
$ javac -source 11 -target 11 CoordinatesMain.java
warning: [options] system modules path not set in conjunction with -source 11
CoordinatesMain.java:2: error: records are not supported in -source 11
    record Coordinates(int x, int y) {
    ^
  (use -source 16 or higher to enable records)
CoordinatesMain.java:2: warning: 'record' may become a restricted type name in a future release and may be unusable for type declarations or as the element type of an array
    record Coordinates(int x, int y) {
    ^
1 error
2 warnings

That makes sense. Let’s see another, more subtle example:

import java.util.List;

public class LastOfList {
    public static void main(String[] args) {
        System.out.println("The last one is " + List.of("a", "b", "c").getLast());
    }
}

It may not be obvious at a first glance, but the List::getLast() method is not available in Java 11. In fact, it was not introduced until Java 21. So what does javac do?

# When the shell is configured to use Java 21
$ javac -source 11 -target 11 LastOfList.java
warning: [options] system modules path not set in conjunction with -source 11
1 warning

It does compile. However, when we try to run it with Java 11, we get a runtime error:

# Now we switch to Java 11
$ java LastOfList
Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.Object java.util.List.getLast()'
	at LastOfList.main(LastOfList.java:5)

This is disappointing. Nobody likes runtime errors! The problem is that code was (successfully) compiled against the Java 21 standard library, which includes List::getLast(). But at runtime, when the JVM tries to execute the code and loads the List class from the Java 11 standard library, it cannot find the getLast() method, and throws an exception.

For years, it was a common misconception that the combination of the -source and -target options of javac was enough to make the bytecode compatible with older versions of the JVM. For example, library developers would use recent versions of Java and compile their code with -target 8 to make it compatible with Java 8, and then distribute the JAR file under the assumption that it would work with those older versions. But as we have seen, that is not the case. The -target option only affects the bytecode generated by the compiler, but it does not affect the standard library that the compiler uses to resolve symbols.

To address that, JEP 247 introduced the --release option to javac, starting with Java 9. The --release option is a more robust way to compile code so the resulting bytecode can be understood and executed by older versions of the JVM using older versions of the standard library. It replaces -source and -target options.

Back to the first example, --release does not change the outcome for CoordinatesMain.java, since the compiler cannot produce bytecode for the record feature. We get the same compile time error as before:

# When the shell is configured to use Java 21
$ javac --release 11 CoordinatesMain.java
CoordinatesMain.java:2: error: records are not supported in -source 11
    record Coordinates(int x, int y) {
    ^
  (use -source 16 or higher to enable records)
CoordinatesMain.java:2: warning: 'record' may become a restricted type name in a future release and may be unusable for type declarations or as the element type of an array
    record Coordinates(int x, int y) {
    ^
1 error
1 warning

But for the other example, LastOfList.java, the --release option does make a difference. When we compile the code with --release 11, the compiler uses the Java 11 standard library, and the code fails at compile time, which is much better than failing at runtime:

# When the shell is configured to use Java 21
$ javac --release 11 LastOfList.java
LastOfList.java:5: error: cannot find symbol
        System.out.println("The last one is " + List.of("a", "b", "c").getLast());
                                                                      ^
  symbol:   method getLast()
  location: interface List<String>
1 error

In summary, the --release option of javac is a better way to compile code that is intended to be run on older versions of the JVM. It ensures that the code is compatible with the standard library of the target version, and it catches errors at compile time instead of at runtime.

For Maven users, remove the old <maven.compiler.source> and <maven.compiler.target> properties from the pom.xml file and replace them with a single <maven.compiler.release> property, as described in the Maven documentation. Similarly, there is release property in the build.gradle file for Gradle projects, as described in the Gradle documentation.