String详解
String是Java中最常用的类,具有**不可变性(Immutable)**特性。理解String的底层实现、常量池机制和性能优化,是Java开发的基础。
一、String的不可变性
1.1 底层实现演进
// JDK 8及之前:char数组
private final char value[];
// JDK 9及之后:byte数组 + 编码标识
private final byte[] value;
private final byte coder; // 0=LATIN1, 1=UTF16
为什么改为byte数组?
- 大部分字符串是Latin-1字符(ASCII),用1字节足够
- 节省内存:Latin-1字符串内存减少50%
- coder标识编码方式,按需选择1字节或2字节存储
1.2 不可变性的实现
public final class String { // ① final类,不可继承
private final byte[] value; // ② final数组,引用不可变
private final byte coder; // ③ 编码标识不可变
private int hash; // ④ 缓存hashCode
// 没有提供修改value的方法 // ⑤ 无修改方法
}
三重保障:
- final类:防止子类破坏不可变性
- final数组:引用不可变(但数组内容理论上可变)
- 无修改方法:不暴露修改内部数据的接口
1.3 为什么设计为不可变?
| 原因 | 说明 |
|---|---|
| 线程安全 | 不可变对象天然线程安全,无需同步 |
| 缓存hashCode | 只需计算一次,HashMap性能提升 |
| 字符串常量池 | 不可变才能安全共享,节省内存 |
| 安全性 | 防止敏感信息(如密码)被篡改 |
| 类加载安全 | 类名不可变,防止恶意篡改类加载 |
1.4 "修改"String的真相
String s = "hello";
s = s + " world"; // 不是修改原对象,而是创建新对象
// 等价于
String s = "hello";
String temp = new StringBuilder(s).append(" world").toString();
s = temp; // s指向新对象,原"hello"对象不变
二、String常量池
2.1 什么是字符串常量池?
字符串常量池(String Pool)是JVM维护的一块特殊内存区域,用于存储字符串字面量,实现字符串复用。
位置变化:
- JDK 6及之前:永久代(PermGen)
- JDK 7及之后:堆内存(Heap)
2.2 字面量 vs new
// 方式1:字面量(推荐)
String s1 = "hello"; // 先查常量池,有则复用,无则创建并入池
String s2 = "hello"; // 复用常量池中的对象
System.out.println(s1 == s2); // true(同一对象)
// 方式2:new关键字
String s3 = new String("hello"); // 堆中创建新对象
String s4 = new String("hello"); // 又创建一个新对象
System.out.println(s3 == s4); // false(不同对象)
System.out.println(s1 == s3); // false(常量池 vs 堆)
2.3 new String("abc") 创建了几个对象?
String s = new String("abc");
答案:1个或2个
- 常量池中的"abc":如果常量池中没有"abc",则创建一个
- 堆中的String对象:new关键字必定在堆中创建一个新对象
// 验证
String s1 = "abc"; // 常量池创建"abc"
String s2 = new String("abc"); // 仅在堆中创建,常量池已有
System.out.println(s1 == s2); // false
System.out.println(s1 == s2.intern()); // true
2.4 intern()方法
public native String intern();
作用: 将字符串放入常量池,并返回常量池中的引用
String s1 = new String("hello"); // 堆中对象
String s2 = s1.intern(); // 返回常量池中的"hello"
String s3 = "hello"; // 常量池中的"hello"
System.out.println(s1 == s2); // false(堆 vs 常量池)
System.out.println(s2 == s3); // true(都是常量池)
2.5 JDK 6 vs JDK 7+ 的intern()差异
String s = new String("1") + new String("1"); // 堆中创建"11"
s.intern(); // 将"11"放入常量池
String s2 = "11";
// JDK 6: s == s2 → false
// JDK 7+: s == s2 → true
原因:
- JDK 6:intern()在永久代创建新对象
- JDK 7+:intern()直接将堆中对象的引用放入常量池(不复制)
2.6 字符串拼接的常量池行为
// 编译期确定
String s1 = "a" + "b" + "c"; // 编译优化为 "abc"
String s2 = "abc";
System.out.println(s1 == s2); // true
// 运行期确定
String a = "a";
String s3 = a + "bc"; // 运行时拼接,堆中创建新对象
System.out.println(s2 == s3); // false
// final修饰
final String b = "a";
String s4 = b + "bc"; // 编译期确定,等价于 "abc"
System.out.println(s2 == s4); // true
三、String常用方法
3.1 比较方法
// equals():比较内容
"hello".equals("hello"); // true
"hello".equals("Hello"); // false
// equalsIgnoreCase():忽略大小写
"hello".equalsIgnoreCase("Hello"); // true
// compareTo():字典序比较
"abc".compareTo("abd"); // -1(c < d)
"abc".compareTo("abc"); // 0
"abd".compareTo("abc"); // 1
3.2 查找方法
String s = "hello world";
s.indexOf("o"); // 4(首次出现位置)
s.lastIndexOf("o"); // 7(最后出现位置)
s.contains("world"); // true
s.startsWith("he"); // true
s.endsWith("ld"); // true
3.3 截取与替换
String s = "hello world";
s.substring(0, 5); // "hello"(左闭右开)
s.replace("l", "L"); // "heLLo worLd"
s.replaceAll("\\s", "-"); // "hello-world"(正则)
s.trim(); // 去除首尾空白
s.strip(); // JDK 11+,去除首尾空白(支持Unicode空白)
3.4 分割与连接
// 分割
"a,b,c".split(","); // ["a", "b", "c"]
// 连接(JDK 8+)
String.join("-", "a", "b", "c"); // "a-b-c"
String.join("-", Arrays.asList("a", "b", "c")); // "a-b-c"
四、高频面试题
Q1: String为什么设计成不可变的?
答:
- 线程安全:不可变对象天然线程安全
- 字符串常量池:不可变才能安全共享
- 缓存hashCode:只需计算一次,提升HashMap性能
- 安全性:防止敏感信息被篡改
- 类加载安全:类名不可变,防止恶意篡改
Q2: String、StringBuilder、StringBuffer的区别?
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是(不可变) | 否 | 是(synchronized) |
| 性能 | 拼接慢 | 最快 | 较快 |
| 使用场景 | 少量操作 | 单线程拼接 | 多线程拼接 |
Q3: String s = new String("abc") 创建了几个对象?
答: 1个或2个
- 如果常量池没有"abc":2个(常量池1个 + 堆1个)
- 如果常量池已有"abc":1个(仅堆中1个)
Q4: String的intern()方法有什么作用?
答:
- 将字符串放入常量池
- 如果常量池已存在,返回常量池中的引用
- JDK 7+:如果堆中有而常量池没有,直接将堆对象引用放入常量池
Q5: 为什么JDK 9将String底层从char[]改为byte[]?
答:
- 大部分字符串是Latin-1字符,用1字节足够
- 节省内存:Latin-1字符串内存减少50%
- 通过coder字段标识编码,按需选择存储方式
Q6: "a" + "b" + "c" 会创建几个对象?
答: 1个
- 编译器优化为 "abc"
- 直接在常量池中创建(如果不存在)
五、最佳实践
✅ 推荐做法
// 1. 优先使用字面量
String s = "hello"; // 而非 new String("hello")
// 2. 字符串比较用equals()
if (s.equals("hello")) { } // 而非 s == "hello"
// 3. 常量放前面,避免NPE
if ("hello".equals(s)) { } // s为null也不会报错
// 4. 多次拼接用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
String result = sb.toString();
❌ 避免做法
// 1. 循环中拼接字符串
String s = "";
for (int i = 0; i < 100; i++) {
s += i; // 每次创建新对象,性能差
}
// 2. 用 == 比较字符串内容
if (s == "hello") { } // 比较的是引用,不是内容
// 3. 不必要的new
String s = new String("hello"); // 浪费内存
相关链接
- 02-StringBuilder系列 - 可变字符串的使用
- 03-BigDecimal - 精确数值计算
记忆口诀
String不可变,三重保障全
final类和数组,无修改方法添
常量池省内存,字面量优先选
拼接用Builder,循环莫用加
⬅️ 类型转换规则 🏠 00-Java ➡️ StringBuilder系列
💬 评论