class版本
在Java开发中,许多童鞋经常被各种版本的JDK搞得晕头转向,本节我们就来详细讲解Java程序编译后的class文件版本问题。
我们通常说的Java 8,Java 11,Java 17,是指JDK的版本,也就是JVM的版本,更确切地说,就是java.exe
这个程序的版本:
$ java -version
java version "17" 2021-09-14 LTS
而每个版本的JVM,它能执行的class文件版本也不同。例如,Java 11对应的class文件版本是55,而Java 17对应的class文件版本是61。
如果用Java 11编译一个Java程序,输出的class文件版本默认就是55,这个class既可以在Java 11上运行,也可以在Java 17上运行,因为Java 17支持的class文件版本是61,表示“最多支持到版本61”。
如果用Java 17编译一个Java程序,输出的class文件版本默认就是61,它可以在Java 17、Java 18上运行,但不可能在Java 11上运行,因为Java 11支持的class版本最多到55。如果使用低于Java 17的JVM运行,会得到一个UnsupportedClassVersionError
,错误信息类似:
java.lang.UnsupportedClassVersionError: Xxx has been compiled by a more recent version of the Java Runtime...
只要看到UnsupportedClassVersionError
就表示当前要加载的class文件版本超过了JVM的能力,必须使用更高版本的JVM才能运行。
打个比方,用Word 2013保存一个Word文件,这个文件也可以在Word 2016上打开。但反过来,用Word 2016保存一个Word文件,就无法使用Word 2013打开。
但是,且慢,用Word 2016也可以保存一个格式为Word 2013的文件,这样保存的Word文件就可以用低版本的Word 2013打开,但前提是保存时必须明确指定文件格式兼容Word 2013。
类似的,对应到JVM的class文件,我们也可以用Java 17编译一个Java程序,指定输出的class版本要兼容Java 11(即class版本55),这样编译生成的class文件就可以在Java >=11的环境中运行。
指定编译输出有两种方式,一种是在javac
命令行中用参数--release
设置:
$ javac --release 11 Main.java
参数--release 11
表示源码兼容Java 11,编译的class输出版本为Java 11兼容,即class版本55。
第二种方式是用参数--source
指定源码版本,用参数--target
指定输出class版本:
$ javac --source 9 --target 11 Main.java
上述命令如果使用Java 17的JDK编译,它会把源码视为Java 9兼容版本,并输出class为Java 11兼容版本。注意--release
参数和--source --target
参数只能二选一,不能同时设置。
然而,指定版本如果低于当前的JDK版本,会有一些潜在的问题。例如,我们用Java 17编译Hello.java
,参数设置--source 9
和--target 11
:
public class Hello {
public static void hello(String name) {
System.out.println("hello".indent(4));
}
}
用低于Java 11的JVM运行Hello
会得到一个LinkageError
,因为无法加载Hello.class
文件,而用Java 11运行Hello
会得到一个NoSuchMethodError
,因为String.indent()
方法是从Java 12才添加进来的,Java 11的String
版本根本没有indent()
方法。
注:如果使用--release 11则会在编译时检查该方法是否在Java 11中存在。
因此,如果运行时的JVM版本是Java 11,则编译时也最好使用Java 11,而不是用高版本的JDK编译输出低版本的class。
如果使用javac
编译时不指定任何版本参数,那么相当于使用--release 当前版本
编译,即源码版本和输出版本均为当前版本。
在开发阶段,多个版本的JDK可以同时安装,当前使用的JDK版本可由JAVA_HOME
环境变量切换。
源码版本
在编写源代码的时候,我们通常会预设一个源码的版本。在编译的时候,如果用--source
或--release
指定源码版本,则使用指定的源码版本检查语法。
例如,使用了lambda表达式的源码版本至少要为8才能编译,使用了var
关键字的源码版本至少要为10才能编译,使用switch
表达式的源码版本至少要为12才能编译,且12和13版本需要启用--enable-preview
参数。
小结
高版本的JDK可编译输出低版本兼容的class文件,但需注意,低版本的JDK可能不存在高版本JDK添加的类和方法,导致运行时报错。
运行时使用哪个JDK版本,编译时就尽量使用同一版本编译源码。