一、String 的设计
String 是一个线程安全的类。我们可以看到在 String 中没有使用任何 “锁”,那么 String 是如何实现线程安全的?
1、String 被 final 修饰,这使得 String 无法被继承,并修改里面的方法
2、String 被设计成不可变对象
什么是不可变对象? 先来说说线程安全,之所以会存在线程安全的问题,是因为存在多个线程对同一个共享的资源对象进行“读写”操作,单纯只是读并不会有问题,问题出在“读写” 同时发生的情况。 而不可变对象的意思是,每一个线程操作的String 对象,对于当前线程来说都是唯一的,一旦有线程对某个 String 进行修改,那么生成会是一个新的 String 对象。
从代码的角度来看可能,来看几个简单的方法,concat
和 replace
public final class String {
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
......
return new String(buf, true);
}
return this;
}
}
这样对当前字符串进行了修改,必定会生成一个新的对象,而不会再此之上进行修改,也就是说对于一个 String 对象,从它创建之后,就不会再进行变更了,所以 初始化之后的String 是一个不可变的对象。
同样的,我们通过简单的代码测试一下,
public static void main(String[] args) {
String a = "a";
System.out.println("a:" + a);
String b = a.concat("b");
System.out.println("b:" + b);
String c = b.replace("a","c");
System.out.println("c:" + c);
// 对比对象
System.out.println("a 和 b是否为同一个对象:" + a == b);
System.out.println("a 和 c是否为同一个对象:" + a == c);
System.out.println("b 和 c是否为同一个对象:" + b == c);
}
二、String 对比 StringBuffer 和 StringBuilder
线程安全问题上
String 被设计成为不可变对象,且String 无法被继承,所以本身就是线程安全的。
StringBuffer 针对所有方法都在方法加上了 synchronized
锁,以当前实例对象为锁保证线程安全问题
StringBuilder 是线程不安全的,所有的append 操作直接在同一个 char[] 数组中进行,多线程并发操作会有线程安全问题。
字符串拼接执行效率对比
“+” 拼接的效率
public class StringDemo {
public static void main(String[] args) {
String s2 = "";
s2 += "abc";
System.out.println(s2);
}
}
来看一下上述代码的字节码信息
所以如果简单对比 String
和 StringBuilder
能够知道,StringBuilder
的效率更高,因为 String 需要多生成一个对象,多一个对象的操作。
StringBuilder
和 StringBuffer
字符串拼接对比
StringBuilder和
StringBuffer在实现上,几乎差不多,差别就在于
StringBuffer` 是线程安全的,所有操作都需要获取 synchroinzed 锁,
所以 StringBuffer
整体上来说会比 StringBuilder
慢一点
总结
总的来说,效率从高到低:StringBuilder > StringBuffer > String
String 每次拼接都需要多生成一个新的对象,相比较于:StringBuilder 和 StringBuffer 来说效率都低。
而 StringBuffer 为了线程安全,加了 synchronized 同步锁,所以效率会比 StringBuilder 低一点。
不过总的来说,在无法确定线程安全问题情况下,直接使用 StringBuffer 是最安全,效率较高的选择。
三、String 的几道面试题
1、以下语句的输出结果
String s1="abc";
String s2="abc";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
#### 结果
true
true
在 Java 中存在常量池,常量池中的数据只有一份。 虽然 s1 和 s2 指向的是同一个常量对象,但是他们仍然是线程安全,因为该值只有一份。 如果此时有操作对 s1 进行修改,那么 s1 所指向的对象也会随着变更,所以 s1 ,s2 仍然是不可变的对象。
String s1 = "abc";
String s2 = "abc";
System.out.println("s1 == s2:" + ( s1 == s2 ));
System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
// 对 s1 进行写操作
System.out.println("=============================================");
s1 = s1 + s1;
System.out.println("s1 新值:" + s1);
System.out.println("s1 == s2:" + ( s1 == s2 ));
System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
## 结果
s1 == s2:true
s1 equal s2:true
=============================================
s1 新值:abcabc
s1 == s2:false
s1 equal s2:false
2、 String s1=new String(“abc”)
语句创建了几个对象?
String s1 = new String("abc"); //创建了两个对象,一个在常量池中为“abc”,一个在堆内存中也就是s1
String s2 = "abc"; // s2 同 s1 创建的常量池对象
// 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象
System.out.println("s1 == s2:" + ( s1 == s2 ));
System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
## 结果
s1 == s2:false
s1 equal s2:true
创建了两个对象,一个在常量池中,一个在堆中,所以应该避免使用 new String()
3、对比一下两个代码块
public static void main(String[] args) {
method1();
method2();
}
public static void method1(){
String s1 = new String("abc"); // 堆中
String s2 = "abc"; // 常量池中
// 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象
System.out.println("s1 == s2:" + ( s1 == s2 ));
System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
}
public static void method2(){
String s1 = "a" + "b" + "c"; // 常量池中
String s2 = "abc"; // 常量池中
// 编译过程中,由于常量化机制,s1成为了 "abc" 所以也在常量池中创建
System.out.println("s1 == s2:" + ( s1 == s2 ));
System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
}
## 结果
s1 == s2:false
s1 equal s2:true
s1 == s2:true
s1 equal s2:true