En Damavis, siempre nos gusta hacer uso de las últimas herramientas añadidas al lenguaje. En ocasiones, es frecuente tener que escribir código para un proyecto que está en una versión antigua de Java, lo cual puede resultar frustrante. Sobretodo, cuando sabemos que con versiones más modernas, podríamos hacer lo mismo de forma más compacta y eficiente, e incluso en menos tiempo.
En estos casos, es normal que nos asalten algunas dudas. “¿Puedo escribir código en la versión más reciente del lenguaje y que, al compilarlo, el bytecode resultante sea el de una versión más antigua de la JVM?”
Ejemplo de compilación cruzada
Supongamos, por ejemplo, que la aplicación que estamos manteniendo se escribió para una versión antigua de Java. Pero nosotros queremos usar una más reciente.
De entrada, puede parecer que para conseguir esto es tan fácil como cambiar unos parámetros en el fichero pom.xml. En maven tenemos las propiedades maven.compiler.source y maven.compiler.target, en la primera se indica la versión del código fuente, y en la segunda la versión del bytecode.
Por lo tanto, debería ser tan simple como establecer la propiedad source y target.
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>Sin embargo, si intentamos compilar así, maven nos va a reportar el siguiente error:
source release 21 requires target release 21¿Qué significa el error de maven?
Las propiedades source y target no son dos variables independientes, hay relaciones entre ellas que se deben cumplir. Concretamente, la propiedad target no puede ser menor que source. Si estamos escribiendo código en Java 21, el bytecode generado debe ser (al menos) el de Java 21.

Existen varias razones para esto. La primera, que puede no haber forma de traducir el código escrito a una versión anterior de la JVM. Las ampliaciones y modificaciones en la semántica del lenguaje, generalmente requieren de un conocimiento explícito por parte del intérprete. Por tanto, una versión anterior del mismo, no será capaz de reconocer el bytecode generado por un compilador más reciente.
Sin embargo, Java es reconocido por tener muy buena compatibilidad hacia atrás. ¿No rompe esto ese principio? No, porque son conceptos diferentes.
¿Qué es la compatibilidad hacia atrás?
Compatibilidad hacia atrás es cuando la JVM es capaz de ejecutar código compilado en una versión anterior del lenguaje. Esta es una funcionalidad de la JVM y Java es reconocido por ser un lenguaje que en toda su historia apenas ha introducido cambios que impidan esto. No obstante, lo que aquí estamos intentando es que la JVM ejecute código de una versión más reciente y esa no es una funcionalidad soportada.
La otra razón que impide llevar a cabo esta “compilación hacia atrás” son las librerías. Por ejemplo, es muy probable que código actual haga un uso extensivo de .toList(). Sin embargo, esta función no se añadió al lenguaje hasta Java 16. Por tanto, la JVM de Java 11 no va a ser capaz de ejecutar este código.
Por consiguiente, cuando se definen los atributos de maven source y target, obligatoriamente target debe ser mayor o igual que source.
Además, es posible que target sea mayor que source. Es decir, podemos escribir código usando sintaxis estrictamente de Java 11 y (usando un compilador Java 21 o posterior) compilarlo al bytecode de Java 21. Aunque, en la práctica, esto no tiene mucha utilidad.
Compilación cruzada con maven
Volviendo a la pregunta inicial, tenemos nuestro proyecto en el viejo y buen Java 11. Y no, no podemos usar nuevas capacidades del lenguaje y que el proyecto siga funcionando bajo el mismo entorno. Para ello, hay que hacer una migración.
Esto no significa que tengamos que usar forzosamente el compilador de Java 11. Lo que implica es que podemos hacer “compilación cruzada”, compilar un proyecto para Java 11 con nuestro compilador de Java 21. Sin embargo, para hacer esto no hay que usar los atributos source y target. En su lugar, hay que usar la propiedad release. Por tanto, en maven, en lugar de poner esto:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>Pondremos esto otro:
<properties>
<maven.compiler.release>11</maven.compiler.release>
</properties>¿Cuál es la diferencia? La propiedad release es equivalente a definir el mismo valor para source y para target, pero añade algo más: indica qué versión de las librerías se debe usar.
Durante una build, las librerías que se usan son las que corresponden al JDK que estamos usando para compilar. Supongamos un mini-proyecto de ejemplo.
Ejemplo práctico con maven
<?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>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>App.java:
package com;
import java.util.List;
import java.util.stream.Stream;
public class App {
public static void main(String[] args) {
List<String> myList = Stream.of("some", "new", "java", "code").toList();
/* ... */
}
}Compilamos esto con el JDK 21 y no da ningún error. Sin embargo, al intentar ejecutar el jar generado con el intérprete de Java 11, salta la siguiente excepción:
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;Esto es debido a que al compilar con Java 21, éste por defecto utiliza las librerías de dicha versión. El atributo release indica al compilador que debe usar las librerías de la versión que se haya especificado.
Probamos a sustituir el pom anterior por el siguiente (es importante tener en cuenta que la propiedad release solo es reconocida a partir de la versión 3.13.0 del maven-compiler-plugin).
<?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>11</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>Ahora, el proyecto ya no compila. Esto es correcto, ya que no hemos escrito código Java 11 válido. Por lo tanto, no tiene que compilar.
Conclusión
- No se puede “compilar hacia atrás”. No es posible compilar código escrito en una versión reciente del lenguaje para que funciones en una JVM pasada.
- Sí se puede usar el JDK que tenemos instalado para compilar proyectos hechos con un JDK anterior. Para eso, hay que usar la propiedad release en lugar de source y target.
Si te ha parecido útil este post, te invitamos a visitar la categoría Software de nuestro blog para ver más artículos como este y a compartirlo con tus contactos para que ellos también puedan leerlo y opinar. ¡Nos vemos en redes!

