前言:关于“String 到底能不能变”的深夜辩论

坐标:学校图书馆(空调滴水,我心冰冷)

状态:键盘磨损度 +1%,咖啡因浓度 80%。

碎碎念: 兄弟们,今天下午我在那敲代码,隔壁桌哥们突然问我:“哎,Java 的 String 为什么不可变啊?” 我当时想都没想,脱口而出:“因为它被 final 修饰了啊!”

结果那哥们冷笑一声,直接翻开源码怼到我脸上:“final 只能保证引用不变,或者类不能被继承,它能保证里面的数组不被改吗?”

卧槽,我当时就愣住了。回来钻研了一晚上,才发现这事儿远没我想的那么简单。今天咱就来深度解剖一下 String 的“钢铁直男”属性——不可变性。

🚀 目录

  1. 真相大白:String 到底是怎么变“硬”的?

    • 1.1 源码三部曲:不仅仅是 final

    • 1.2 这里的 char[] 藏着猫腻

  2. 灵魂拷问:为什么要费劲心思让它不可变?

    • 2.1 字符串常量池(String Pool)的刚需

    • 2.2 线程安全:天生的“冷淡风”

    • 2.3 性能优化:Hash 值的“一劳永逸”

    • 2.4 安全防火墙:防止 SQL 注入与路径篡改

  3. 骚操作:用“反射”强行掰弯 String

  4. 不可变的代价:为什么要有 StringBuilder?

  5. 核心对比:String vs StringBuffer vs StringBuilder

  6. 结语:不可变不是僵化,是保护

1. 真相大白:String 到底是怎么变“硬”的?

很多人(比如之前的我)以为 String 不可变是因为类前面加了 final。但其实,这只是第一层防御。

1.1 源码三部曲

打开 JDK 8 源码(JDK 9 以后变成了 byte[],但逻辑一致),你会发现 String 的防御是“全方位”的:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 1. 数组被私有化了,外面看不到
    // 2. 被 final 修饰了,数组的引用地址不能改
    private final char value[]; 

    // ...
}

1.2 这里的坑在哪里?

  • final 修饰类:说明 String 没有子类,防止别人通过继承来恶意重写方法。

  • private final char[]:这里最阴险!final 只能保证这个 value 变量不再指向别的数组,但它拦不住你修改数组里面的元素(比如 value[0] = 'a')。

  • 真正的关键:String 类里面没有任何 setter 方法!它提供的所有操作方法(比如 replace, substring, toLowerCase)底层都是 new 了一个新的 String 对象并返回。

结论:String 的不可变性,是靠 final 类、私有成员变量,以及“绝不提供修改接口”这三连招共同达成的。

2. 灵魂拷问:为什么要费劲心思让它不可变?

设计者不傻,搞这么麻烦肯定是有巨大收益的。

2.1 字符串常量池(String Pool)的刚需

这是最重要的原因。 Java 为了省内存,搞了个常量池。如果你写 String s1 = "Java", String s2 = "Java",它们其实指向的是内存里的同一个地址。

  • 大学生比喻:如果 String 是可变的,你把 s1 改成了 Python,那 s2 也跟着变了,这不乱套了吗?这就好比你改了自己的外卖订单,结果全宿舍的饭都跟着变了。

2.2 线程安全

因为 String 是不可变的,所以它在多线程环境下是绝对安全的。 你不需要给它加任何锁,随便多少个线程同时访问它,它都稳如泰山。这大大节省了系统开销。

2.3 性能优化:Hash 值的“一劳永逸”

String 经常被拿来当 HashMap 的 Key。 因为 String 不可变,所以它的 hashCode 只需要计算一次,然后就可以缓存起来(源码里有个 hash 字段)。下次再用直接取,性能飞快。如果 String 能变,那 Map 的数据结构就全毁了。

2.4 安全防火墙

这是最硬核的一点。 Java 的很多关键参数都是 String,比如数据库连接的 URL、服务器的 IP、文件的 Path。 如果 String 是可变的,黑客就可以在你的程序通过权限校验之后,悄悄把路径改掉。

3. 骚操作:用“反射”强行掰弯 String

虽然 Java 官方不建议,但咱们程序员总有“骚操作”。 通过反射(Reflection),我们可以强行突破 private 的限制,修改 String 内部的值。

String s = "Hello";
// 1. 获取 String 类的 value 字段
Field valueField = String.class.getDeclaredField("value");
// 2. 强行开启访问权限
valueField.setAccessible(true); 
// 3. 拿到那个“私有”的数组
char[] v = (char[]) valueField.get(s);
// 4. 强行改它!
v[0] = 'W';

System.out.println(s); // 输出:Wello!它居然变了!

警告:这种操作会破坏字符串常量池。如果你改了 "Hello",那么全程序里所有的 "Hello" 都会变成 "Wello"。千万别在生产代码里这么干,会被开除的!

4. 不可变的代价:为什么要有 StringBuilder?

万物皆有代价。String 的不可变导致了一个大问题:产生大量临时对象。 如果你在循环里用 + 拼接字符串:

String s = "";
for(int i=0; i<100; i++) {
    s += i; // 每次循环都会 new 一个对象,GC 忙得满头大汗
}

所以,当需要频繁修改字符串时,务必使用 StringBuilderStringBuffer

5. 核心对比:三兄弟大乱斗

特征

String

StringBuffer

StringBuilder

可变性

不可变

可变

可变

线程安全

安全

安全 (synchronized)

不安全

性能

最慢

中等

最快

建议

少量操作

多线程环境

单线程频繁操作

6. 结语

写完这篇,图书馆的闭馆音乐已经响起了。 String 为什么不可变?不仅仅是一个语法设定,更是 Java 在内存、安全、性能之间做出的终极权衡。

作为大学生的我们,以后面试再被问到这题,把常量池、线程安全、Hash 缓存和反射这些点全甩出来,保证面试官对你刮目相看。

最后,祝大家:

  • String 一样坚定不移(守住底线)。

  • StringBuilder 一样灵活多变(适应环境)。

  • 所有的 Bug 都能被一键修复!

如果你也被“反射”修改 String 的代码惊到了,点个赞支持一下!咱们下期聊聊String, StringBuilder,和StringBuffer

更多推荐