问题

String为什么设计成不可变的?

答案

1. 核心概念

String的不可变性(Immutability)是指:一旦String对象被创建,其内部存储的字符序列就无法被修改。任何”修改”操作都会返回一个新的String对象。

这是Java语言设计中的重要决策,带来了多方面的优势。

2. 五大设计原因

原因1:安全性(Security)

网络连接安全:

// 假设String是可变的,会引发安全问题
public void connect(String host) {
    checkPermission(host);  // 检查权限:允许连接到 "safe.com"
    // 如果String可变,恶意代码可能在此处修改host
    // host 被篡改为 "malicious.com"
    doConnect(host);  // 实际连接到被篡改的地址!
}

文件路径安全:

File file = new File("/safe/path/file.txt");
// 如果String可变,路径可能被修改为"/etc/passwd"
// 导致权限检查被绕过

反射安全:

Class<?> clazz = Class.forName(className);
// className必须不可变,否则可能加载恶意类

String作为方法参数、网络协议、文件路径等关键场景的数据类型,不可变性是安全的基石

原因2:线程安全(Thread Safety)

// 不可变对象天然线程安全,无需同步
public class UserService {
    private final String apiKey = "secret_key_12345";

    public void request() {
        // 多个线程同时访问apiKey,无需加锁
        sendRequest(apiKey);
    }
}

优势:

  • 无需synchronized、Lock等同步机制
  • 避免了竞态条件(Race Condition)
  • 可安全地在多线程间共享

如果String可变,每次访问都需要加锁,性能开销巨大。

原因3:支持字符串常量池(String Pool)

String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);  // true - 指向同一对象

String s3 = new String("hello");
String s4 = s3.intern();  // 返回常量池中的引用
System.out.println(s1 == s4);  // true

常量池机制依赖不可变性:

  • JVM在常量池中只保存一份相同字符串的副本
  • 多个引用指向同一对象,节省内存
  • 如果String可变,修改一个引用会影响所有引用,破坏常量池语义

内存优化效果:

// 假设系统中有10000个"SUCCESS"字符串
// 不可变 + 常量池:仅占用1份内存
// 可变:需要10000份独立对象,内存浪费巨大
for (int i = 0; i < 10000; i++) {
    String status = "SUCCESS";  // 都指向同一对象
}

原因4:缓存HashCode(Performance)

// String源码中的hashCode缓存
public final class String {
    private int hash;  // 默认为0

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = calculateHashCode();  // 只计算一次
        }
        return h;
    }
}

性能提升:

HashMap<String, User> userMap = new HashMap<>();
String key = "user123";

// 第一次使用:计算hashCode
userMap.put(key, user);

// 后续使用:直接返回缓存的hash值
userMap.get(key);  // 无需重新计算
userMap.containsKey(key);  // 无需重新计算

适用场景:

  • HashMap/HashSet作为key(最常见)
  • 频繁进行equals比较的场景
  • 大量字符串查找操作

如果String可变,每次修改后hashCode都会变化,无法缓存,且会导致HashMap等数据结构失效。

原因5:便于设计和优化(Design & Optimization)

字符串常量编译期优化:

String s = "a" + "b" + "c";
// 编译器优化为:
String s = "abc";  // 直接从常量池获取

String Pool共享优化:

// JVM内部优化:字面量自动入池
String s1 = "hello";
String s2 = new String("hello").intern();
// 两者指向同一对象,节省内存

便于并发编程:

// 不可变对象可以作为安全发布的共享数据
public class Config {
    private static final String DB_URL = "jdbc:mysql://localhost:3306/db";
    // 无需volatile,所有线程看到的都是同一个不可变对象
}

3. 不可变性的权衡

优点总结

  • ✅ 天然线程安全
  • ✅ 可安全共享和缓存
  • ✅ 支持常量池,节省内存
  • ✅ 适合作为HashMap的key
  • ✅ 防止恶意篡改,提高安全性

缺点与解决方案

  • ❌ 频繁修改产生大量临时对象
  • ✅ 解决方案:使用StringBuilder/StringBuffer
// 错误做法:循环拼接String
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // 产生1000个临时对象
}

// 正确做法:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);  // 在原对象上修改
}
String result = sb.toString();

4. 源码体现

public final class String  // final类,不能被继承
    implements java.io.Serializable, Comparable<String>, CharSequence {

    // JDK 8
    private final char[] value;  // final数组,引用不可变

    // JDK 9+
    private final byte[] value;  // final数组
    private final byte coder;    // 编码标识

    // 没有提供修改内部数组的公开方法
    // 所有"修改"方法都返回新对象
    public String substring(int beginIndex) {
        // ...
        return new String(value, beginIndex, subLen);  // 返回新对象
    }

    public String concat(String str) {
        // ...
        return new String(buf, true);  // 返回新对象
    }
}

5. 实际应用场景

适合使用String的场景

// 1. 配置常量
public static final String API_KEY = "your-api-key";

// 2. HashMap的key
Map<String, Object> cache = new HashMap<>();

// 3. 方法参数(需要保证数据不被修改)
public void sendEmail(String recipient, String subject) {
    // 调用方无法修改传入的字符串
}

// 4. 多线程共享数据
private final String sharedData = "readonly";

不适合使用String的场景

// 大量字符串拼接 - 用StringBuilder
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM users WHERE ");
for (String condition : conditions) {
    sql.append(condition).append(" AND ");
}

// 频繁修改的文本编辑器 - 用StringBuffer(多线程)或StringBuilder

6. 面试答题要点

标准回答结构:

  1. 安全性:防止恶意修改(网络连接、文件路径、类加载)
  2. 线程安全:天然线程安全,无需同步,可安全共享
  3. 常量池:支持字符串常量池,节省内存,提升性能
  4. 缓存hashCode:适合作为HashMap的key,hashCode只需计算一次
  5. 设计简洁:便于编译器优化和并发编程

加分点:

  • 提到Java安全模型和权限检查
  • 说明不可变对象的”发布安全性”
  • 对比可变类(如StringBuilder)的适用场景
  • 了解String在JDK 9中的改进(byte[]存储)

7. 总结

String的不可变设计是安全性、性能和便利性的完美平衡:

  • 安全优先:防止篡改,保证系统安全
  • 性能优化:常量池复用、hashCode缓存
  • 编程简化:线程安全、无需防御性拷贝

虽然不可变带来了”修改”操作的性能开销,但通过StringBuilder/StringBuffer可以轻松解决。总体来说,不可变设计的收益远大于成本,是Java语言最成功的设计之一。