乐于分享
好东西不私藏

Java面试必问:字符串为什么不可变?源码+实例一次性讲透

Java面试必问:字符串为什么不可变?源码+实例一次性讲透

🔥 面试官:String 为什么是不可变的?

大家好,我是云扬~
做 Java 开发的同学,几乎都被面试官问过这个问题:“String 为什么是不可变的?”
看似基础,实则考察你对源码设计、内存模型的理解。今天就用最通俗的语言,搭配可直接运行的代码实例,把这个知识点彻底讲透 —— 不管是面试还是实际开发,都能帮你少踩坑!

📌 先看源码:String 不可变的核心设计

要搞懂 “不可变”,先打开 JDK 源码,String 类的核心定义其实很简单:
// 1. 类被 final 修饰 → 不能被继承public final class String    implements java.io.Serializable, Comparable    // 2. 存储字符串的字符数组:private + final    private final char value[];    // 哈希值缓存(后面会讲作用)    private int hash; // Default to 0    // 构造方法:初始化字符数组    public String(String original) {        this.value = original.value;        this.hash = original.hash;    }}
划重点!这两个设计直接决定了 String 的不可变性:
  1. 类被 final 修饰:String 不能被继承,子类没法重写方法篡改逻辑;
  2. char 数组 private + final
    1. private:外部不能直接操作这个数组(比如修改某个字符);
    2. final:数组的引用地址一旦确定,就不能指向新的数组。
简单说:String 对象创建后,底层存储的字符内容和引用,都没法被修改—— 这就是 “不可变性” 的本质。

🤔 为什么要设计成不可变?3 个关键原因

1. 安全!避免敏感数据被篡改
日常开发中,字符串常用来存密码、密钥、接口地址等敏感信息。如果 String 是可变的,可能出现致命问题:
比如你传一个密码给验证方法,结果方法里偷偷把密码改了,后续逻辑全乱了!
代码实例:验证不可变的安全性
public class SecurityDemo {    public static void main(String[] args) {        String password = "yunyang_123456"// 原始密码        checkPassword(password);        System.out.println("最终密码:" + password); // 输出:yunyang_123456    }    private static void checkPassword(String pwd) {        // 看似修改,实则创建新对象        pwd = pwd.replace("123456""xxxxxx");     }}
因为 String 不可变,checkPassword 里的 “修改” 其实是创建了新对象,原始密码完全没变化 —— 这就保证了数据安全。
2. 高效!缓存哈希值,提升集合性能
String 经常作为 HashMap、HashSet 的键(Key),而哈希表的查找全靠 hashCode(哈希值)。
如果 String 是可变的,每次修改后哈希值都会变,集合就找不到原来的元素了!
所以 String 设计了 “哈希值缓存”:首次计算 hashCode 后,就存在 hash 字段里,后续直接复用 —— 大大提升哈希表的查询效率。
代码实例:哈希值缓存验证
public class HashDemo {    public static void main(String[] args) {        String s1 = "abc";        String s2 = "abc";        // 相同内容的字符串,哈希值相同且复用缓存        System.out.println(s1.hashCode()); // 96354        System.out.println(s2.hashCode()); // 96354(直接用缓存,不重复计算)        // 新对象重新计算哈希值        String s3 = new String("abc");        System.out.println(s3.hashCode()); // 96354(内容相同,哈希值也相同)    }}
3. 省内存!字符串常量池的核心前提
Java 有个 “字符串常量池”(String Constant Pool),核心逻辑是:相同内容的字符串,只存一份,多个变量共享引用
比如你写 String a = “hello” 和 String b = “hello”,a 和 b 指向的是同一个对象 —— 这样就不用重复创建,省了大量内存。
而这一切的前提,就是 String 不可变!如果可变,a 修改了字符串内容,b 的值也会跟着变,直接乱套~
代码实例:常量池复用验证
public class ConstantPoolDemo {    public static void main(String[] args) {        String a = "hello"// 从常量池获取(无则创建)        String b = "hello"// 共享 a 的引用        String c = new String("hello"); // 新建对象,不进常量池        System.out.println(a == b); // true(同一对象)        System.out.println(a == c); // false(不同对象)        System.out.println(a.equals(c)); // true(内容相同)    }}

⚠️ 易错点:String 的 “修改” 其实是创建新对象

很多同学会疑惑:“我明明用了 substring、concat 方法修改字符串,怎么回事?”
其实这些方法并没有修改原对象,而是创建了新的 String 对象—— 原对象始终不变!
代码实例:“修改” 本质是新建对象
public classModifyDemo{    public static void main(String[] args) {        String original = "hello world";        // 1. 截取 substring()        String sub = original.substring(6);        System.out.println("原字符串:" + original); // hello world(不变)        System.out.println("截取后:" + sub); // world(新对象)        // 2. 拼接 concat()        String concat = original.concat("!");        System.out.println("原字符串:" + original); // hello world(不变)        System.out.println("拼接后:" + concat); // hello world!(新对象)    }}
记住:String 没有真正的 “修改” 方法,所有看似修改的操作,都是返回新对象

📝 实用技巧:频繁修改字符串用什么?

如果需要频繁修改字符串(比如循环拼接),直接用 String 会创建大量临时对象,性能很差!
这时建议用 StringBuilder(线程不安全,效率高)或 StringBuffer(线程安全,效率稍低)—— 它们的底层是可变字符数组,修改时不会创建新对象。
对比示例:
// 不推荐:循环中用 String 拼接(创建大量临时对象)String str = "";for (int i = 0; i 00; i++) {    str += i; }// 推荐:用 StringBuilder 拼接StringBuilder sb = new StringBuilder();for (int i = 0; i < 1000; i++) {    sb.append(i);}String result = sb.toString();

🌟 总结

String 的不可变性,是 Java 最经典的设计之一:
  • 底层依赖「final 类 + final 字符数组」;
  • 带来 3 大好处:安全(防篡改)、高效(缓存哈希)、省内存(常量池);
  • 易错点:“修改” 其实是新建对象,频繁修改用 StringBuilder/StringBuffer。
互动时间
你在面试或开发中,遇到过哪些 String 相关的坑?欢迎在评论区留言讨论~
如果觉得这篇文章有用,别忘了点赞 + 在看 + 转发给身边的小伙伴呀~