String它是一个引用数据类型,不是一个基础数据类型。
先思考一个问题:String为什么是不可更改的。
查看String类的签名如下:
public final class String
implements java.io.Serializable, Comparable, CharSequence {}
然后再看看String到底是怎么存储字符串的:
/** The value is used for character storage. */
private final char value[];
String类的签名,和存储String的char数组都被final修饰,它们确保了String对象是永远不会被修改的。
一、内存存储
下面我们继续String之旅吧,先贴一段学习代码。
public class LearnString {
public static void main(String args[]) {
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
}
}
运行结果也如大家所想:
true
false
true
我们先看看String的内存分布情况吧
public class LearnString {
String b = "abc";
public static void main(String args[]) {
String a = "abc";
}
}
编译后,我们通过javap -v LearnString来查看内存的分布情况,可以看到在字节码的Constant pool区有如下一行:
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."":()V
#2 = String #24 // ab
#3 = Fieldref #5.#25 // com/example/learn/LearnString.d:Ljava/lang/String;
#4 = String #26 // abc
可见不管是成员变量还是局部变量,只要String一开始就被赋值了,那么它的值就会被保存在字节码的常量池中。其实你会发现如果把之前的:
String d = "ab" + "d";
重新编译后再javap后如下:
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."":()V
#2 = String #24 // ab
#3 = Fieldref #5.#25 // com/example/learn/LearnString.d:Ljava/lang/String;
#4 = String #26 // abc
这时候我们可以发现结果是一样的,也就是说compiler发现这些"+"操作完全可以在编译阶段优化掉,compiler就会进行一定的优化操作。
接下来我们可以讨论开篇的那个例子了。“abc”字符串按照上面所说,在编译的时候就被保存在字节码的常量池中了,所以a 和 b都是拿的常量池中的“abc”值(指向了同一个堆地址),故a==b为true;c = new String("abc") ,它其实创建了两个对象,一个new出来的另一个就是常量“abc”,c的引用指向了new出来的对象,故a!=c。
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;
}
这是String重写Object的equals方法,由此可以发现equals方法是比较的是两个String对象里头的字符数组(char[]),比较的是堆中值而非堆地址,故a.equals(c)是相等。
二、注意的问题
先看看下面代码
String s = "";
for(int i = 0; !"end".equals(args[i]);){
s+=args[i];
}
同样用javap反编译字节码
Classfile /F:/workspaces/JavaLearn/out/production/JavaLearn/com/example/learn/LearnString.class
Last modified 2017-4-25; size 779 bytes
MD5 checksum 646b7bf02cd674af0bc5e55605ee3713
Compiled from "LearnString.java"
public class com.example.learn.LearnString
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."":()V
#2 = String #31 //
#3 = String #32 // end
#4 = Methodref #33.#34 // java/lang/String.equals:(Ljava/lang/Object;)Z
#5 = Class #35 // java/lang/StringBuilder
#6 = Methodref #5.#30 // java/lang/StringBuilder."":()V
#7 = Methodref #5.#36 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#37 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #38 // com/example/learn/LearnString
#10 = Class #39 // java/lang/Object
#11 = Utf8
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/example/learn/LearnString;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 i
#21 = Utf8 I
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 s
#25 = Utf8 Ljava/lang/String;
#26 = Utf8 StackMapTable
#27 = Class #40 // java/lang/String
#28 = Utf8 SourceFile
#29 = Utf8 LearnString.java
#30 = NameAndType #11:#12 // "":()V
#31 = Utf8
#32 = Utf8 end
#33 = Class #40 // java/lang/String
#34 = NameAndType #41:#42 // equals:(Ljava/lang/Object;)Z
#35 = Utf8 java/lang/StringBuilder
#36 = NameAndType #43:#44 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = NameAndType #45:#46 // toString:()Ljava/lang/String;
#38 = Utf8 com/example/learn/LearnString
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/String
#41 = Utf8 equals
#42 = Utf8 (Ljava/lang/Object;)Z
#43 = Utf8 append
#44 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#45 = Utf8 toString
#46 = Utf8 ()Ljava/lang/String;
{
public com.example.learn.LearnString();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/learn/LearnString;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: ldc #3 // String end
7: aload_0
8: iload_2
9: aaload
10: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
13: ifne 40
16: new #5 // class java/lang/StringBuilder
19: dup
20: invokespecial #6 // Method java/lang/StringBuilder."":()V
23: aload_1
24: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: aload_0
28: iload_2
29: aaload
30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 5
40: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 16
line 12: 40
LocalVariableTable:
Start Length Slot Name Signature
5 35 2 i I
0 41 0 args [Ljava/lang/String;
3 38 1 s Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 5
locals = [ class java/lang/String, int ]
frame_type = 250 /* chop */
offset_delta = 34
}
SourceFile: "LearnString.java"
在执行"+"时候
13: ifne 40
16: new #5 // class java/lang/StringBuilder
19: dup
20: invokespecial #6 // Method java/lang/StringBuilder."":()V
23: aload_1
在这个循环里每一次对String执行"+"操作,都会创建一个StringBuilder对象,可见这多么消耗性能。为了避免这种事情发生只要你在执行循环之前创建一个StringBuilder对象然后将之后的"+"操作换成StringBuilder.append()就可以。