查看原文
其他

运行时常量池的一道面试题(JDK8环境)

点击关注 👉 Java技术图谱 2021-09-05
点击关注下方公众号,编程资料 都在这里
作者:诗水人间
blog.csdn.net/qq_41813208/article/details/109192324

事先说明环境 在jdk8下,高版本的jdk可能找不到对应的Version类
代码:
public class TestDemo { @Test public void test01() { // String str1 = new StringBuilder("hello").append("World").toString(); System.out.println(str1.intern()); System.out.println(str1 == str1.intern());
String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern()); System.out.println(str2 == str2.intern());
String str3 = new StringBuilder("hello").toString(); System.out.println(str3.intern()); System.out.println(str3 == str3.intern()); } }

请写出控制台中打印的结果?


答案:
helloWorldtruejavafalsehellofalse

你答对了吗?

为什么会出现这种答案呢?

== 运算,对于非基本类型比较的是内存地址值,要知道这道题的输出的结果,最简单的方法就是知道对象:str1、str1.intern()、str2、str2.intern()、str3、str3.intern()的内存地址值就能判断输出是 true还是false。

但这是jvm极力要遏制的,因此我是无法得知内存地址的,当然如果通过其它方式是可以做到的,但是jvm是没有这个api的,下面我们通过hashCode侧面排除一些情况,如果hash值不同,肯定不是同一个对象,hash值相同可能不是同一个对象。

hash值相同不能确认是同一个对象,但是能得知不同则一定不是同一个对象。也就是下面"java"。str1和str2是两个不同的对象,str1来自堆的Eden区中,而str2则是Old区的字符串常量池中。
public class GCTest {
public static void main(String[] args) {
String str1 = new StringBuilder("ja").append("va").toString(); String str2 = str1.intern(); System.out.println(str1==str2); System.out.println(System.identityHashCode(str1)); // 通过System提供的方法得到hash值,打印hash值相当于内存地址,hashCode方法不完全等于 System.out.println(System.identityHashCode(str2)); // 打印地址
String str3 = new StringBuilder("hello").append("world").toString(); String str4 = str3.intern(); System.out.println(str3==str4); System.out.println(System.identityHashCode(str3)); System.out.println(System.identityHashCode(str4));
}}

得到下面一组打印输出,同一个对象的hash值肯定是相同的,而下面str1和str2的hash值不同肯定不是同一个对象(注意不能调用String重写的hashCode(),我们要调用Object提供的native修饰的hashCode()或者利用System.identityHashCode()得到hash值(这种情况就相当于是内存地址))
false // str1和str2不是同一个对象460141958 // 堆空间创建的那个对象 ”java“1163157884 // 字符串常量池中的字符串对象true // 说明str3和str4是同一个对象1956725890 // 堆中创建的字符串对象 ”helloworld“1956725890 // 堆中创建的字符串对象 ”helloworld“

先讲一讲前面2行的由来
helloWorldtrue

我们都知道String类有一个intern()方法,它的作用就是将字符串存入常量池中,并且方法执行完后将这个字符串对象返回。

不难理解第一次打印前在常量池中没有helloWorld字符串,因此会将这个对象存入常量池中。然后返回字符串打印了第一个helloWorld字符串。在比较==的 intern()方法返回的是常量池中的字符串对象(也是前面创建的对象,两个对象是同一个对象),所以返回了第一个true

为了证明,我们可以通过javap -v TestDemo.class命令将字节码文件反编译得到如下字节码。

下面请阅读一遍:
字节码中分常量池、方法test01是我们重点关注的地方,首先注意到常量池中已经有了hello、World、ja、va字符串。
原因在于我们代码中使用String str1 = new StringBuilder("hello").append("World").toString();这里面"hello"这类就是常量,接着我们直接读test01方法
public class com.example.demo.test.TestDemo minor version: 0 major version: 58 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #47 // com/example/demo/test/TestDemo super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/lang/StringBuilder #8 = Utf8 java/lang/StringBuilder #9 = String #10 // 常量"hello"字符串 #10 = Utf8 hello #11 = Methodref #7.#12 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V #13 = Utf8 (Ljava/lang/String;)V #14 = String #15 // 常量"World"字符串 #15 = Utf8 World #16 = Methodref #7.#17 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #17 = NameAndType #18:#19 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #18 = Utf8 append #19 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #20 = Methodref #7.#21 // java/lang/StringBuilder.toString:()Ljava/lang/String; #21 = NameAndType #22:#23 // toString:()Ljava/lang/String; #22 = Utf8 toString #23 = Utf8 ()Ljava/lang/String; #24 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream; #25 = Class #27 // java/lang/System #26 = NameAndType #28:#29 // out:Ljava/io/PrintStream; #27 = Utf8 java/lang/System #28 = Utf8 out #29 = Utf8 Ljava/io/PrintStream; #30 = Methodref #31.#32 // java/lang/String.intern:()Ljava/lang/String; #31 = Class #33 // java/lang/String #32 = NameAndType #34:#23 // intern:()Ljava/lang/String; #33 = Utf8 java/lang/String #34 = Utf8 intern #35 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V #36 = Class #38 // java/io/PrintStream #37 = NameAndType #39:#13 // println:(Ljava/lang/String;)V #38 = Utf8 java/io/PrintStream #39 = Utf8 println #40 = Methodref #36.#41 // java/io/PrintStream.println:(Z)V #41 = NameAndType #39:#42 // println:(Z)V #42 = Utf8 (Z)V #43 = String #44 // 常量"ja"字符串 #44 = Utf8 ja #45 = String #46 // 常量"va"字符串 #46 = Utf8 va #47 = Class #48 // com/example/demo/test/TestDemo #48 = Utf8 com/example/demo/test/TestDemo #49 = Utf8 Code #50 = Utf8 LineNumberTable #51 = Utf8 LocalVariableTable #52 = Utf8 this #53 = Utf8 Lcom/example/demo/test/TestDemo; #54 = Utf8 test01 #55 = Utf8 str1 #56 = Utf8 Ljava/lang/String; #57 = Utf8 str2 #58 = Utf8 str3 #59 = Utf8 StackMapTable #60 = Utf8 RuntimeVisibleAnnotations #61 = Utf8 Lorg/junit/Test; #62 = Utf8 SourceFile #63 = Utf8 TestDemo.java{ public com.example.demo.test.TestDemo(); descriptor: ()V flags: (0x0001) 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 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/example/demo/test/TestDemo;
public void test01(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=3, locals=4, args_size=1 0: new #7 // 创建 java/lang/StringBuilder 对象 3: dup 4: ldc #9 // 入栈常量池(#9)中的字符串常量hello 6: invokespecial #11 // 实例初始化将hello传入 9: ldc #14 // 入栈常量池(在#14)中的字符串常量World 11: invokevirtual #16 // StringBuilder.append方法调用传入World 14: invokevirtual #20 // 调用StringBuilder.toString方法 17: astore_1 // 将返回的值的地址引用存入到局部变量1 18: getstatic #24 // 获取打印流 21: aload_1 // 将局部变量1装载成引用类型,也就是"helloWorld" 22: invokevirtual #30 // "helloWorld"调用String.intern:()方法 25: invokevirtual #35 // 调用打印流,打印"helloWorld" 28: getstatic #24 // 获取打印流 31: aload_1 // 加载局部变量1 也就是"helloWorld" 32: aload_1 // 加载局部变量1 "helloWorld" 33: invokevirtual #30 // "helloWorld"调用intern方法 36: if_acmpne 43 // 如果条件满足就转执行43 的iconst_0也就是将0入栈 39: iconst_1 // 将int类型常量值1压入栈 40: goto 44 // 无条件转移到44 43: iconst_0 // 将0入栈 44: invokevirtual #40 // 调用方法传入也就是“ja"(对应常量池的#44) 47: new #7 // 创建StringBuilder对象 50: dup 51: ldc #43 // 加载字符串"ja" 53: invokespecial #11 // 实例化StringBuilder 56: ldc #45 // 加载字符串"va" 58: invokevirtual #16 // 调用append方法 61: invokevirtual #20 // 调用toString方法 64: astore_2 // 将toString的结果存入局部变量2中 65: getstatic #24 // 获取打印流 68: aload_2 // 加载局部变量2的值也就是加载"java" 69: invokevirtual #30 // 调用intern方法 72: invokevirtual #35 // 打印"java" 75: getstatic #24 // 获取打印流 78: aload_2 // 加载局部变量2"java" 79: aload_2 // 加载局部变量2"java" 80: invokevirtual #30 // 调用intern方法 83: if_acmpne 90 // 如果条件成立跳转到90也就是将0入栈 86: iconst_1 // 将常量值1入栈 87: goto 91 // 无条件跳转到91 90: iconst_0 // 将常量值0入栈 91: invokevirtual #40 // 打印 94: new #7 // class java/lang/StringBuilder 97: dup 98: ldc #9 // String hello 100: invokespecial #11 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 103: invokevirtual #20 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 106: astore_3 107: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream; 110: aload_3 111: invokevirtual #30 // Method java/lang/String.intern:()Ljava/lang/String; 114: invokevirtual #35 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 117: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream; 120: aload_3 121: aload_3 122: invokevirtual #30 // Method java/lang/String.intern:()Ljava/lang/String; 125: if_acmpne 132 128: iconst_1 129: goto 133 132: iconst_0 133: invokevirtual #40 // Method java/io/PrintStream.println:(Z)V 136: return LineNumberTable: line 13: 0 line 14: 18 line 15: 28 line 17: 47 line 18: 65 line 19: 75 line 21: 94 line 22: 107 line 23: 117 line 24: 136 LocalVariableTable: Start Length Slot Name Signature 0 137 0 this Lcom/example/demo/test/TestDemo; 18 119 1 str1 Ljava/lang/String; 65 72 2 str2 Ljava/lang/String; 107 30 3 str3 Ljava/lang/String; StackMapTable: number_of_entries = 6 frame_type = 255 /* full_frame */ offset_delta = 43 locals = [ class com/example/demo/test/TestDemo, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class com/example/demo/test/TestDemo, class java/lang/String ] stack = [ class java/io/PrintStream, int ] frame_type = 255 /* full_frame */ offset_delta = 45 locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] frame_type = 255 /* full_frame */ offset_delta = 40 locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class com/example/demo/test/TestDemo, class java/lang/String, class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] RuntimeVisibleAnnotations: 0: #61() org.junit.Test}SourceFile: "TestDemo.java"

继续向下讲解:
javafalse

会发现和上面一样的流程操作,为什么前面会返回true后面返回false。为什么会出现这种情况呢?

通过阅读字节码,会发现"helloWorld"、"java"他们的字节码指令逻辑顺序是一模一样的,为何这里是false呢?

原因在于"java"在类加载机制过程中执行了System类的System.initializeSystemClass()方法,在方法中调用了sun.misc.Version.init();就将"java"已经加载到常量池中了(字节码中的常量池只是class文件常量池不代码jvm环境的所有常量池,而运行时常量池中已经存在"java"字符串),部分截图如下,会发现"java"常量已经被使用了。


故,判断语句变成了,new创建出来的"java"对象,与运行时常量池中的"java"是两个不同的对象,因此返回false。

反过来说明,前面之所以返回true的原因在于new创建好"helloWorld"后,当调用intern方法时,不会重新创建一个新的"helloWorld",而是会将创建好的"helloWorld"存入运行时常量池中,此时intern()实际上只是做了一个地址引用(这个结论应该不正确,因为gc也会导致对象移动,我估摸着是真正的将对象移动到了Old区的字符串常量池)。因此当判断str1 == str1.intern()时,他们的地址是同一个地址,也是同一个对象,因此返回true。
总结:这道题目的前面两问区别在于"java"是在程序运行时就已经在运行时常量了,而其它字符串则没有,因此出现不同的结果,同理类似于"java"这类字符串常量的应该还有一些在某些类中有定义。
有了前面的基础后,后面一问
hellofalse

不难解释,str3是通过new StringBuilder().toString();创建出来的对象,因此是一个全新的"hello"字符串对象,而调用.intern()方法后,返回的则是常量池中的"hello",两者明显不是同一个对象,因此返回false



推荐阅读:



点个在看你最好看

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存