Android面试-为什么 Java 中的 String 被设计成不可变?

一、从一个常见现象说起

在 Java 中,String 是被使用频率最高的类之一。很多初学者都会遇到这样一个问题:既然字符串经常需要拼接、修改,为什么 JDK 还要把 String 设计成不可变(immutable)的?

先看一段代码:

String s = "hello";
s = s + " world";
System.out.println(s); // hello world

看起来 s 被“修改”了,但实际上原来的 "hello" 对象并没有变化,只是变量 s 指向了一个新的字符串对象 "hello world"。这正是不可变的含义:对象一旦创建,其内部状态就不能再被改变。

那么,这种设计到底带来了哪些好处?下面从几个关键角度来分析。

二、String 不可变的实现原理

在 JDK 8 及之前,String 内部使用 char[] 存储字符;从 JDK 9 开始,为了节省内存,改为使用 byte[] 加编码标志(Compact Strings)。无论哪种实现,核心字段都被声明为 final:

public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {

    private final byte[] value;
    private int hash; // 缓存哈希值

    // ...
}

不可变主要由三点保证:

  • 类被 final 修饰:不能被继承,避免子类破坏不可变语义。
  • 内部数组被 final 修饰且不对外暴露:外部无法拿到数组引用直接修改内容。
  • 所有“修改”方法都返回新对象:如 substring、concat、replace 等,都不会改动原对象。

需要注意的是,final 修饰引用只保证引用本身不变,并不保证数组内容不变。真正保证内容不可变的关键在于:String 没有提供任何可以修改内部数组元素的方法,也没有把数组引用泄露出去。

三、字符串常量池与内存复用

Java 中有字符串常量池(String Pool)。当我们写下字面量时,JVM 会优先从常量池中查找是否已存在相同内容的字符串:

String a = "java";
String b = "java";
System.out.println(a == b); // true

两个变量指向的是同一个对象。这种复用之所以安全,前提就是字符串不可变。如果 String 可变,那么通过 a 修改内容后,b 也会受到影响,整个常量池的共享机制就会崩溃。

不可变让常量池可以放心地共享对象,从而节省大量内存,也减少了对象创建的开销。

四、哈希值可以安全缓存

String 是 HashMap、HashSet 等集合中最常用的键类型。哈希表的正常工作依赖于:对象作为键期间,其哈希值不能发生变化。

由于 String 不可变,它的哈希值在创建后就是固定的,因此可以在第一次计算后缓存起来:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        // 计算哈希
        hash = h;
    }
    return h;
}

这样既保证了哈希值稳定,又避免了重复计算,提升了集合操作的性能。如果 String 可变,那么把它作为 HashMap 的 key 后一旦内容改变,哈希值就会变化,导致再也无法正确查找到该键值对。

五、线程安全与并发友好

不可变对象天生是线程安全的。多个线程可以同时读取同一个 String 对象,而不需要任何同步措施,因为没有任何线程能够修改它的状态。

这在并发场景下意义重大:

  • 省去了加锁的开销,读取性能更高。
  • 避免了因共享可变状态导致的数据竞争问题。
  • 可以安全地在多线程之间传递和共享字符串。

可以说,String 的不可变设计是 Java 并发编程中“不可变对象”思想的典型范例。

六、安全性保障

字符串在 Java 中被广泛用于敏感场景,例如类名、方法名、文件路径、网络地址、数据库连接串、用户名密码等。

如果 String 可变,攻击者就可能在某些环节篡改字符串内容,从而绕过安全检查。例如:

  • 类加载时使用的类名被篡改,可能加载恶意类。
  • 文件路径被修改,可能访问到未授权文件。
  • 网络连接地址被替换,可能导致请求被劫持。

不可变保证了这些关键字符串在传递过程中不会被悄悄修改,从而提升了系统的整体安全性。

七、不可变带来的代价

任何设计都有取舍,String 不可变也并非没有成本:

  • 频繁拼接会产生大量临时对象:每次 + 操作都会创建新字符串,可能带来 GC 压力。
  • 需要额外的可变替代类:因此 JDK 提供了 StringBuilder 和 StringBuffer 用于可变字符串操作。

所以在循环中拼接字符串时,推荐使用 StringBuilder:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

编译器也会对简单的字符串拼接做优化,但循环场景下显式使用 StringBuilder 仍然是更好的选择。

八、总结

String 被设计成不可变,是综合了内存、性能、并发和安全等多方面因素的结果。可以归纳为以下几点:

设计原因 说明
常量池复用 相同字面量共享同一对象,节省内存
哈希值缓存 哈希值稳定,适合作为集合的键
线程安全 不可变对象天然并发安全,无需加锁
安全性 关键字符串不会被中途篡改
实现简单 语义清晰,减少出错可能

理解 String 不可变的背后逻辑,不仅能帮助我们写出更高效的代码,也能加深对 Java 设计思想的理解。当需要频繁修改字符串时,记得选择合适的可变替代类,做到扬长避短。

使用小程序,查看更多面试题目,随时随地刷题,刷题更方便!
code2

posted @ 2026-11-10 17:11  自由侠  阅读(3)  评论(0)    收藏  举报