作为作为一个已经入了门的 java 程序猿,肯定对 Java 中的 String、StringBuffer 和 StringBuilder 都略有耳闻了,尤其是 String 肯定是经常用的。但肯定你有一点很好奇,为什么 java 中有三个关于字符串的类?一个不够吗!先回答这个问题,黑格尔曾经说过——存在必合理,单纯一个 String 确实是不够的,所以要引入 StringBuffer。再后来引入 StringBuilder 是另一个故事了,后面会详细讲到。
要了解为什么,我们就得先来看下这三者各自都有什么样的特点,有什么样的异同,对其知根知底之后,一切谜团都会被解开。
String
点开 String 的源码,可以发现 String 被定义为 final 类型,意味着它不能被继承,再仔细看其提供的方法,没有一个能对原始字符串做任何操作的,有几个开启了貌似是操作原字符串的,比如 replaceFirst replaceAll,点进去一看,其实是重新生成了一个新的字符串,对原始内容没有做任何修改。
是的,从实现的角度来看,它是不可变的,所有 String 的变更其实都会生成一个新的字符串,比 String str = "abcdefghijklmnopqrstuvwxy"; str = str + "z";
之后新生成的 a - z 并不包含原来的 a -y,原来的 a - y 已经变成垃圾了。简单概括,只要是两个不同的字符,肯定都是两个完全不同不相关的对象,即便其中一个是从另一个 subString 出来的,两个也没有任何关系。如果是两个相同的字符串,情况比较复杂,可能是同一份也可能不是。如果在 JVM 中使用 G1gc,而且开启-XX:+UseStringDeduplication
,JVM 会对字符串的存储做优化,所以如果你的服务中有大量相同字符串,建议开启这个参数。
Java 作为一个非纯面向对象的语言,除了提供分装对象外,也提供了一些原始类型(比如:int long double char),String 的使用居然可以像用原始类型一样不需要 new,直接String str = "a"
这样声明,我觉得 String 更像是面向对象和非面向对象结合的一个产物。
String 最大的特点就是 __ 不可变__,这是它的优点,因为不可变意味着使用简单,没有线程安全的问题。但这也是它的缺点,因为每次变更都会生成一个新的字符串,明显太浪费空间了。
StringBuffer
我觉得 StringBuffer 是完全因为 String 的缺点而生的。我们日常使用 String 的过程中,肯定经常会用到字符串追加的情况,按 String 的实现,没次追加即便只是一个字符,都是生成一个完全不同的对象,如果这次操作很频繁很多的话会大幅提高内存的消耗,并且增加 gc 的压力。对于这种问题,StringBuffer 是如何解决的呢?我们直接从源码上来看。
但看 StringBuffer 里,几乎所有的方法都会调 super 父类,其实它所有的实现都是在 AbstractStringBuilder 里的。鉴于我们对其最长用的方法是 append,所以我们就从 append 入手,其实 append 也是 StringBuffer 比较核心的功能。
/**
* The value is used for character storage.
*/
char[] value;
AbstractStringBuilder(int capacity) {value = new char[capacity];
}
public AbstractStringBuilder append(String str) {if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
原来是 StringBuffer 父类 AbstractStringBuilder 有个 char 数组 value,用来存放字符串所有的字符,StringBuffer 默认初始大小是 16。StringBuffer 在每次 append 的时候,如果 value 的容量不够,就会申请一个容量比当前所需大一倍的字符数组,然后把旧的数据拷贝进去。这种一次性扩容一倍的方式,在我们之前 HashMap 源码浅析中已经看到过了。一次性多申请内存,虽然看起来会有大段的内存空闲,但其实可以减少 String append 时频繁创建新字符串的问题。
所以记住,如果你代码中对 String 频繁操作,千万不用用 String 而是选择用 StringBuffer 或者我们下面要讲的 StringBuilder。还有一个优化点,如果你能提前知道你字符串最大的长度,建议你在创建 StringBuffer 时指定其 capacity,避免在 append 时执行 ensureCapacityInternal,从而提升性能。
对于 StringBuffer 还有一个点没提到,注意看它源码的所有方法,除构造函数外,所有的方法都被__synchronized__修饰,意味着它是有个线程安全的类,所有操作查询方法都会被加同步,但是如果我们只是单线程呢,想用 StringBuffer 的优势,但又觉得加同步太多余,太影响性能。这个时候就轮到 StringBuilder 上场了。
StringBuilder
StringBuilder 从类图上看和 StringBuffer 完全没有任何区别,再打开它的源码,和 StringBuffer 一样几乎啥逻辑都没有,全是调调 super 父类 AbstractStringBuilder,它和 StringBuffer 最大的区别就是所有方法没有用 synchronized 修复,它不是一个线程安全的类,但也意味着它没有同步,在单线程情况下性能会优于 StringBuffer。
总结
看完上面内容,我觉得你应该知道上面时候用 String、什么时候用 StringBuffer、什么时候用 StringBuilder 了。
- 如果是常量字符串,用 String。
- 多线程环境下经常变动的字符串用 StringBuffer。
- 单线程经常变动的字符串用 StringBuilder。
彩蛋
我们来看个比较底层的东西,是关于 jvm 对 String 优化的,现在有如下代码。
public class StringTest {public static void main(String[] args) {
String str = "abc";
str = str + "d";
str = str + "e";
}
}
我们用 javac StringTest.java
编译成 class 文件,然后用 javap -c StringTest
生成字节码,内容如下
public class StringTest {public StringTest();
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 abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String d
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: return
}
➜ java git:(master) ✗ javap -c StringTest
Compiled from "StringTest.java"
public class StringTest {public StringTest();
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 abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String d
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: return
}
➜ java git:(master) ✗ javac StringTest.java
➜ java git:(master) ✗ javap -c StringTest
Compiled from "StringTest.java"
public class StringTest {public StringTest();
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 abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String d
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: new #3 // class java/lang/StringBuilder
26: dup
27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
30: aload_1
31: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc #8 // String e
36: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_1
43: return
}
其实可以看出,java 底层实现字符串 + 的时候其实是用 StringBuilder 的 append()来实现的,如果有字符串的连续 +,jvm 用 StringBuilder append 也可以实现优化。
备注:源码来自 JDK11
本文由博客一文多发平台 OpenWrite 发布!