一道比较经典的Java试题,题目如下:
String str = new String("abc");
问以上语句创建了几个对象?答案是“两个”。下面对为什么是两个的问题加以说明
1.常量池
class文件中的常量池包含了与文件中类和接口相关的常量,包括:文字字符串,final变量值,类名和方法名的常量。Java虚拟机把常量池组织为入口列表的形式。每一个常量池的入口都从一个长度位一个字节的标志开始,这个标志指出了列表中该位置的常量类型。每一个标志都有一个相对应的表,字符串类型对应的表明是Constant_String_info,该表用来存储字符串值,该值也可以表示为java.lang.String的实例。该表的格式是:
名称 | 数量 |
tag | 1 |
string_index | 1 |
tag:值为CONSTANT_String(8)
string_index:给出了包含文字字符串的CONSTANT_Utf8_info表的索引。
CONSTANT_Utf8_info表用于存储
a.字符串 b.被定义的类和接口描述 c.对其他类或接口的符号引用 d.与属性相关的字符串
2.常量池解析时对CONSTANT_String_info的解析
解析CONSTANT_String_info,Java虚拟机必须要把一个指向内部字符串对象的引用放置到要被解析的常量池入口数据中去。该String对象必须按照string_index在CONSTANT_String_info中指明的CONSTANT_Utf8_info指定的字符顺序组织。每一个Java虚拟机必须维护一个内部列表,列出了所有在运行时已被intern(拘留)的字符串对象的引用,任何特定的字符序列在这个内部列表内只出现一次。CONSTANT_String_info入口所代表的字符序列,如果已存在与内部列表中,虚拟机会使用以前intern的字符串对象的引用。否则虚拟机按照这个字符序列创建一个新的字符串对象,并把这个对象的引用放入内部列表。然后虚拟机会把该字符串对象的引用放入到CONSTANT_String_info入口数据中去。
3.举例
用一个例子说明上述内容,程序代码如下:
public class TestDemo1 { public static void main(String[] args) { String testString = new String("abc"); String testString1 = "abc"; System.out.println(testString == testString1); //false; System.out.println( testString.intern() == testString1.intern());//true; } }
为了表明是用String str = new String("XXX") 和 String str = "XXX"的不同,程序中是用了分别两种方式。
上述代码的class文件内容如下:
// Compiled from TestDemo1.java (version 1.5 : 49.0, super bit) public class TestDemo1 { // Method descriptor #6 ()V // Stack: 1, Locals: 1 public TestDemo1(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [8] 4 return Line numbers: [pc: 0, line: 2] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: TestDemo1 // Method descriptor #15 ([Ljava/lang/String;)V // Stack: 3, Locals: 3 public static void main(java.lang.String[] args); 0 new java.lang.String [16] 3 dup 4 ldc <String "abc"> [18] 6 invokespecial java.lang.String(java.lang.String) [20] 9 astore_1 [testString] 10 ldc <String "abc"> [18] 12 astore_2 [testString1] 13 getstatic java.lang.System.out : java.io.PrintStream [23] 16 aload_1 [testString] 17 aload_2 [testString1] 18 if_acmpne 25 21 iconst_1 22 goto 26 25 iconst_0 26 invokevirtual java.io.PrintStream.println(boolean) : void [29] 29 getstatic java.lang.System.out : java.io.PrintStream [23] 32 aload_1 [testString] 33 invokevirtual java.lang.String.intern() : java.lang.String [35] 36 aload_2 [testString1] 37 invokevirtual java.lang.String.intern() : java.lang.String [35] 40 if_acmpne 47 43 iconst_1 44 goto 48 47 iconst_0 48 invokevirtual java.io.PrintStream.println(boolean) : void [29] 51 return Line numbers: [pc: 0, line: 5] [pc: 10, line: 6] [pc: 13, line: 8] [pc: 29, line: 9] [pc: 51, line: 10] Local variable table: [pc: 0, pc: 52] local: args index: 0 type: java.lang.String[] [pc: 10, pc: 52] local: testString index: 1 type: java.lang.String [pc: 13, pc: 52] local: testString1 index: 2 type: java.lang.String }
首先,在main方法开始,指令如下:
0 new java.lang.String [16]
以上的一行代码说明要创建一个String类型的对象。
4 ldc <String "abc"> [18]
上面的这句指令就要用到我们上面说的内容了,解析常量池入口是ldc指令执行的一部分,当虚拟机执行到这条指令的时候,会检查常量池入口18,如果发现这是一个未被解析的CONSTANT_String_info入口,因此创建一个新的值为“abc”的字符串对象,并且将其intern,然后在这个常量池入口中放入此字符串对象的引用。在后面同样执行ldc指令时
10 ldc <String "abc"> [18]
这个地方要注意常量池入口索引是一致的([18]),说明对应的内容是一样的,由于前面的ldc指令创建了“abc”,因此在内部列表中可以找到,此处直接将其引用放入常量池入口中。
程序的运行结果
语句
System.out.println(testString == testString1);
运行为false ,说明这个引用所值得不是一个地址。
语句
System.out.println( testString.intern() == testString1.intern());//true;
运行为true,说明testString和testString1的intern的内容是一致的,由于intern()的值具有唯一性,因此说明它们指向是相同的。
为了更进一步的说明,再给出另一段程序:
public class TestDemo2 { public static void main(String[] args) { String str = "abc"; } }
此程序对应的class内容为:
// Compiled from TestDemo2.java (version 1.5 : 49.0, super bit) public class TestDemo2 { // Method descriptor #6 ()V // Stack: 1, Locals: 1 public TestDemo2(); 0 aload_0 [this] 1 invokespecial java.lang.Object() [8] 4 return Line numbers: [pc: 0, line: 2] Local variable table: [pc: 0, pc: 5] local: this index: 0 type: TestDemo2 // Method descriptor #15 ([Ljava/lang/String;)V // Stack: 1, Locals: 2 public static void main(java.lang.String[] args); 0 ldc <String "abc"> [16] 2 astore_1 [str] 3 return Line numbers: [pc: 0, line: 5] [pc: 3, line: 6] Local variable table: [pc: 0, pc: 4] local: args index: 0 type: java.lang.String[] [pc: 3, pc: 4] local: str index: 1 type: java.lang.String }
请注意,同样是创建一个值为“abc”的String对象,这里与第一个例子的不同在于只有
0 ldc <String "abc"> [16]
这么一句指令,其后就是将对象引用赋给str。这与第一个例子的部分有明显的不同
0 new java.lang.String [16] 3 dup 4 ldc <String "abc"> [18] 6 invokespecial java.lang.String(java.lang.String) [20]
由以上得出,在使用String str = new String("XXX")创建了两个对象:1.new创建的String对象;2.由ldc指令创建的String对象。(虚拟机的内部列表,应该就是上面所说的CONSTANT_Utf8_info表)