Java常用类-String三剑客
目录
- 一、核心差异总结(先看结论)
- 二、String类(不可变的秘密)
- 1. 源码关键设计
- 1.1关键差异总结
- 2. 内存管理机制
- 2.1各个迭代版本的区别
- Java 6及之前
- 示例代码(Java 6):
- Java 7
- Java 8
- Java 9
- 源码示例(Java 9+):
- Java 11
- 示例代码:
- Java 12+
- Java 15+
- 2.2内存占用对比
- 2.3总结:各版本内存管理重点
- 2.4优化建议
- 3.常用方法
- 3.1 字符串拼接与替换
- 3.2 查找与截取
- 3.3 大小写转换与空白处理
- 3.4 分割与匹配
- 三、StringBuilder(单线程王者)
- 1. 存储结构:char[] 动态扩容
- 源码关键部分:
- 2. 动态扩容机制
- 源码关键部分:
- 3. 核心方法:append() 的实现
- 源码关键部分:
- 高效操作原理
- 4. 线程不安全设计
- 对比示例:
- 5. 字符编码优化(Java 9+)
- 源码关键部分:
- 6. toString() 方法的优化
- 源码关键部分:
- 核心性能优势
- 使用建议
- 四、StringBuffer(多线程卫士)
- 1. 继承结构与存储设计
- 源码关键部分:
- 2. 线程安全实现
- 示例对比:
- 3. 动态扩容机制
- 源码关键部分:
- 4. 字符编码优化(Java 9+)
- 源码关键部分:
- 5. 同步开销与性能权衡
- 性能测试示例(单线程场景):
- 6. 特殊方法实现
- 源码关键部分:
- 五、StringBuffer和StringBuilder两者扩容机制详解
- 1. 存储结构与编码优化
- 源码关键部分:
- 2. 核心扩容逻辑
- 源码关键部分:
- 3. 编码感知的容量计算
- 源码关键部分:
- 总结:JDK 17+ 扩容机制的核心特点
- 六、StringBuffer/StringBuilder 常用方法
- 1. 追加与插入
- 2. 删除与替换
- 3. 反转与容量管理
- 4. 转换为String
- 七、三者对比表
- 八、终极使用策略
- 1.String:
- 2.StringBuilder:
- 3.StringBuffer:
- 使用场景建议
- 一句话记忆口诀
深入解析Java字符串三剑客:String、StringBuilder、StringBuffer
一、核心差异总结(先看结论)
类 | 可变性 | 线程安全 | 性能 | 适用场景 |
---|---|---|---|---|
String | ❌ 不可变 | ✅ 安全 | 最差 | 少量字符串操作 |
StringBuilder | ✅ 可变 | ❌ 不安全 | 最高 | 单线程频繁修改 |
StringBuffer | ✅ 可变 | ✅ 安全 | 中等 | 多线程环境 |
二、String类(不可变的秘密)
1. 源码关键设计
//Java 17+ 密封类声明(强调不可继承性)
public final class String implements Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {@Stable// Java9 存储字符串值的字节数组(关键!final修饰的不可变数组)private final byte[] value; // Java9改用byte数组存储private final byte coder; // 0=LATIN1, 1=UTF16private int hash; // 哈希缓存// Java7 substring方法(不共享数组,复制新数组)public String substring(int beginIndex, int endIndex) {int subLen = endIndex - beginIndex;char[] sub = new char[subLen];System.arraycopy(value, beginIndex, sub, 0, subLen);return new String(sub, false);}// Java9构造函数(根据内容选择编码)public String(char[] value, int offset, int count) {this.value = StringUTF16.compress(value, offset, count);this.coder = this.value == null ? UTF16 : LATIN1;}// Java9 charAt方法(需根据coder处理)public char charAt(int index) {if (isLatin1()) {return (char)(value[index] & 0xff);} else {return StringUTF16.charAt(value, index);}}// Java11 新增repeat方法public String repeat(int count) {if (count < 0) {throw new IllegalArgumentException();}if (count == 1) {return this;}int len = length();if (len == 0 || count == 0) {return "";}// ...省略数组复制逻辑}//Java11 新增isBlank方法public boolean isBlank() {for (int i = 0; i < value.length; i++) {if (!Character.isWhitespace(value[i])) {return false;}}return true;}
}
- 所有修改操作(如concat、replace)都会创建新String对象
- 例子:String s = “a”; s += “b”; → 实际产生2个对象(“a"和"ab”)
1.1关键差异总结
版本 | 核心改动点 |
---|---|
Java 6 | char[] value + offset/count(子串共享数组) |
Java 7 | 移除 offset/count,substring 复制新数组 |
Java 9 | byte[] value + coder(Latin-1/UTF-16 动态编码) |
Java 11 | 新增 repeat()、isBlank()、strip() 等实用方法 |
Java 15+ | 密封类声明,强制 final(实际自 Java 1 即不可继承) |
这些改动体现了 Java 对内存效率(如 Latin-1 单字节存储)和 API 易用性(如repeat方法)的持续优化。
2. 内存管理机制
String 的内存管理机制在不同版本中都经历了重大变革,主要围绕节省内存、减少GC压力和优化字符串常量池展开。
2.1各个迭代版本的区别
Java 6及之前
- 存储结构:使用char[] value存储字符,每个字符占2字节(UTF-16),即使仅包含ASCII字符也需2字节。
- 字符串常量池:位于永久代(PermGen),空间固定(默认 64MB),无法动态扩容,易导致OutOfMemoryError: PermGen space。
- substring 内存泄漏风险:子串与原字符串共享char[],即使原字符串不再使用,数组也无法被GC(示例如下)。
示例代码(Java 6):
String str = "abcdefghijklmnopqrstuvwxyz";
String sub = str.substring(0, 2); // sub仅需2字符,但共享原数组(26字符)
// str不再使用,但原char[]无法被回收
Java 7
- 字符串常量池迁移:从永久代移至堆内存(Heap),避免永久代溢出,可通过-Xmx参数调整堆大小。
- substring优化:不再共享原char[],而是复制新数组,解决内存泄漏问题,但可能增加内存使用(每次截取都创建新数组)。
Java 8
- 永久代移除:引入元空间(Metaspace)替代永久代,字符串常量池仍在堆中。
- 压缩指针优化(Compressed OOPs):对象指针压缩为32 位,减少内存开销。
Java 9
- 字符串常量池优化:
- 1)启动时预加载常用字符串(如JDK内部字符串)。
- 2)新增-XX:StringTableSize 参数调整哈希表大小(默认 60013),减少哈希冲突。 - 革命性存储结构:byte[] + coder
将 char[] 改为 byte[],并新增 coder 字段标记编码:
- 1)LATIN1(单字节,占 1 字节 / 字符):存储 ASCII 字符(最常见场景)。
- 2)UTF16(双字节,占 2 字节 / 字符):存储非 ASCII 字符(如中文、 emoji)。
内存节省:对于纯 ASCII 字符串,内存占用减少 50%。
源码示例(Java 9+):
public final class String {private final byte[] value; // 存储字节private final byte coder; // 0=LATIN1, 1=UTF16public char charAt(int index) {return coder == LATIN1 ? (char)(value[index] & 0xff) : // Latin-1解码StringUTF16.charAt(value, index); // UTF-16解码}
}
Java 11
- G1 GC字符串去重(默认开启):
通过 -XX:+UseStringDeduplication 参数,让G1在GC时自动检测并合并内容相同的字符串(通过byte[]比较),进一步节省内存。
示例代码:
String s1 = new String("hello");
String s2 = new String("hello");
// 经G1去重后,s1和s2可能指向同一byte[]
Java 12+
- Shenandoah GC优化:
对字符串常量池的并发标记和回收更高效,减少STW(Stop The World)时间。 - ZGC支持(Java 15+):
处理大堆(TB 级)时,字符串常量池的管理性能显著提升。
Java 15+
- Compact Strings 成为默认:Java 9 引入的 byte[] 存储方案完全稳定,无法通过参数关闭。
- 字符串常量池动态调整:
JVM 会根据运行时情况自动调整字符串常量池的哈希表大小,减少内存碎片。
2.2内存占用对比
版本 | 存储方式 | 纯 ASCII 字符串(100 字符) | 包含中文的字符串(50 中文 + 50 英文) |
---|---|---|---|
Java 6 | char[] | 200 字节 | 300 字节 |
Java 9 | byte[] | 100 字节(节省 50%) | 200 字节(节省 33%) |
2.3总结:各版本内存管理重点
版本 | 核心改进点 |
---|---|
Java 6 | 永久代常量池,substring共享数组导致内存泄漏 |
Java 7 | 常量池移至堆,substring复制数组 |
Java 8 | 移除永久代,引入元空间 |
Java 9 | byte[] + coder 存储,字符串常量池哈希表优化 |
Java 11 | G1 GC字符串去重,减少重复字符串内存占用 |
Java 15+ | Compact Strings强制启用,ZGC优化大堆字符串管理 |
2.4优化建议
- 优先使用 Java 9+:利用 byte[] 存储节省内存。
- 避免大字符串频繁截取:Java 7+ 虽修复内存泄漏,但复制数组仍有开销。
- 控制字符串常量池大小:通过 -XX:StringTableSize 优化哈希表性能。
- 启用字符串去重(Java 11+):对重复字符串多的场景(如缓存、日志),开启 -XX:+UseStringDeduplication。
- 避免手动 intern():除非明确需要共享字符串(如缓存键),否则可能增加 GC 压力。
3.常用方法
String 是不可变类,所有修改操作都会返回新的 String 对象。
3.1 字符串拼接与替换
String s = "Hello";// 拼接
String s1 = s.concat(" World"); // "Hello World"
String s2 = s + " Java"; // "Hello Java"// 替换
String s3 = s.replace('e', 'a'); // "Hallo"
String s4 = s.replaceAll("ll", "yy"); // "Heyylo"
3.2 查找与截取
String s = "HelloWorld";// 查找
int index = s.indexOf("World"); // 5
boolean contains = s.contains("lo"); // true// 截取
String sub = s.substring(5); // "World"
String sub2 = s.substring(0, 5); // "Hello"
3.3 大小写转换与空白处理
String s = " Hello ";// 大小写
String upper = s.toUpperCase(); // " HELLO "
String lower = s.toLowerCase(); // " hello "// 空白处理
String trimmed = s.trim(); // "Hello" (Java 11+ 可用 strip())
3.4 分割与匹配
String s = "a,b,c";// 分割
String[] parts = s.split(","); // ["a", "b", "c"]// 正则匹配
boolean isNum = "123".matches("\\d+"); // true
三、StringBuilder(单线程王者)
1. 存储结构:char[] 动态扩容
StringBuilder内部使用char[]数组存储字符序列,并通过count字段记录当前实际长度。数组会在容量不足时动态扩容。
源码关键部分:
abstract class AbstractStringBuilder {char[] value; // ← 关键!没有final修饰,可扩容int count; // 实际字符数量AbstractStringBuilder(int capacity) {value = new char[capacity]; // 初始容量}
}
public final class StringBuilder extends AbstractStringBuilder {public StringBuilder() {super(16); // 默认初始容量为16}// 所有方法都没有synchronized修饰!@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
}
2. 动态扩容机制
当追加内容导致容量不足时,会触发扩容逻辑:
- 计算新容量(通常为原容量的2倍 + 2)。
- 创建新数组并复制原有内容。
- 丢弃原数组,指向新数组。
源码关键部分:
abstract class AbstractStringBuilder {private void ensureCapacityInternal(int minimumCapacity) {if (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value, newCapacity(minimumCapacity));}}private int newCapacity(int minCapacity) {int oldCapacity = value.length;int newCapacity = oldCapacity * 2 + 2; // 关键公式:2倍+2if (newCapacity - minCapacity < 0) {newCapacity = minCapacity; // 至少满足最小需求}return newCapacity;}
}
3. 核心方法:append() 的实现
append() 是最常用的方法,支持追加各种类型的数据。其核心逻辑是:
- 检查容量是否足够,不足则扩容。
- 将数据转换为字符并复制到value数组。
源码关键部分:
public AbstractStringBuilder append(String str) {if (str == null) return appendNull(); // 处理nullint len = str.length();ensureCapacityInternal(count + len); // 确保容量足够str.getChars(0, len, value, count); // 复制字符到value数组count += len; // 更新实际长度return this;
}
高效操作原理
StringBuilder sb = new StringBuilder();
sb.append("A").append("B"); // 始终操作同一个对象
- 链式调用:方法返回this对象,支持链式编程
- 扩容示例:
- 初始:char[16]
- 追加17个字符 → 扩容到(16 * 2)+2=34
4. 线程不安全设计
StringBuilder不保证线程安全,所有方法均未加锁。相比之下,StringBuffer通过synchronized修饰方法保证线程安全,但性能较低。
对比示例:
// StringBuilder(线程不安全,性能高)
public StringBuilder append(String str) {// 无同步操作
}// StringBuffer(线程安全,性能低)
public synchronized StringBuffer append(String str) {// 同步块
}
5. 字符编码优化(Java 9+)
Java 9 及以后,StringBuilder与String保持一致,改用byte[]存储:
- Latin-1 编码:单字节存储 ASCII 字符。
- UTF-16 编码:双字节存储其他字符。
源码关键部分:
abstract class AbstractStringBuilder {byte[] value;byte coder; // 0=LATIN1, 1=UTF16// 根据内容选择编码private void ensureCapacityInternal(int minimumCapacity) {if (nonLatin1(capacity)) {value = new byte[newCapacity(minimumCapacity)];coder = UTF16;} else {value = new byte[newCapacity(minimumCapacity)];coder = LATIN1;}}
}
6. toString() 方法的优化
toString() 会创建新的String对象,但通过复用char[]或byte[]避免重复复制:
源码关键部分:
public String toString() {// 创建新String对象,复用内部数组(Java 9+使用byte[])return isLatin1() ?new String(value, LATIN1) :new String(value, UTF16);
}
核心性能优势
- 避免频繁创建对象:通过动态扩容减少内存分配和垃圾回收。
- 直接操作数组:append() 等方法通过System.arraycopy() 高效复制数据。
- 无同步开销:线程不安全设计避免了锁竞争。
使用建议
- 预分配容量:已知大致长度时,通过new StringBuilder(initialCapacity) 减少扩容次数。
- 循环外创建实例:避免在循环中重复创建StringBuilder。
反例:
for (int i = 0; i < 1000; i++) {StringBuilder sb = new StringBuilder(); // 每次循环创建新对象sb.append(i);
}
正例:
StringBuilder sb = new StringBuilder(); // 在循环外创建
for (int i = 0; i < 1000; i++) {sb.append(i);
}
四、StringBuffer(多线程卫士)
1. 继承结构与存储设计
StringBuffer继承自AbstractStringBuilder,与StringBuilder共享存储逻辑,但所有公开方法均被synchronized修饰以保证线程安全。
源码关键部分:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable {// 继承AbstractStringBuilder的字段// char[] value; // 存储字符的数组// int count; // 实际字符数量public StringBuffer() {super(16); // 默认初始容量16}
}
2. 线程安全实现
所有修改操作(如append、insert)均被synchronized修饰,确保同一时间只有一个线程能修改内部状态。
示例对比:
// StringBuffer(线程安全)
public synchronized StringBuffer append(String str) {super.append(str);return this;
}// StringBuilder(线程不安全)
public StringBuilder append(String str) {super.append(str);return this;
}
3. 动态扩容机制
与StringBuilder完全相同,通过ensureCapacityInternal() 和newCapacity() 实现数组动态扩容,默认扩容为2倍 + 2。
源码关键部分:
abstract class AbstractStringBuilder {private void ensureCapacityInternal(int minimumCapacity) {if (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value, newCapacity(minimumCapacity));}}private int newCapacity(int minCapacity) {int newCapacity = (value.length << 1) + 2; // 2倍+2// ... 其他边界处理}
}
4. 字符编码优化(Java 9+)
Java 9及以后,与String和StringBuilder一致,改用byte[]存储:
- Latin-1 编码:单字节存储 ASCII 字符。
- UTF-16 编码:双字节存储其他字符。
源码关键部分:
abstract class AbstractStringBuilder {byte[] value;byte coder; // 0=LATIN1, 1=UTF16// 根据内容选择编码private void ensureCapacityInternal(int minimumCapacity) {if (nonLatin1(capacity)) {value = new byte[newCapacity(minimumCapacity)];coder = UTF16;} else {value = new byte[newCapacity(minimumCapacity)];coder = LATIN1;}}
}
5. 同步开销与性能权衡
synchronized修饰导致的锁竞争会带来显著性能开销,尤其在高并发场景下。测试数据显示(单线程场景),StringBuffer的单线程性能比StringBuilderda低20%-50%。
性能测试示例(单线程场景):
@Test//约为38mspublic void test1() {long startTime = System.currentTimeMillis();StringBuilder sb = new StringBuilder();for (int i = 0; i < 10000000; i++) {sb.append("a");}long endTime = System.currentTimeMillis();System.out.println(endTime - startTime);}@Test//约为212mspublic void test2() {long startTime = System.currentTimeMillis();StringBuffer sb = new StringBuffer();for (int i = 0; i < 10000000; i++) {sb.append("a");}long endTime = System.currentTimeMillis();System.out.println(endTime - startTime);}
6. 特殊方法实现
toStringCache 字段
StringBuffer维护一个缓存字段toStringCache,用于暂存最后一次toString() 的结果。当对象被修改时,该缓存会被清空。
源码关键部分:
public final class StringBuffer {private transient char[] toStringCache; // 缓存toString()结果@Overridepublic synchronized String toString() {if (toStringCache == null) {toStringCache = Arrays.copyOfRange(value, 0, count);}return new String(toStringCache, true);}@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null; // 修改时清空缓存super.append(str);return this;}
}
五、StringBuffer和StringBuilder两者扩容机制详解
1. 存储结构与编码优化
自JDK 9起,两者均使用byte[]替代char[],并通过coder字段标记编码类型:
- Latin-1(单字节):存储 ASCII 字符(占 1 字节 / 字符)。
- UTF-16(双字节):存储非 ASCII 字符(如中文、emoji)。
源码关键部分:
abstract class AbstractStringBuilder implements Appendable, CharSequence {byte[] value; // 存储字节数据byte coder; // 0=LATIN1, 1=UTF16int count; // 实际字符数量(非字节数)// 返回容量(字节数)final int capacity() {return value.length;}
}
2. 核心扩容逻辑
注意:StringBuilder/StringBuffer的length()返回的是count,不是value.length。
例如:sb.length() == 5 但底层数组可能是char[34]
当追加内容导致容量不足时,会触发 ensureCapacityInternal() 方法:
源码关键部分:
private void ensureCapacityInternal(int minimumCapacity) {// 如果当前容量不足if (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value, newCapacity(minimumCapacity));}
}private int newCapacity(int minCapacity) {// 旧容量加倍 + 2(核心扩容公式)int newCapacity = (value.length << 1) + 2;// 确保新容量至少满足最小需求if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}// 检查是否超过最大数组大小(Integer.MAX_VALUE - 8)return hugeCapacity(minCapacity);
}private int hugeCapacity(int minCapacity) {if (Integer.MAX_VALUE - minCapacity < 0) {throw new OutOfMemoryError();}return (minCapacity > MAX_ARRAY_SIZE) ?minCapacity :MAX_ARRAY_SIZE;
}
- 关键点:
- 默认扩容公式:新容量 = 旧容量 × 2 + 2。
- 边界处理:
- 若新容量小于所需最小容量,直接使用最小容量。
- 若新容量超过 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),则尝试使用minCapacity,但可能触发 OutOfMemoryError。
3. 编码感知的容量计算
在计算容量时,需根据 coder 类型(Latin-1/UTF-16)调整字节数:
源码关键部分:
// 追加String时的容量计算
public AbstractStringBuilder append(String str) {if (str == null) return appendNull();int len = str.length();ensureCapacityInternal(count + len); // 按字符数计算最小容量str.getBytes(value, coder, count); // 按实际编码存储count += len;return this;
}
- 关键点:
- ensureCapacityInternal(count + len) 按字符数计算最小容量。
- 实际存储时,getBytes() 会根据 coder 类型决定占用字节数:
- Latin-1:1 字符 = 1 字节。
- UTF-16:1 字符 = 2 字节。
总结:JDK 17+ 扩容机制的核心特点
特性 | 详情 |
---|---|
存储结构 | byte[] + coder(Latin-1/UTF-16),节省ASCII字符串50%内存 |
扩容公式 | 新容量 = 旧容量 × 2 + 2,确保最小需求 |
线程安全 | StringBuffer同步所有方法,StringBuilder非线程安全 |
边界处理 | 最大容量为Integer.MAX_VALUE - 8,超量则抛OutOfMemoryError |
六、StringBuffer/StringBuilder 常用方法
两者方法基本相同,区别在于StringBuffer线程安全(方法有synchronized修饰),StringBuilder非线程安全。
1. 追加与插入
StringBuilder sb = new StringBuilder("Hello");// 追加
sb.append(" World"); // "Hello World"
sb.append(123); // "Hello World123"// 插入
sb.insert(5, ","); // "Hello, World123"
2. 删除与替换
StringBuilder sb = new StringBuilder("HelloWorld");// 删除
sb.delete(5, 10); // "Hello"
sb.deleteCharAt(4); // "Hell"// 替换
sb.replace(0, 2, "Hi"); // "Hill"
3. 反转与容量管理
StringBuilder sb = new StringBuilder("Hello");// 反转
sb.reverse(); // "olleH"// 容量管理
int capacity = sb.capacity(); // 默认16,动态扩容
sb.ensureCapacity(100); // 确保容量至少为100
4. 转换为String
StringBuilder sb = new StringBuilder("Hello");
String s = sb.toString(); // "Hello"
七、三者对比表
方法 / 场景 | String(不可变) | StringBuffer(线程安全) | StringBuilder(非线程安全) |
---|---|---|---|
拼接性能 | 低(每次创建新对象) | 中(同步开销) | 高(无同步) |
追加操作 | s = s + “a”(效率低) | sb.append(“a”) | sb.append(“a”) |
插入操作 | 无直接方法 | sb.insert(0, “a”) | sb.insert(0, “a”) |
删除操作 | 无直接方法 | sb.delete(0, 1) | sb.delete(0, 1) |
反转字符串 | 无直接方法 | sb.reverse() | sb.reverse() |
线程安全 | 是(不可变) | 是(所有方法同步) | 否 |
适用场景 | 少量拼接、常量字符串 | 多线程频繁操作 | 单线程频繁操作 |
八、终极使用策略
1.String:
字符串常量(如配置信息)
不需要修改的字符串
作为HashMap的键(不可变性保证哈希值稳定)
之所以设计String,且设计为不可变
安全性:作为网络连接参数、类加载名称时不会被篡改
哈希缓存:String常用作HashMap的键,不可变保证hashCode稳定
线程安全:天然线程安全
2.StringBuilder:
SQL拼接、JSON构建、日志组装等单线程场景
示例:
StringBuilder sql = new StringBuilder(128);
sql.append("SELECT * FROM users").append(" WHERE age > ").append(age);
3.StringBuffer:
多线程日志处理
全局共享的字符串缓冲区
注意:即使使用StringBuffer,复合操作仍需额外同步
使用场景建议
单线程环境:优先使用 StringBuilder,避免不必要的同步开销。
多线程环境:若需线程安全,使用 StringBuffer 或通过 Collections.synchronizedList 包装 StringBuilder。
高并发场景:考虑使用 StringBuilder 配合手动同步(如 ReentrantLock),或分段处理后合并结果。
一句话记忆口诀
“一静(String)两动(Builder/Buffer),单(线程)快多(线程)稳”
静:String不可变
动:Builder/Buffer可变
单快:单线程用Builder
多稳:多线程用Buffer