返回首页面试题

面试必备:String 为什么是不可变的?

2026年03月25日7 min read

面试必备:String 为什么是不可变的?

先搞懂基本概念

什么是不可变对象?

一旦创建,对象的状态(属性值)不能被改变。

String s = "hello";
s = "world";  // 这不是改变原对象,而是创建了新对象

表面上 s 变了,但 "hello" 这个字符串本身从未改变。


String 的底层结构

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    // JDK 9 之后使用 byte[] 存储
    private final byte[] value;
    
    // 编码标识
    private final byte coder;
    
    // 缓存的 hash 值
    private int hash;
    
    // ...
}

JDK 8 vs JDK 9+

版本存储结构说明
JDK 8char[] value每个字符占 2 字节
JDK 9+byte[] value + coderLatin-1 用 1 字节,节省内存

为什么 String 是不可变的?

1. class 层面:final 修饰

public final class String { ... }

final 修饰类,表示类不能被继承,防止子类修改行为。

2. 存储层面:value 是 final 的

private final byte[] value;

value 数组的引用不能改变,但这不意味着数组内容不可变?

关键点:虽然数组内容理论上可以修改,但 String 类没有提供任何修改 value 的公开方法。

3. 方法层面:不提供修改方法

// String 类中没有这些方法
// public void setChar(int index, char c) {}
// public void append(String s) {}
// ...

所有"修改"操作都返回新对象

String s1 = "hello";
String s2 = s1.toUpperCase();  // 返回新对象 "HELLO"

s1  // 仍是 "hello",未被改变
s2  // 是新对象 "HELLO"

不可变性的好处

1. 线程安全

不可变对象天然线程安全,不需要同步:

String s = "hello";
// 多个线程可以安全地共享这个 String,不需要加锁

2. 安全性

// 数据库连接、文件路径、网络地址都依赖 String
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db");

// 如果 String 可变,恶意代码可能修改连接字符串,造成安全漏洞

3. 字符串常量池

String s1 = "hello";  // 在常量池中创建
String s2 = "hello";  // 复用已有的,不创建新对象

System.out.println(s1 == s2);  // true,指向同一个对象

不可变性是字符串常量池工作的基础

  • 如果 String 可变,两个引用指向同一个对象,一个改了,另一个也变了
  • 缓存共享就变得危险

4. 哈希缓存

// String 的 hashCode 被缓存了
private int hash;

// 因为不可变,所以可以放心缓存
public int hashCode() {
    int h = hash;
    return (h != 0) ? h : (hash = computeHashCode());
}

5. 作为 HashMap/HashSet 的 key

Map<String, Integer> map = new HashMap<>();
map.put("hello", 1);

// 如果 String 可变,修改后会导致:
// 1. 存入的桶位置和查询的桶位置不一致
// 2. 永远找不到刚才放进去的值

字符串常量池

创建字符串的两种方式

// 方式1:字面量(使用常量池)
String s1 = "hello";
String s2 = "hello";
s1 == s2  // true

// 方式2:new 对象(不使用常量池)
String s3 = new String("hello");
String s4 = new String("hello");
s3 == s4  // false
s3 == s1  // false

图解

常量池:
┌─────────┐
│ "hello" │ ← s1, s2 都指向这里
└─────────┘

堆内存:
┌─────────────────┐  ┌─────────────────┐
│ new String()    │  │ new String()    │
│ s3 → │         │  │ s4 → │         │
└─────────────────┘  └─────────────────┘

intern() 方法

String s1 = new String("hello");  // 堆中
String s2 = s1.intern();           // 尝试放入常量池,返回池中的引用

System.out.println(s1 == s2);  // false
System.out.println(s2 == "hello");  // true

String、StringBuilder、StringBuffer 对比

可变性线程安全性能
String不可变安全每次修改都创建新对象,较慢
StringBuilder可变不安全直接修改,少量对象,最快
StringBuffer可变synchronized有锁,较慢

使用场景

// 字符串不常变化 → String
String name = "张三";

// 单线程字符串拼接 → StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");

// 多线程字符串拼接 → StringBuffer
StringBuffer sb = new StringBuffer();

编译器的优化

// 这种情况,编译器会优化成 StringBuilder
String s = "a" + "b" + "c";

// 编译后相当于
String s = new StringBuilder().append("a").append("b").append("c").toString();

面试高频问题

Q1:String s = new String("hello") 创建了几个对象?

两个

  1. 字符串常量池中的 "hello"
  2. 堆内存中的 new String() 对象

Q2:String 有 length() 方法,数组有 length 属性,为什么?

  • 数组是语言层面的设计,length 是属性
  • String 是类,length() 是方法(因为还有 charAt() 等配套方法)

Q3:能不能继承 String?为什么?

不能。因为 String 是 final 类,不允许被继承。


总结

String 不可变的原因:

  1. final 修饰 class 和 value 数组
  2. 不提供修改方法
  3. 好处:线程安全、字符串常量池、哈希缓存、安全性

记住一句话:不可变是设计选择,不是语言限制,Java 8 的 String 就是这么设计的。

评论区