前言
String str="1"和new String("1")的区别,其实是个老生常谈的问题,理解起来也挺简单,但是许多人还是很模糊。今天跟新人交流时发现他们对这块的认识也存在偏差,遂在这里系统整理一下String的相关问题。
在叙述问题先,我们要先明白赋值和引用。因为这点对我们理解下边的代码很重要,不然很容易晕。参考这篇文章:https://blog.csdn.net/yz930618/article/details/76278997
问题示例
首先把String涉及到的常见问题用java方法一一列举出来。
package com.ming.test;
public class StringTest {
StringTest() {
}
private StringTest(String ss) {
}
public void test() {
String str="123";
}
public void test1() {
String str=new String("123");
}
public void test2() {
String str="1"+"2"+"3";
}
public void test3() {
String s="1";
String t="2";
String r="3";
String str=s+t+r;
}
public void test4() {
String str=new StringBuffer().append("1").append("2").append("3").toString();
}
public void test5() {
String str=new StringBuilder().append("1").append("2").append("3").toString();
}
public void test6() {
String s1="123";
String s2="123";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
}
public void test7() {
new StringTest("123");
}
public void test8() {
String str="123";
str.intern();
String str1=new String("123");
String str2=new String("123");
}
}
查看java字节码文件
首先通过javac 将java文件编译成.class文件,然后通过命令:
javap -v 字节码.class
查看.class文件内容,对命令不熟的自己百度下,或者通过java -help查看,字节码内容如下如下,大致分为:基本信息、常量池、构造器、方法内容:
public class com.ming.test.StringTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #22.#43 // java/lang/Object."<init>":()V
#2 = String #44 // 123
#3 = Class #45 // java/lang/String
#4 = Methodref #3.#46 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = String #47 // 1
#6 = String #48 // 2
#7 = String #49 // 3
#8 = Class #50 // java/lang/StringBuilder
#9 = Methodref #8.#43 // java/lang/StringBuilder."<init>":()V
#10 = Methodref #8.#51 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #8.#52 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#12 = Class #53 // java/lang/StringBuffer
#13 = Methodref #12.#43 // java/lang/StringBuffer."<init>":()V
#14 = Methodref #12.#54 // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#15 = Methodref #12.#52 // java/lang/StringBuffer.toString:()Ljava/lang/String;
#16 = Fieldref #55.#56 // java/lang/System.out:Ljava/io/PrintStream;
#17 = Methodref #57.#58 // java/io/PrintStream.println:(Z)V
#18 = Methodref #3.#59 // java/lang/String.equals:(Ljava/lang/Object;)Z
#19 = Class #60 // com/ming/test/StringTest
#20 = Methodref #19.#46 // com/ming/test/StringTest."<init>":(Ljava/lang/String;)V
#21 = Methodref #3.#61 // java/lang/String.intern:()Ljava/lang/String;
#22 = Class #62 // java/lang/Object
#23 = Utf8 <init>
#24 = Utf8 ()V
#25 = Utf8 Code
#26 = Utf8 LineNumberTable
#27 = Utf8 (Ljava/lang/String;)V
#28 = Utf8 test
#29 = Utf8 test1
#30 = Utf8 test2
#31 = Utf8 test3
#32 = Utf8 test4
#33 = Utf8 test5
#34 = Utf8 test6
#35 = Utf8 StackMapTable
#36 = Class #60 // com/ming/test/StringTest
#37 = Class #45 // java/lang/String
#38 = Class #63 // java/io/PrintStream
#39 = Utf8 test7
#40 = Utf8 test8
#41 = Utf8 SourceFile
#42 = Utf8 StringTest.java
#43 = NameAndType #23:#24 // "<init>":()V
#44 = Utf8 123
#45 = Utf8 java/lang/String
#46 = NameAndType #23:#27 // "<init>":(Ljava/lang/String;)V
#47 = Utf8 1
#48 = Utf8 2
#49 = Utf8 3
#50 = Utf8 java/lang/StringBuilder
#51 = NameAndType #64:#65 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#52 = NameAndType #66:#67 // toString:()Ljava/lang/String;
#53 = Utf8 java/lang/StringBuffer
#54 = NameAndType #64:#68 // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
#55 = Class #69 // java/lang/System
#56 = NameAndType #70:#71 // out:Ljava/io/PrintStream;
#57 = Class #63 // java/io/PrintStream
#58 = NameAndType #72:#73 // println:(Z)V
#59 = NameAndType #74:#75 // equals:(Ljava/lang/Object;)Z
#60 = Utf8 com/ming/test/StringTest
#61 = NameAndType #76:#67 // intern:()Ljava/lang/String;
#62 = Utf8 java/lang/Object
#63 = Utf8 java/io/PrintStream
#64 = Utf8 append
#65 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#66 = Utf8 toString
#67 = Utf8 ()Ljava/lang/String;
#68 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuffer;
#69 = Utf8 java/lang/System
#70 = Utf8 out
#71 = Utf8 Ljava/io/PrintStream;
#72 = Utf8 println
#73 = Utf8 (Z)V
#74 = Utf8 equals
#75 = Utf8 (Ljava/lang/Object;)Z
#76 = Utf8 intern
{
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 123
2: astore_1
3: return
LineNumberTable:
line 11: 0
line 12: 3
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #3 // class java/lang/String
3: dup
4: ldc #2 // String 123
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
LineNumberTable:
line 14: 0
line 15: 10
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 123
2: astore_1
3: return
LineNumberTable:
line 17: 0
line 18: 3
public void test3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: ldc #5 // String 1
2: astore_1
3: ldc #6 // String 2
5: astore_2
6: ldc #7 // String 3
8: astore_3
9: new #8 // class java/lang/StringBuilder
12: dup
13: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_3
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore 4
33: return
LineNumberTable:
line 20: 0
line 21: 3
line 22: 6
line 23: 9
line 24: 33
public void test4();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #12 // class java/lang/StringBuffer
3: dup
4: invokespecial #13 // Method java/lang/StringBuffer."<init>":()V
7: ldc #5 // String 1
9: invokevirtual #14 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
12: ldc #6 // String 2
14: invokevirtual #14 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
17: ldc #7 // String 3
19: invokevirtual #14 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
22: invokevirtual #15 // Method java/lang/StringBuffer.toString:()Ljava/lang/String;
25: astore_1
26: return
LineNumberTable:
line 26: 0
line 27: 26
public void test5();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #8 // class java/lang/StringBuilder
3: dup
4: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
7: ldc #5 // String 1
9: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: ldc #6 // String 2
14: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #7 // String 3
19: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_1
26: return
LineNumberTable:
line 29: 0
line 30: 26
public void test6();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String 123
2: astore_1
3: ldc #2 // String 123
5: astore_2
6: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual #17 // Method java/io/PrintStream.println:(Z)V
22: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
25: aload_1
26: aload_2
27: invokevirtual #18 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
30: invokevirtual #17 // Method java/io/PrintStream.println:(Z)V
33: return
LineNumberTable:
line 32: 0
line 33: 3
line 34: 6
line 35: 22
line 36: 33
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class com/ming/test/StringTest, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class com/ming/test/StringTest, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
public void test7();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: new #19 // class com/ming/test/StringTest
3: dup
4: ldc #2 // String 123
6: invokespecial #20 // Method "<init>":(Ljava/lang/String;)V
9: pop
10: return
LineNumberTable:
line 38: 0
line 39: 10
public void test8();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: ldc #2 // String 123
2: astore_1
3: aload_1
4: invokevirtual #21 // Method java/lang/String.intern:()Ljava/lang/String;
7: pop
8: new #3 // class java/lang/String
11: dup
12: ldc #2 // String 123
14: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
17: astore_2
18: new #3 // class java/lang/String
21: dup
22: ldc #2 // String 123
24: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
27: astore_3
28: return
LineNumberTable:
line 41: 0
line 42: 3
line 43: 8
line 44: 18
line 45: 28
}
对于上边的内容大家可能一脸懵逼。其实这些都是jvm的指令,网上一搜一大把,我随便搜了一篇文章的。具体的指令集可以参看这篇文章传送门。那么我把上边需要用到的指令贴一下。
- ldc:代表将int、float或String型常量值从常量池中推送至栈顶。
-
astore:代表将栈顶数值(objectref)存入当前 frame的局部变量数组中指定下标(index)处的变量中,栈顶数值出栈。
-
new:代表创建一个对象,并且其引用进栈;
-
dup:代表复制栈顶数值,并且复制值进栈;
-
invokespecial:代表调用超类构造方法、实例初始化方法、私有方法;
-
aload:代表当前frame的局部变量数组中下标为index的引用型局部变量进栈;
常量池就是存放一些类基本信息和类中的常量,代码中会调用常量池的内容,我们要理解方法是如何运作的,利用一个栈和一个本地变量区,来做运算的,具体分析见下边代码。
完成以上概念的介绍后,我们就解读下字节码的内容,首先看下test方法做了什么
//原方法
public void test() {
String str="123";
}
//对应的jvm指令
public void test();
Code:
//将常量池中的123(#2代表去常量池中找#2的常量)推至栈顶
0: ldc #2 // String 123
//将栈顶数值(123)存入当前frame的局部变量数组中指定下标(1)处的变量中,栈顶数值出栈
2: astore_1
//当前方法返回void
3: return
其实上边的操作就是把常量池中的123地址赋值给当前方法栈中的临时变量str。至于常量池中的123,是在类加载的时候就已经创建了。系统会先看下常量池中是否有123这个字符串,没有的话就会新增一个123字符串,有的话就不会创建了。
看下test1方法做了什么
//原方法
public void test1() {
String str=new String("123");
}
//jvm指令
public void test1();
Code:
//在堆中创建一个对象,并将其堆地址存入栈顶
0: new #3 // class java/lang/String
//复制栈顶堆地址,并且复制值进栈,就是说当前栈中有2个一样的堆地址值
3: dup
//将String型常量值123从常量池中推送至栈顶
4: ldc #2 // String 123
//调用超类构造方法、实例初始化方法、私有方法,并取栈中一个堆地址作为该对象的this
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
//将栈顶数值(123)存入当前frame的局部变量数组中指定下标(1)处的变量中,栈顶数值出栈
9: astore_1
10: return
概括一下就是:new String("123")不管常量池中有没有123这个字符池,系统都会在堆内存中创建一个String对象,并且String对象的成员变量char[] value指向"123"常量池对象的char[] value(参考String构造方法),这也是为什么new String("123")这个对象的值是123,常量池中如果没有123字符串的话还会在常量池中新增123字符串常量。
这里要明确一点test1方法中str的值是123,而非string对象的堆地址。
由test方法和test1方法,我们可以知道,使用new String()创建字符串会比常规的写法多new一个对象。
test2方法的字节码内容和test方法的字节码内容是一样的。
由此可知String str="1"+"2"+"3";和String str="123";效果是一样的。
我们接着看下test3方法的字节码内容会发现String str=s+t+r;相当于系统给我们创建一个StringBuilder对象调用了三次append方法。和test5的方法是一样的。相比之下test3多创建了1、2、3三个字符串常量在常量池中。
我们接着看下test4方法会发现其实跟test5方法是一样的,只不过test4创建的是StringBuffer,test5创建的是StringBuilder,我们知道StringBuffer是线程安全的,所以增加了线程安全的处理,常规下会比StringBuilder慢一点。如果多线程操作的话,使用StringBuffer。
String的intern方法
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
intern是个native方法。我们可以看下方法的注释,大致意思就是返回该字符串常量池中的地址,如果常量池中没有这个字符串就会新增一个到常量池中。
举个例子,String str=new String("123"),通过上边分析我们知道,执行完这句话,常量池中会新增123的常量,java堆中会新增一个对象,值为“123”.那么str.intern()就会返回字符串“123”在常量池中的地址。而不会返回堆中的地址。
看到这不知道大家对于这块有没有一个新的认识。看下下边这个代码输出的结果是什么?
String str1=new String("123");
String str2=new String("123");
System.out.println(str1==str2);
答案是false,我们知道==是判断两个对象的地址是否相等。
上边我们说过new String("123")返回的地址是java堆中的对象地址。而且每次调用这句话都会在堆中新创建一个对象并初始化值“123”.虽然str1和str2的代码是一样的,但是是创建了2个不同的对象,所以它两的地址是不一样的。
但是如果str1.intern()和str2.intern()的地址是一样的。因为他们的值是一样的,都会返回字符串常量池中“123”的地址。
equals和==
equals方法是object对象的方法,如果类没有重写这个方法的话都是默认调用的object的equals方法。默认的equals方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
说明默认的equals方法和==是没有区别的。
但String类是重写了equals方法的。方法如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
首先会判断是否地址相等,如果地址相等的话,直接返回true,如果地址不相等的话,在判断值是否相等,如果值相等的话返回true。概括一下就是String类的equals方法是判断值相等。所以当我们用字符串判断相等时,要考虑是什么用途,再考虑用哪种方法。
String str1=new String("123");
String str2=new String("123");
System.out.println(str1==str2);
通过上边的介绍,我们不难猜想出,str1.equals(str2) 是true,因为他们值相等。都是“123”。但他们地址是不相等的。
以上就是对字符串相关的小知识点总结。