String与StringBuilder与StringBuffer
目录
1. 深究字符串String类
1.1 重载运算符 “+” 与 StringBuilder 的冤缘
public class T {
public static void main(String[] args) {
String a = "-";
String b = "123" + a + "456" + a + 789;
System.out.println(b);
}
}
// 使用JDK自带javap命令生成字节码 javap -c T.class
public class T {
public T();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String -
2: astore_1
3: new #3 // class java/lang/StringBuilder -- 编译器自动引用java.lang.StringBuilder类, 并实例化了一个StringBuilder对象
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String 123 -- 每个运算符 "+" 调用一次 append 方法
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String 456
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_1
25: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: sipush 789
31: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; -- 最后调用 toString 方法, 返回String
37: astore_2
38: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
41: aload_2
42: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: return
}
// 再看看循环使用运算符 "+"
public class T {
public static void main(String[] args) {
String a = "";
for(int i=0; i<10; i++) {
a += i;
}
System.out.println(a);
}
}
public class T {
public T();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2 -- 循环开始..
6: bipush 10
8: if_icmpge 36
11: new #3 // class java/lang/StringBuilder -- 每次循环都会实例化一个StringBuilder对象
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: astore_1
30: iinc 2, 1
33: goto 5 -- 跳转到第5行..
36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
39: aload_1
40: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: return
}
1.2. String不可变性
//String
public final class String
{
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
public String(String original) {
// 把原字符串original切分成字符数组并赋给value[];
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
如上代码所示,可以观察到以下设计细节
- String类被final修饰,不可继承
- String内部所有成员变量都设为私有
- 不存在value的setter方法
- value设为final
- 当传入可变数组value[]时,使用Arrays.copayOf复制给内部变量
- 获取value时,不是直接返回对象引用,而是返回对象的拷贝
1.3. String不可变性的优缺点
优点
- 字符串常量池,避免每次都重新创建相同的对象、节省存储空间。
- 线程安全,同一个字符串对象可以背多个线程共享。
- 类加载器使用到字符串,不可变性提供了安全性,以便正确的类被加载。
- 支持hash隐射和缓存,字符串的hashcode在它创建的时候就被缓存了,不需要重新计算,这使得字符串很适合作为Map中的键。
缺点
- 如果对String对象的值进行改变,那么会创建大量的String对象
1.4. String对象是否真的不可变
虽然String对象将value设置为final,并且还通过各种机制保证其成员变量的不可变,但是可以通过反射机制的手段改变其值。
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
2. StringBuffer与StringBuilder的线程安全性问题
StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字
synchronized
修饰了,而StringBuilder没有。
3. String和StringBuffer的效率问题
// 测试代码位置1
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
// 测试代码位置2
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - beginTime);
3.1. String常量与String变量的”+”操作比较
测试①
测试代码位置1 String str = “”;
测试代码位置2 str = “Hello” + “World”;
耗时: 0ms
测试②
测试代码位置1 String str = “”, s1 = “Hello”, s2 = “World”;
测试代码位置2 str = s1 + s2;
耗时: 15ms
原因:
测试①的”Hello” + “World”在编译阶段就已经连接起来,形成了一个字符串常量”HelloWorld”,并指向堆中的拘留字符串对象。
运行时只需要将”HelloWorld”指向的拘留字符串对象地址取出1W次,存放在局部变量str中。这确实不需要什么时间。
测试②中局部变量s1和s2存放的是两个不同的拘留字符串对象的地址。然后会通过下面三个步骤完成“+连接”:
1、StringBuilder temp=new StringBuilder(s1),
2、temp.append(s2);
3、str=temp.toString();
我们发现,虽然在中间的时候也用到了append()方法,但是在开始和结束的时候分别创建了StringBuilder和String对象。
可想而知:调用1W次,是不是就创建了1W次这两种对象呢?不划算。
3.2. String对象的”累+”连接操作与StringBuffer对象的append()累和连接操作比较
测试①
测试代码位置1 String str = “Hello”, s = “”;
测试代码位置2 s = s + str;
耗时: 1371ms
测试②
测试代码位置1 String str = “Hello”; StringBuilder s = new StringBuffer();
测试代码位置2 s.append(str);
耗时: 0ms
原因:
测试① 中的s=s+str,JVM会利用首先创建一个StringBuilder,并利用append方法完成s和str所指向的字符串对象值的合并操作,
接着调用StringBuilder的toString()方法在堆中创建一个新的String对象,其值为刚才字符串的合并结果。而局部变量s指向了新创建的String对象。
因为String对象中的value[]是不能改变的,每一次合并后字符串值都需要创建一个新的String对象来存放。
循环1W次自然需要创建1W个String对象和1W个StringBuilder对象,效率低就可想而知了。
测试②中s.append(str);只需要将自己的value[]数组不停的扩大来存放str即可。循环过程中无需在堆中创建任何新的对象。效率高就不足为奇了。