深入理解String字符串
相信大家在工作中,面试中,聊天中一直都会遇到一个很争论的问题,就是String的一堆知识,这里我就通过一些例子去全面的解析一遍,让自己也让同行更全面的去知其所以然.如有错误,欢迎指正!
举个栗子
java源代码
public class Demo{
public static void main(String[] args){
String a = "1";
String b = "11";
String c = "111";
String d = a+b;
System.out.println(c == d);
String e = new String("111");
System.out.println(c == e);
String f = new String("111").intern();
System.out.println(f == c);
}
}
输出结果
false
false
true
继续往下看,别问为什么!
执行javac Demo.java去编译源代码生成Demo.class
重点来了,把编译后的Demo.class拿过来用javap -v -c -s -l Demo.class执行一下,你可以看到这个东西:
Classfile /E: /java/src/Demo.class
Last modified 2019-4-3; size 978 bytes
MD5 checksum 83f619899d9e19c6b48661d72c802f5d
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #15.#28 // java/lang/Object."<init>":()V
#2 = String #29 // 1
#3 = String #30 // 11
#4 = String #31 // 111
#5 = Class #32 // java/lang/StringBuilder
#6 = Methodref #5.#28 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#33 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#34 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #37.#38 // java/io/PrintStream.println:(Z)V
#11 = Class #39 // java/lang/String
#12 = Methodref #11.#40 // java/lang/String."<init>":(Ljava/lang/String;)V
#13 = Methodref #11.#41 // java/lang/String.intern:()Ljava/lang/String;
#14 = Class #42 // Demo
#15 = Class #43 // java/lang/Object
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 StackMapTable
#23 = Class #44 // "[Ljava/lang/String;"
#24 = Class #39 // java/lang/String
#25 = Class #45 // java/io/PrintStream
#26 = Utf8 SourceFile
#27 = Utf8 Demo.java
#28 = NameAndType #16:#17 // "<init>":()V
#29 = Utf8 1
#30 = Utf8 11
#31 = Utf8 111
#32 = Utf8 java/lang/StringBuilder
#33 = NameAndType #46:#47 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#34 = NameAndType #48:#49 // toString:()Ljava/lang/String;
#35 = Class #50 // java/lang/System
#36 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#37 = Class #45 // java/io/PrintStream
#38 = NameAndType #53:#54 // println:(Z)V
#39 = Utf8 java/lang/String
#40 = NameAndType #16:#55 // "<init>":(Ljava/lang/String;)V
#41 = NameAndType #56:#49 // intern:()Ljava/lang/String;
#42 = Utf8 Demo
#43 = Utf8 java/lang/Object
#44 = Utf8 [Ljava/lang/String;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 append
#47 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = Utf8 toString
#49 = Utf8 ()Ljava/lang/String;
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 println
#54 = Utf8 (Z)V
#55 = Utf8 (Ljava/lang/String;)V
#56 = Utf8 intern
{
public Demo();
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 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=7, args_size=1
0: ldc #2 // String 1
2: astore_1
3: ldc #3 // String 11
5: astore_2
6: ldc #4 // String 111
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_3
33: aload 4
35: if_acmpne 42
38: iconst_1
39: goto 43
42: iconst_0
43: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
46: new #11 // class java/lang/String
49: dup
50: ldc #4 // String 111
52: invokespecial #12 // Method java/lang/String."<init>":(Ljava/lang/String;)V
55: astore 5
57: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
60: aload_3
61: aload 5
63: if_acmpne 70
66: iconst_1
67: goto 71
70: iconst_0
71: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
74: new #11 // class java/lang/String
77: dup
78: ldc #4 // String 111
80: invokespecial #12 // Method java/lang/String."<init>":(Ljava/lang/String;)V
83: invokevirtual #13 // Method java/lang/String.intern:()Ljava/lang/String;
86: astore 6
88: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
91: aload 6
93: aload_3
94: if_acmpne 101
97: iconst_1
98: goto 102
101: iconst_0
102: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
105: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
line 7: 29
line 9: 46
line 10: 57
line 12: 74
line 13: 88
line 15: 105
StackMapTable: number_of_entries = 6
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class "[Ljava/lang/String;", class java/lang/String, 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 "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 255 /* full_frame */
offset_delta = 26
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, 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 "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 255 /* full_frame */
offset_delta = 29
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, 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 "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo.java"
这个就是class文件的字节码,一直被我们认为很深奥的东西,其实也不是多么难理解,就是需要记住的东西有点多而已,看得多了其实也就掌握了(其实也是很难的).ok,栗子就到这了.
解析一下
栗子我们举完了,那我们就去分析一下,下边的内容涉及到了JVM,希望你有点JVM的基础再来看,大家都知道,JVM的体系结构其实是很复杂的,大体的结构描述的也很简单,比如堆栈方法区什么的这些大类别,但是方法区里边的内容,还有其他结构里边的内容可能就不太了解了.扯个蛋,我这里也不说这些玩意,我这里要说的是常量池
上边那个结果false,false,true到底是怎么来的?咱们徐徐渐进的来,结合字节码一步一步的分析,安全带请系好,我要开车了!
代码咱就不说了,既然要研究,那咱们就研究字节码,研究研究JVM到底是怎么去运算这些代码的.
1.创建一个类
从字节码可以看出来,前边的内容大致就是类的信息,比如:
Classfile /E:/java/src/Demo.class //类的位置
Last modified 2019-4-3; size 978 bytes //类最后修改的时间和大小
MD5 checksum 83f619899d9e19c6b48661d72c802f5d //MD5效验码
Compiled from "Demo.java" //名称
public class Demo //类声明
minor version: 0 //次版本号
major version: 52 //主版本号
flags: ACC_PUBLIC, ACC_SUPER //类似于权限的东西,是否为Public类型,是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真
2.常量池
从#1到#56都是,太多了就不一一解析了,给你们一个表格可以自己去一个一个分析,其实就是类常量池加载了那些东西而已.
常量池中数据项类型 | 类型标志 | 类型描述 |
---|---|---|
Utf8 | 1 | UTF-8编码的Unicode字符串 |
Integer | 3 | int类型字面值 |
Float | 4 | float类型字面值 |
Long | 5 | long类型字面值 |
Double | 6 | double类型字面值 |
Class | 7 | 对一个类或接口的符号引用 |
String | 8 | String类型字面值 |
Fieldref | 9 | 对一个字段的符号引用 |
Methodref | 10 | 对一个类中声明的方法的符号引用 |
InterfaceMethodref | 11 | 对一个接口中声明的方法的符号引用 |
NameAndType | 12 | 对一个字段或方法的部分符号引用 |
分析以后咱们就知道,在类被加载的时候其实a,b,c三个字符串已经被加载到常量池中了,这个常量池是类常量池,而不是运行时常量池,不理解没事,下面咱慢慢来.
3.类文件执行过程
具体的执行之类这里不多说,内容太多,可以参考我另一篇class指令的博文去对照着看一下.
咱们来分析.
System.out.println(c == d); //为什么是false?
字节码是
0: ldc #2
//把常量池中的String 1压入栈
2: astore_1
//将引用类型或returnAddress类型值存入局部变量1
3: ldc #3
// 把常量池中的String 11压入栈
5: astore_2
6: ldc #4
// String 111
8: astore_3
9: new #5
// class java/lang/StringBuilder new一个Stringbuilder对象
12: dup
//复制栈顶部一个字长内容
13: invokespecial #6
// Method java/lang/StringBuilder."<init>":()V 根据编译时类型来调用实例方法
16: aload_1
//从局部变量1中装载引用类型值
17: invokevirtual #7
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 调用append
20: aload_2
//从局部变量2中装载引用类型值
21: invokevirtual #7
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 调用append
24: invokevirtual #8
// Method java/lang/StringBuilder.toString:()Ljava/lang/String; 调用toString
27: astore 4
//将将引用类型或returnAddress类型值存入局部变量
29: getstatic #9
// Field java/lang/System.out:Ljava/io/PrintStream; 从类中获取静态字段
32: aload_3
//从局部变量3中装载引用类型值
33: aload 4
//从局部变量中装载引用类型值(refernce)
35: if_acmpne 42
//有条件转移
38: iconst_1
//将int类型常量1压入栈
39: goto 43
//无条件跳转
42: iconst_0
//将int类型常量0压入栈
43: invokevirtual #10
// Method java/io/PrintStream.println:(Z)V 比较结果
从上边的方法表我们可以知道,在程序执行时,jvm会把类常量池中的字面量,常量等等加载到运行时常量池里去参与运算,然后再赋予引用,也就是说此时的a,b,c三个常量已经在运行时常量池中了,或许也可以理解为字符串常量池,这几个池子变来变去很是恶心,普及一下:
JDK1.7之前:运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代
JDK1.7的时候:字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区., 也就是hotspot中的永久代
JDK1.8的时候 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
而+这个运算符在java中底层其实就是append,通过字节码我们知道了,字符串的+运算其实就是创建了一个Stringbuildder对象,并调用它的append方法去计算结果的,最后调用toString方法得到结果,然后把引用再给局部变量c的.再看toString的源码:
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
Stringbuilder的toString方法其实就是new 了一个String的对象在堆空间中,它本身是一个String对象而不是常量,所以拿一个堆中的String对象引用和字符串常量池中的常量引用去做对比,结果肯定是false,如果想让他变成true,就必须让这个String对象入常量池,也就是调intern()方法.
现在再去看第二和第三个输出结果,自然也就明白了:
String e = new String("111");
System.out.println(c == e);
String f = new String("111").intern();
System.out.println(f == c);
e和c不相等是因为这个e的引用是一个新的String对象,通过new关键字创建的都是新对象,当然这句话也产生了2个对象,常见的面试题,就不多说了,一个是构造方法构建的,一个是new出来的,存放位置也不同,而f和c相等是因为f的引用进行了入池操作,而字符串常量池有点享元模式的意思,在类被加载的时候111这个字符串常量就已经被加载进去了,所以再入池的时候发现已经存在,就不会再创建新的了.好了,分析到此为止.