导读
- String、StringBuffer、StringBuilder 的区别以及如何正确使用
- 如何理解 String 不可变?
- String、StringBuffer、StringBuilder 在 JVM 内存中的分布,以及面试常问:Java 不同字符类会创建几个对象?
版本及环境
- JDK 运行版本:JDK8
String、StringBuffer、StringBuilder
- String 是不可变字符串,StringBuffer、StringBuilder 是可变字符串。
- StringBuffer 是线程安全的,StringBuilder 是线程非安全的。StringBuilder 具有 StringBuffer 一切能力,当不涉及多线程安全时,优先使用 StringBuilder,没有同步操作,性能更好。
如何理解 String 不可变
- 即无法通过引用地址修改 String 对象的值。
// String 类
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
....
}
1、value[] 数组是 final 类型,表示 value 的引用地址不可变,但是 Arrary 数组是可变的
final char[] charArray = {'a', 'b', 'c', 'd', 'e'};
char ch = 'z';
charArray[0] = ch;
System.out.println(charArray);
2、但是由于 value[] 数组私有,且没有提供外部访问能力,因此无法修改
String 不可变的好处
String HashCode缓存
- String的HashCode在比如HashMap等容器当中都有使用,String的不变性保证了HashCode的不变性,不必每次去计算新的HashCode,这也是Map喜欢将String作为Key的原因,这是一种性能考虑。
安全性
- String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患,比如使用的url被修改。
可变 StringBuilder vs StringBuffer 与线程安全
- StringBuilder、StringBuffer 都是可变的,StringBuffer 是线程安全的,StringBuilder 是线程非安全的。StringBuilder 具有 StringBuffer 一切能力,当不涉及多线程安全是,优先使用 StringBuilder,没有同步操作,性能更好。
// 继承 AbstractStringBuilder 实现不同
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
字符常量池
- Java 内部维护了一个字符常量池,当创建一个字符 String 时,如果已经在字符常量池中存在,则使用该存在该变量,而不会创建新的。
思考一下
- 下面的代码你可以说出它们的创建过程,以及在 JVM 内存中的分配?
// 在常量池中
String str1 = "abc";
// 在堆上
String str2 = new String("abc");
// 字符串常量 号的重载 编译时合并成 abcdef 字符串
String str3 = "abc" "def";
String str4 = "abc" new String("def");
String str41 = new String("def") "abc";
String str5 = new String("abc") new String("def");
String str6 = "defabc";
""、new String()、new StringBuffer()、new StringBuilder() 与字符串常量池
- 从上面可以看出,""、new String()、StringBuffer()、StringBuilder() 创建字符串的过程和结果并不一样,我们分为以下几种情况讨论:
"" 和 new String()
代码语言:Java复制String str1 = "abc";
String str2 = "abc";
// 直接在字符串常量池中查找,如果存在直接使用,不存在创建,即str1、str2指向常量池中的同一个值
String str2 = new String("abc");
// 在堆中创建一个 String 对象,判断 abc 在字符串常量是否存在,存在char数组直接指向,不存在创建新的字符串常量再指向
// 上述检查常量池是否有相同Unicode的字符串常量时,使用的方法是String中的intern()方法
StringBuffer() 和 StringBuilder()
- 两者都是在堆中创建对象,并不涉及字符串常量池,char 数组维护在堆的对象中。
拓展: 当两个字符串连接时,JVM 底层是如何实现的
代码语言:Java复制 // 字符串常量 号的重载 编译时优化合并成 abcdef 字符串
String str3 = "abc" "def";
String str4 = "abc" new String("def");
String str41 = new String("def") "abc";
String str5 = new String("abc") new String("def");
// 上面三种情况,都会先创建一个 StringBuilder 对象再进行字符串拼接,最后调用toString()方法创建一个 String 对象
// 注意: 在这里创建 String 时,并没有在字符串常量池中创建,而是直接指向了 StringBuilder 的 value 数组
// @Override
// public String toString() {
// Create a copy, don't share the array
// return new String(value, 0, count);
// }
String str6 = "defabc";
// 因此,上述 String str6 = "defabc" 会在字符串常量池中创建 "defabc" 常量
字节码分析
完整源码
代码语言:Java复制public class MainTest {
public static void main(String[] args) throws InterruptedException {
// 在常量池中
String str1 = "abc";
// 在堆上
String str2 = new String("abc");
// 字符串常量 号的重载 编译时合并成 abcdef 字符串
String str3 = "abc" "def";
String str4 = "abc" new String("def");
String str41 = new String("def") "abc";
String str5 = new String("abc") new String("def");
String str6 = "defabc";
}
}
字节码
- 使用 javap 命令查看字节码
// javap -c .MainTest.class
Compiled from "MainTest.java"
public class org.example.MainTest {
public org.example.MainTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
---------------------------------------------String str1 = "abc"; --------------------------------------------------------------------------
0: ldc #2 // String abc
2: astore_1
---------------------------------------------String str2 = new String("abc");---------------------------------------------------------------
3: new #3 // class java/lang/String // 创建 String 对象
6: dup
7: ldc #2 // String abc
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
---------------------------------------------String str3 = "abc" "def";-------------------------------------------------------------------
13: ldc #5 // String abcdef 编译时优化为 "abcdef"
15: astore_3
---------------------------------------------String str4 = "abc" new String("def");-------------------------------------------------------
16: new #6 // class java/lang/StringBuilder // 创建 StringBuilder
19: dup
20: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
23: ldc #2 // String abc
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //拼接 abc
28: new #3 // class java/lang/String 创建 String 对象(def)
31: dup
32: ldc #9 // String def
34: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
37: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //拼接 def
40: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; // 调用 toString 方法生成一个新 String 对象
43: astore 4
--------------------------------------------------------------------------------------------------------------------------------------------
45: new #6 // class java/lang/StringBuilder
48: dup
49: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
52: new #3 // class java/lang/String
55: dup
56: ldc #9 // String def
58: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
61: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
64: ldc #2 // String abc
66: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
69: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
72: astore 5
74: new #6 // class java/lang/StringBuilder
77: dup
78: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
81: new #3 // class java/lang/String
84: dup
85: ldc #2 // String abc
87: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
90: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
93: new #3 // class java/lang/String
96: dup
97: ldc #9 // String def
99: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
102: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
105: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
108: astore 6
110: ldc #11 // String defabc
112: astore 7
114: return
}
总结
- 当用于 url、路径地址 等需要保证不可以变的使用 String,可变的场景下需要保证线程安全使用 StringBuffer,不需要保证线程安全使用 StringBuilder,减少同步加锁开销。
- 常见面试题:不同使用字符串的方式会创建多少个对象:
案例一:
String str3 = "abc" "def";
编译阶段优化为 “abcdef” 在常量池创建一个对象 “abcdef”
案例二:
String str4 = "abc" new String("def")
"abc" 在常量池创建一个对象
new String("def") 在堆中创建一个 String 对象,常量池创建对象 "def"
创建一个 StringBuilder 对象用于拼接两个字符串,最后调用 toString() 方法创建一个 String 对象
// 即在不考虑字符串常量池已存在常量字符串的情况,需要创建 5 个对象
// 再思考一下:其实还有一个强引用对象 str4 对创建字符串的强引用
个人简介