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的方法      // ⑤ 无修改方法
}

三重保障:

  1. final类:防止子类破坏不可变性
  2. final数组:引用不可变(但数组内容理论上可变)
  3. 无修改方法:不暴露修改内部数据的接口

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个

  1. 常量池中的"abc":如果常量池中没有"abc",则创建一个
  2. 堆中的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为什么设计成不可变的?

答:

  1. 线程安全:不可变对象天然线程安全
  2. 字符串常量池:不可变才能安全共享
  3. 缓存hashCode:只需计算一次,提升HashMap性能
  4. 安全性:防止敏感信息被篡改
  5. 类加载安全:类名不可变,防止恶意篡改

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");  // 浪费内存

相关链接


记忆口诀

String不可变,三重保障全
final类和数组,无修改方法添
常量池省内存,字面量优先选
拼接用Builder,循环莫用加

⬅️ 类型转换规则 🏠 00-Java ➡️ StringBuilder系列