文章目录
-
- [1. 经典问题:`new String("abc")` 创建了几个对象?](#1. 经典问题:
new String("abc")创建了几个对象?) - [2. 为什么 String 要设计成不可变的 (Immutable)?](#2. 为什么 String 要设计成不可变的 (Immutable)?)
-
- 核心原因解析
-
- [A. 字符串常量池 (String Pool) 的需要](#A. 字符串常量池 (String Pool) 的需要)
- [B. 安全性 (Security)](#B. 安全性 (Security))
- [C. HashCode 缓存 (Performance)](#C. HashCode 缓存 (Performance))
- [3. String vs StringBuilder vs StringBuffer](#3. String vs StringBuilder vs StringBuffer)
- [4. 最佳实践总结](#4. 最佳实践总结)
- [1. 经典问题:`new String("abc")` 创建了几个对象?](#1. 经典问题:
在 Java 开发中,String 是最常用的类之一。本文将深入底层,剖析 String 的创建机制、不可变性的设计哲学以及与 StringBuilder、StringBuffer 的核心区别。
1. 经典问题:new String("abc") 创建了几个对象?
要回答这个问题,我们需要理解 Java 的 字符串常量池 (String Constant Pool) 机制。
代码分析
java
String s = new String("abc");
答案
答案通常是 1 个或 2 个,具体取决于代码执行时的上下文环境。
-
情况一:2 个对象
如果字符串常量池中还没有
"abc"这个字符串引用。- 第 1 个对象 :字面量
"abc"会被加载到字符串常量池中(在堆中创建一个 String 对象,并在池中保存引用)。 - 第 2 个对象 :
new String(...)关键字会在堆内存中创建一个新的 String 对象,内容也是"abc"。
- 第 1 个对象 :字面量
-
情况二:1 个对象
如果字符串常量池中已经存在
"abc"。- 此时只需要执行
new String(...),在堆中创建一个新的对象。常量池中的那个已经存在,不需要重复创建。
- 此时只需要执行
内存布局示意图
s 指向 StringObject_1
StringObject_1 内部 char[] 指向实际数据
引用指向 StringObject_2
Stack
s(引用)
Heap
StringObject_1(new出来的)
StringObject_2(常量池引用的)
StringPool
"abc"(引用指向 StringObject_2)
2. 为什么 String 要设计成不可变的 (Immutable)?
在 Java 中,String 类被 final 修饰,且内部用于存储字符的数组(Java 9 之前是 char[],Java 9 之后是 byte[])也被 private final 修饰。这意味着一旦 String 对象被创建,其包含的字符序列就不能被改变。
核心原因解析
A. 字符串常量池 (String Pool) 的需要
只有当字符串是不可变时,字符串常量池才有可能实现。如果 String 是可变的,当一个引用改变了字符串的值,其他指向同一个常量的引用也会受到影响,这将导致混乱。
java
String s1 = "hello";
String s2 = "hello";
// s1 和 s2 指向内存中同一个对象
// 如果 s1 修改了内容,s2 也会莫名其妙地变化,这显然是不合理的。
B. 安全性 (Security)
String 被广泛用于网络连接、文件路径、数据库连接 URL 以及类加载机制中。
- 网络/文件安全:如果 String 可变,黑客可以利用漏洞修改验证通过后的路径或参数。
- 多线程安全:不可变对象天生是线程安全的。多个线程可以安全地共享同一个 String 对象,无需进行同步控制。
C. HashCode 缓存 (Performance)
String 经常被用作 HashMap 或 HashSet 的 Key。String 类内部有一个 hash 字段用来缓存哈希码。
java
// String 源码片段
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 计算 hash 值并赋值给 h
hash = h; // 缓存起来
}
return h;
}
因为 String 是不可变的,所以它的 HashCode 只需要计算一次就可以缓存下来,后续使用时直接返回,性能极高。如果 String 可变,每次使用都要重新计算 HashCode。
3. String vs StringBuilder vs StringBuffer
这三者是 Java 字符串处理的三驾马车,理解它们的区别对于编写高性能代码至关重要。
核心区别对比表
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 (Immutable) | 可变 (Mutable) | 可变 (Mutable) |
| 线程安全 | 安全 (常量特性) | 安全 (方法由 synchronized 修饰) | 不安全 |
| 性能 | 修改时需创建新对象,慢 | 中等 (有锁开销) | 最快 |
| 适用场景 | 少量字符串操作,作为常量 | 多线程环境下的字符串拼接 | 单线程环境下的大量字符串拼接 |
源码层面的区别
1. String (JDK 8)
java
public final class String {
private final char value[]; // final 修饰,不可变
}
2. StringBuffer (JDK 8)
java
public final class StringBuffer extends AbstractStringBuilder {
@Override
public synchronized StringBuffer append(String str) { // synchronized 锁
toStringCache = null;
super.append(str);
return this;
}
}
3. StringBuilder (JDK 8)
java
public final class StringBuilder extends AbstractStringBuilder {
@Override
public StringBuilder append(String str) { // 无锁,直接调用父类方法
super.append(str);
return this;
}
}
性能对比时序图
假设我们要进行 10000 次字符串拼接操作:
StringBuilder StringBuffer String User StringBuilder StringBuffer String User 场景:循环拼接 "a" 1万次 loop [10000次] 产生大量垃圾对象,极慢 loop [10000次] 有锁竞争开销,较快 loop [10000次] 无锁,最快 s = s + "a" 1. 创建 StringBuilder\n2. append\n3. toString (创建新String) sb.append("a") 获取锁 ->> 修改数组 ->> 释放锁 sb.append("a") 直接修改数组
4. 最佳实践总结
-
字面量优于 new:
- 推荐:
String s = "hello";(利用常量池) - 避免:
String s = new String("hello");(多余的堆对象)
- 推荐:
-
循环拼接禁止使用
+:- 在循环体内,使用
+拼接字符串会导致编译器每次循环都创建一个新的StringBuilder对象,造成巨大的内存浪费和 GC 压力。 - 正确做法 :在循环外创建
StringBuilder,在循环内调用append。
java// ❌ 错误示范 String result = ""; for (int i = 0; i < 1000; i++) { result += i; // 每次都会 new StringBuilder() 和 new String() } // ✅ 正确示范 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append(i); // 复用同一个对象内部的数组 } String result = sb.toString(); - 在循环体内,使用
-
多线程环境:
- 如果是一个类的成员变量被多个线程并发修改,使用
StringBuffer。 - 如果是方法内部的局部变量(栈封闭),依然推荐使用
StringBuilder,因为局部变量不存在线程安全问题。
- 如果是一个类的成员变量被多个线程并发修改,使用