使用Javac's Release选项
如果在编程语言中选择一个版本帝的话,Java绝对是最有力的竞争者。拜模块化技术所赐,从Java9之后,每隔6个月Java就会发布一个新版本,从底层VM到上层语法特性都会进行特性的更新(以及删除)。在以前的Java8时代,Java开发人员下载一个JDK(Java Development Kit)能用好久,但随着Java版本的快速发布,就需要区分需要哪个版本的JDK:自己学习使用的JDK版本,生产环境上用的JDK版本,以及哪个版本是LTS(Long Term Support)的。为什么这么麻烦呢?原因是低版本的JVM无法运行高版本class文件。
向前兼容是Java的核心特性,Java21的JVM可以运行Java8的class文件,问题是:可以使用JDK21的编译器生成Java8的class文件,并顺利运行在Java8的JVM上吗?讲道理是可以的,也是兑现向前兼容特性的要求之一。如果一切顺利的话,Java开发同学就只用选择最新的JDK版本,然后按需编译生成目标版本的class文件即可,那为什么还有那么多的Java开发同学会在机器上准备那么多不同版本的JDK呢?事实就是Java并不会完全兑现向前兼容,只是说尽可能的做到向前兼容。
在Mac上使用Eclipse Termurin,不同版本JDK切换的知识可以参考这里。
在利用javac
编译生成class文件时,可以通过指定source和target两个选项来选择目标class的版本,在JDK8下,尝试以Java7的语法来源作为输入,Java7的目标class作为输出。
% openjdk8
% javac -version
javac 1.8.0_422
% javac -source 1.7 -target 1.7 T.java
警告: [options] 未与 -source 1.7 一起设置引导类路径
注: T.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
1 个警告
% java T
[A]
其中T.java
的内容为:
public class T {
public static void main(String[] args) {
java.util.concurrent.ConcurrentHashMap map = new java.util.concurrent.ConcurrentHashMap();
map.put("A", "B");
System.out.println(map.keySet());
}
}
利用source
和target
选项,可以在JDK8下实现编译输出Java7可以运行的class,感觉这看起来很正常。如果我们在maven中,可以通过配置compile插件来做到一样的效果,如下所示:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
这一切看着都像一回事,甚至通过javap
去观察T.class
时,也可以看到class的major version值是51
,妥妥的Java7,但事实上如果你在Java7的环境中运行T,就会得到一个类找不到的错误,原因就是ConcurrentHashMap
的keySet()
方法返回的类型是KeySetView
,一个Java8新增的类型。
使用javap
仔细观察T.class
,可以看到如下端倪。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/util/concurrent/ConcurrentHashMap
3: dup
4: invokespecial #3 // Method java/util/concurrent/ConcurrentHashMap."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String A
11: ldc #5 // String B
13: invokevirtual #6 // Method java/util/concurrent/ConcurrentHashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
16: pop
17: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_1
21: invokevirtual #8 // Method java/util/concurrent/ConcurrentHashMap.keySet:()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
27: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 17
line 6: 27
第21行指令调用ConcurrentHashMap
的keySet()
方法,返回类型为Java8新增的KeySetView
。一个Java7的class,在Java7的JVM上运行,结果就报错了,世界就是这么一个草台班子。这么看来,如果要杜绝这种问题,只能根据目标Java版本选择对应的JDK了。这的确是一个方案,如果是Mac用户,可以参考这篇文章,除此之外,还有没有更简单的办法呢?
答案是:使用(Java9新增的)release
选项可以更好的编译生成class文件。
实验一:使用ConcurrentHashMap的keySet()方法,输出Java7的class
由于是Java9新增的,所以切换到JDK17,该版本是最后几个可以输出Java7的JDK了,如果你使用JDK21,就无法输出major version为51
的class了。
% openjdk17
% javac --version
javac 17.0.12
% javac -version
javac 17.0.12
% javac --release 7 T.java
警告: [options] 源值7已过时, 将在未来所有发行版中删除
警告: [options] 目标值7已过时, 将在未来所有发行版中删除
警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。
注: T.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
3 个警告
% java T
[A]
在通过javap
观察一下main(String[] args)
方法中的指令。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #7 // class java/util/concurrent/ConcurrentHashMap
3: dup
4: invokespecial #9 // Method java/util/concurrent/ConcurrentHashMap."<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String A
11: ldc #12 // String B
13: invokevirtual #14 // Method java/util/concurrent/ConcurrentHashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
16: pop
17: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_1
21: invokevirtual #24 // Method java/util/concurrent/ConcurrentHashMap.keySet:()Ljava/util/Set;
24: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
27: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 17
line 6: 27
可以看到,第21行指令,调用ConcurrentHashMap
的keySet()
方法,返回的是java.util.Set
接口,这样该class一定可以运行在Java7上。从这个实验可以看出release
相比较source
和target
的组合而言,能够做到更好的向前兼容,事实上release
确实修复了不少Java编译器的bug,也是被用作解放source
和target
的。
实验二:ByteBuffer的flip方法,输出Java8的class
在Java的nio中,ByteBuffer
是Buffer
的子类,其中Buffer
具有flip()
方法,在Java8中,它是这样定义的:public final Buffer flip()
。这就使得ByteBuffer
也继承了flip()
方法,调用后会返回自己的超类Buffer
。这一切在Java9中有所改变,首先超类Buffer
的flip()
方法没有了final
修饰,子类ByteBuffer
扩展了它。
Buffer
中的定义:public Buffer flip()
,再看ByteBuffer
的扩展代码。
public ByteBuffer flip() {
super.flip();
return this;
}
这样改动的目的是为了ByteBuffer
在调用flip()
方法后返回ByteBuffer
类型,这样外部就不需要再次转型,根本上讲就是早期Buffer
类型没有设计好。接下来看一下这段代码:
import java.nio.ByteBuffer;
public class B {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.allocate(16);
bb.flip();
}
}
flip()
方法被调用,这就需要看B的class文件中对flip()
方法的链接是否正确,如果能够在Java8下运行,那就需要使用Buffer
的flip()
方法,但是如果稍有不慎,喜欢虚方法的Java就会将其链接到ByteBuffer
的flip()
方法上,这就会导致出现问题。
切换到JDK21,然后使用source
和target
选项编译生成一个Java8的class,然后再切换回Java8去运行该class,结果如何?
% openjdk21
% javac --version
javac 21.0.4
% javac -source 8 -target 8 B.java
警告: [options] 未与 -source 8 一起设置引导类路径
警告: [options] 源值 8 已过时,将在未来发行版中删除
警告: [options] 目标值 8 已过时,将在未来发行版中删除
警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。
4 个警告
% openjdk8
% java B
Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
at B.main(B.java:7)
可以看到结果是找不到方法flip()
,它需要返回ByteBuffer
类型的flip()
方法,但是Java8中ByteBuffer
实际没有该方法,它只有一个返回超类Buffer
的flip()
方法。虽然使用source
和target
选项,要求编译生成的class文件能够运行在Java8上,但是JDK编译器还是蠢蠢的将Java9中的改动输出到自己以为能够运行在Java8上的class中。
这就是一个Bug,一个JDK编译器的Bug。是不是Oracle或者社区修复它就好了?估计他们想着如果修复了这个问题,可能会导致问题,所以干脆不要改了,做一个新的,也就是release选项,用它来搞定。
还是切换到JDK21,然后用release选项编译生成一个Java8的class,重新做一下测试看看。
% openjdk21
% javac --version
javac 21.0.4
% javac --release 8 B.java
警告: [options] 源值 8 已过时,将在未来发行版中删除
警告: [options] 目标值 8 已过时,将在未来发行版中删除
警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。
3 个警告
% openjdk8
% java -version
openjdk version "1.8.0_422"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_422-b05)
OpenJDK 64-Bit Server VM (Temurin)(build 25.422-b05, mixed mode)
% java B
正常执行并返回,再使用javap
观察一下class文件。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: bipush 16
2: invokestatic #7 // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
5: astore_1
6: aload_1
7: invokevirtual #13 // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer;
10: pop
11: return
LineNumberTable:
line 6: 0
line 7: 6
line 8: 11
其中第7行指令,调用的方法就是超类Buffer
的flip()
方法,从字节码层面看,是符合预期的。这样看来在Java9之后,source
和target
选项就应该成为历史了,使用release
选项会更好一些。
在maven中使用release
选项也很简单,修改一下配置即可。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>8</release>
</configuration>
</plugin>
JDK17可以使用release
选项输出Java7的字节码,而JDK21最低只能输出Java8的字节码,能够看出来随着JDK的继续演进,通过release
选项输出的最低字节码版本也在逐渐升高。这种有策略,工业化的语言演进机制,也只有唯一完成模块化改造的主流编程语言Java所具备。不要再固守Java8了,赶快升级吧。