0
点赞
收藏
分享

微信扫一扫

每天学习一点点之再理解 Java 中的常量和相关助记符

以沫的窝 2022-12-22 阅读 110

文章目录

  • ​​编译期常量​​
  • ​​运行时常量​​
  • ​​相关助记符​​
  • ​​附:常量入栈指令​​
  • ​​References​​


关于常量,在《Java 核心技术 卷 I》中是这么定义的:

在 Java 中,利用关键字 ​​final​​ 指示常量。一旦被赋值之后,就不能够再更改了。

在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字 ​​static final​​ 设置一个类常量。

而常量又分为编译期常量和运行时常量。

编译期常量

在 ​​小议 Java 类的初始化​​​ 中也提到过,对于 HotSpot 虚拟机来说,可以通过 ​​-XX:+TraceClassLoading​​ 参数查看有哪些类被加载;根据《Java 虚拟机规范》,验证一个类是否被初始化就是通过查看静态代码块是否执行。

看一个例子:

package dongguabai.demo.testing.classloader;

/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 01:38
*/
public class Main2 {
public static void main(String[] args) {
System.out.println(Person.str);
}
}

class Person{

public static String str = "User_Str";

static {
System.out.println("User static....");
}
}

输出:

[Loaded dongguabai.demo.testing.classloader.Main2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
[Loaded dongguabai.demo.testing.classloader.Person from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]

User static....
User_Str

可以发现,​​Person​​​ 类被加载和初始化了。接下来将 ​​Person​​​ 中的 ​​str​​​ 加上 ​​final​​ 修饰,使之成为一个类常量:

public static final String str = "User_Str";

重新运行输出结果:

[Loaded dongguabai.demo.testing.classloader.Main2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
User_Str

发现 ​​Person​​ 类既没有被加载,也没有被初始化。

实际上,在编译期间,常量会被存入调用常量的方法所在的类的常量池中。在上面的例子中调用类 ​​Main2​​​ 并没有直接引用到定义常量 ​​str​​​ 的 ​​Person​​ 类,所以这里并没有触发 Person 类的加载和初始化,所以编译之后其实 Main2 类和 Person 类没啥关系了。

再看一个例子:

➜  java git:(master) ✗ cd /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java
➜ java git:(master) ✗ javac dongguabai/demo/testing/classloader/Main2.java
➜ java git:(master) ✗ ls dongguabai/demo/testing/classloader
Main.java Main2.class Main2.java Person.class
➜ java git:(master) ✗ rm dongguabai/demo/testing/classloader/Person.class
➜ java git:(master) ✗ java dongguabai/demo/testing/classloader/Main2
User_Str

在这个例子中我使用 ​​javac​​​ 将 ​​Main2.java​​​ 编译后,删除了 ​​Person.class​​​,但是此时 ​​Main2.class​​​ 仍然可以正常运行,下面是 ​​Main2.class​​ 反编译后的结果:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package dongguabai.demo.testing.classloader;

public class Main2 {
public Main2() {
}

public static void main(String[] var0) {
System.out.println("User_Str");
}
}

的确是跟 ​​Person​​ 没有任何关联了。

接下来使用 ​​javap​​​ 查看一下 ​​Main2.class​​ 的字节码指令:

➜  java git:(master) ✗ javap -c dongguabai/demo/testing/classloader/Main2.class
Compiled from "Main2.java"
public class dongguabai.demo.testing.classloader.Main2 {
public dongguabai.demo.testing.classloader.Main2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String User_Str
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

可以看到 ​​Main2.java​​​ 中的 ​​Person.str​​​ 已经被替换成了 ​​User_Str​​​,​​ldc​​ 是 Java 中操作常量的指令助记符。

运行时常量

在《深入理解 Java 虚拟机》中关于触发类的初始化的行为有这么一段描述:

遇到 ​​new​​​、​​getstatic​​​、​​putstatic​​​ 或 ​​invokeStatic​​ 这 4 条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有:

  • 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候。(补充:分别会触发 ​​getstatic​​​ 和 ​​putstatic​​ )。

也就是说在编译器就能够确定结果的会直接放入常量池,不会触发常量所在类的初始化。再看一个例子:

package dongguabai.demo.testing.classloader;

import java.util.Date;

/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 10:36
*/
public class Main4 {
public static void main(String[] args) {
System.out.println(Person2.date);
}
}

class Person2{

public static final Date date = new Date();

static {
System.out.println("User2 static....");
}
}

运行结果:

[Loaded dongguabai.demo.testing.classloader.Main4 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
[Loaded dongguabai.demo.testing.classloader.Person2 from file:/Users/dongguabai/IdeaProjects/thinkingTest/testing/target/classes/]
User2 static....
Sun Nov 22 10:49:38 CST 2020

这时候发现 ​​Person2​​​ 被加载也被初始化了,因为此时的常量 ​​date​​​ 是无法在编译期确定的。此时在 ​​Main4​​​ 中调用 ​​Person2.date​​​ 就相当于是主动使用了 ​​Person2​​​,会触发 ​​Person2​​ 的初始化。

相关助记符

在 ​​com.sun.org.apache.bcel.internal.generic​​ 包下有很字节码相关的类。

/**
* LDC - Push item from constant pool.
*
* <PRE>Stack: ... -> ..., item</PRE>
*
* @author <A HREF="mailto:markus.dahm@berlin.de">M. Dahm</A>
*/
public class LDC extends CPInstruction
implements PushInstruction, ExceptionThrower, TypedInstruction {
/**
* Empty constructor needed for the Class.newInstance() statement in
* Instruction.readInstruction(). Not to be used otherwise.
*/
LDC() {}

public LDC(int index) {
super(com.sun.org.apache.bcel.internal.Constants.LDC_W, index);
setSize();
}

// Adjust to proper size
protected final void setSize() {
if(index <= com.sun.org.apache.bcel.internal.Constants.MAX_BYTE) { // Fits in one byte?
opcode = com.sun.org.apache.bcel.internal.Constants.LDC;
length = 2;
} else {
opcode = com.sun.org.apache.bcel.internal.Constants.LDC_W;
length = 3;
}
}
...
...
}

这里的 ​​index​​​ 来自父类 ​​CPInstruction​​:

protected int index; // index to constant pool

通过 ​​com.sun.org.apache.bcel.internal.generic.LDC#setSize​​​ 方法可以大概猜出 ​​ldc​​​ 与 ​​ldc_w​​ 的区别,这里测试一下:

package dongguabai.demo.testing.classloader;

/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 03:21
*/
public class Main3 {
public static void main(String[] args) {
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
...
...
System.out.println("253");
System.out.println("254");
System.out.println("255");
}
}

​javap​​ 查看:

➜  java git:(master) ✗ javap -c -verbose dongguabai/demo/testing/classloader/Main3.class
Classfile /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java/dongguabai/demo/testing/classloader/Main3.class
Last modified 2020-11-22; size 5692 bytes
MD5 checksum 414b5be640030c3f3b05118b4fb376ad
Compiled from "Main3.java"
public class dongguabai.demo.testing.classloader.Main3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #261.#270 // java/lang/Object."<init>":()V
#2 = Fieldref #271.#272 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #273 // 0
#4 = Methodref #274.#275 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #276 // 1
#6 = String #277 // 2
...
#382 = Utf8 107
#383 = Utf8 108
...
#526 = Utf8 251
#527 = Utf8 252
#528 = Utf8 253
#529 = Utf8 254
#530 = Utf8 255
#531 = Utf8 dongguabai/demo/testing/classloader/Main3
#532 = Utf8 java/lang/Object
#533 = Utf8 java/lang/System
#534 = Utf8 out
#535 = Utf8 Ljava/io/PrintStream;
#536 = Utf8 java/io/PrintStream
#537 = Utf8 println
#538 = Utf8 (Ljava/lang/String;)V
{
public dongguabai.demo.testing.classloader.Main3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 0
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String 1
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #6 // String 2
...
2000: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2003: ldc #254 // String 250
2005: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2008: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2011: ldc #255 // String 251
2013: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2016: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2019: ldc_w #256 // String 252
...
2031: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2034: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2037: ldc_w #258 // String 254
2040: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2043: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
2046: ldc_w #259 // String 255
2049: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
2052: return
LineNumberTable:
...
}
SourceFile: "Main3.java"

可以看到 ​​index​​​ 大于 255 的常量使用的是 ​​ldc_w​​。

同样地,还有比如 ​​ICONST​​:

public ICONST(int i) {
super(com.sun.org.apache.bcel.internal.Constants.ICONST_0, (short)1);

if((i >= -1) && (i <= 5))
opcode = (short)(com.sun.org.apache.bcel.internal.Constants.ICONST_0 + i); // Even works for i == -1
else
throw new ClassGenException("ICONST can be used only for value between -1 and 5: " +
i);
value = i;
}

看一个例子:

package dongguabai.demo.testing.classloader;

/**
* @author Dongguabai
* @Description
* @Date 创建于 2020-11-22 15:32
*/
public class Main5 {

public static void main(String[] args) {
System.out.println(0);
System.out.println(10);
}
}

​javap​​ 查看:

➜  java git:(master) ✗ javap -c -verbose dongguabai/demo/testing/classloader/Main5.class
Classfile /Users/dongguabai/IdeaProjects/thinkingTest/testing/src/main/java/dongguabai/demo/testing/classloader/Main5.class
Last modified 2020-11-22; size 428 bytes
MD5 checksum e5ab95769f8d7d9f91305d67c0434419
Compiled from "Main5.java"
public class dongguabai.demo.testing.classloader.Main5
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // dongguabai/demo/testing/classloader/Main5
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Main5.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 dongguabai/demo/testing/classloader/Main5
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public dongguabai.demo.testing.classloader.Main5();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_0
4: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: bipush 10
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 11: 0
line 12: 7
line 13: 15
}
SourceFile: "Main5.java"

可以看到 0 使用的是 ​​iconst_0​​​,而 10 使用的是 ​​bipush​​。

附:常量入栈指令


指令码

操作码(助记符)

操作数

描述(栈指操作数栈)

0x01

aconst_null

null值入栈。

0x02

iconst_m1

-1(int)值入栈。

0x03

iconst_0

0(int)值入栈。

0x04

iconst_1

1(int)值入栈。

0x05

iconst_2

2(int)值入栈。

0x06

iconst_3

3(int)值入栈。

0x07

iconst_4

4(int)值入栈。

0x08

iconst_5

5(int)值入栈。

0x09

lconst_0

0(long)值入栈。

0x0a

lconst_1

1(long)值入栈。

0x0b

fconst_0

0(float)值入栈。

0x0c

fconst_1

1(float)值入栈。

0x0d

fconst_2

2(float)值入栈。

0x0e

dconst_0

0(double)值入栈。

0x0f

dconst_1

1(double)值入栈。

0x10

bipush

valuebyte

valuebyte值带符号扩展成int值入栈。

0x11

sipush

valuebyte1valuebyte2

(valuebyte1 << 8) | valuebyte2 值带符号扩展成int值入栈。

0x12

ldc

indexbyte1

常量池中的常量值(int, float, string reference, object reference)入栈。

0x13

ldc_w

indexbyte1indexbyte2

常量池中常量(int, float, string reference, object reference)入栈。

0x14

ldc2_w

indexbyte1indexbyte2

常量池中常量(long, double)入栈。

References

  • 《The Java Virtual Machine Specification Java SE 8 Edition》
  • 《Java 核心技术 卷 I》
  • ​​https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.ldc_w​​
  • 《深入理解 Java 虚拟机》

​​

每天学习一点点之再理解 Java 中的常量和相关助记符_System


举报

相关推荐

0 条评论