String 与字符串常量池 | JavaSE
String 与字符串常量池
一、学习目标
学完本章,你应该能够:
- 能够解释
String是什么,以及为什么它属于引用数据类型。 - 能够使用字符串字面量和构造器创建字符串对象。
- 能够解释 String 不可变(Immutable)的真正含义。
- 能够区分字符串变量改变与字符串对象内容改变。
- 能够解释字符串字面量为什么可以共享对象。
- 能够判断常见字符串代码中的
==比较结果。 - 能够区分
==与equals()在 String 比较中的基本语义。 - 能够理解
String.intern()与字符串池的基本关系。 - 能够避免“所有字符串都在字符串常量池”“只要写
+就一定创建新对象”等过度简化结论。
二、核心知识
2.1 String 是什么
String 是 Java 中表示字符串的核心类。
例如:
String name = "LingXi";
String language = "Java";
String siteName = "星雨笔录";
这些变量引用的对象都是:
java.lang.String
类型。
String 位于:
java.lang
包。
因此通常不需要手动:
import java.lang.String;
2.2 String 是引用数据类型
Java 数据类型可以从基础阶段分为:
数据类型
│
├── 基本数据类型
│ ├── byte
│ ├── short
│ ├── int
│ ├── long
│ ├── float
│ ├── double
│ ├── char
│ └── boolean
│
└── 引用数据类型
└── String
所以:
String text = "hello";
不是说变量 text 本身直接“就是字符串对象”。
更准确的对象模型是:
text
│
│ reference
↓
String 对象 "hello"
这也是后面理解:
==
为什么容易出错的前提。
2.3 字符串字面量本身就是 String 对象
Java 代码:
String s = "abc";
其中:
"abc"
叫做字符串字面量(String Literal)。
它的类型就是:
String
因此甚至可以直接调用方法:
System.out.println("abc".length());
这里并不是:
"abc"先变成某种特殊基本类型,再转成 String。
而是:
字符串字面量求值后得到 String 实例的引用。
2.4 创建 String 的两种基本思路
方式一:字符串字面量
String s1 = "hello";
这是最常见、最推荐的普通字符串创建方式。
方式二:构造器
例如:
String s2 = new String();
创建空字符串对象。
还可以:
String s3 = new String("hello");
或者:
char[] chars = {'J', 'a', 'v', 'a'};
String s4 = new String(chars);
再例如:
byte[] bytes = {65, 66, 67};
String s5 = new String(bytes);
不过 byte[] 到 String 涉及字符集与解码问题,后面的字符编码章节会进一步学习。
2.5 实际开发中是否应该写 new String("abc")
大多数普通情况没有必要写:
String name = new String("LingXi");
直接:
String name = "LingXi";
通常更加自然。
因为:
new String(...)
明确要求构造一个新的 String 对象,而字符串字面量本身已经能够表示这个字符串。
因此:
new String("abc")
在很多基础业务代码中属于没有必要的额外对象创建。
2.6 String 是不可变对象
JDK 对 String 的一个核心设计是:
String 对象创建以后,其值不能被修改。
例如:
String s = "Java";
假设执行:
s = s + "SE";
最后:
System.out.println(s);
输出:
JavaSE
初学者很容易得出:
“原来的
String对象从 Java 被修改成 JavaSE 了。”
其实不是。
正确模型:
第一步
s
↓
"Java"
执行:
s = s + "SE";
后可以理解成:
原对象
"Java"
新结果
"JavaSE"
s
↓
"JavaSE"
发生变化的是:
引用变量
s最终指向哪个字符串对象。
而不是:
原来的
"Java"对象被修改。
2.7 “变量可变”和“对象可变”必须分开
下面代码完全合法:
String s = "A";
s = "B";
s = "C";
因此不能说:
String 变量不能改。
变量 s 当然可以重新赋值。
String 不可变真正说的是:
一个已经创建的 String 对象所表示的字符序列不会被原地修改。
例如:
"A"
这个 String 对象不会因为:
s = "B";
而自身变成:
"B"
只是 s 改为引用另一个对象。
2.8 为什么 String 设计为不可变
不可变性带来一个非常重要的能力:
String 对象可以安全共享。
例如:
String a = "hello";
String b = "hello";
如果字符串对象可以被某一方直接修改:
a 修改 hello
↓
b 看到的内容也被一起修改
共享就会变得非常危险。
因为 String 不可变:
共享 "hello"
↓
任何引用都不能把这个对象原地改掉
所以相同字符串内容可以安全复用。
这也是字符串池设计能够发挥价值的重要基础。
2.9 什么是字符串池
Java 语言会对字符串字面量进行 intern,使相同字符串值能够共享规范化(canonical)实例。
初学时可以使用“字符串池”这个概念理解:
String Pool
"Java"
"hello"
"abc"
...
当代码中多次出现相同的字符串字面量:
String a = "abc";
String b = "abc";
二者可以引用同一个规范化 String 实例。
因此:
System.out.println(a == b);
结果为:
true
2.10 == 比较引用
对于引用类型:
a == b
主要判断:
两个引用是否指向同一个对象。
例如:
String a = "abc";
String b = "abc";
System.out.println(a == b);
由于相同字符串字面量被 intern:
a ──┐
├──> "abc"
b ──┘
因此:
a == b
→ true
2.11 new String("abc") 又发生了什么
观察:
String a = "abc";
String b = new String("abc");
可以建立基础模型:
a
↓
interned "abc"
b
↓
new String object
内容也是 "abc"
因此:
System.out.println(a == b);
通常结果是:
false
因为:
内容一样,并不代表两个引用指向的是同一个对象。
2.12 两个 new String
例如:
String a = new String("abc");
String b = new String("abc");
new 明确表示创建新的类实例。
因此:
System.out.println(a == b);
结果是:
false
可以抽象成:
a → String 对象 #1 → "abc"
b → String 对象 #2 → "abc"
对象不同。
2.13 == 和 equals 的核心区别
对于 String:
==
关注:
是否是同一个对象引用。
而:
equals()
用于比较 String 所表示的字符内容是否相同。
例如:
String a = "abc";
String b = new String("abc");
System.out.println(a == b);
System.out.println(a.equals(b));
结果:
false
true
可以记成:
==
问:
“是不是同一个对象?”
equals()
问:
“字符串内容是否相等?”
因此业务中的:
用户名比较
密码比较
验证码比较
状态文本比较
通常关注字符串内容,而不是引用身份。
三、使用方法
3.1 字符串字面量对象复用
观察:
public class StringDemo {
public static void main(String[] args) {
String a = "Java";
String b = "Java";
System.out.println(a == b);
}
}
结果:
true
原因:
相同字符串字面量
↓
intern
↓
共享规范化实例
3.2 new String 与字面量比较
public class StringDemo {
public static void main(String[] args) {
String a = "Java";
String b = new String("Java");
System.out.println(a == b);
System.out.println(a.equals(b));
}
}
结果:
false
true
这是 String 初学阶段最重要的代码模型之一。
3.3 字符串拼接不能只背结论
观察:
String a = "hello";
String b = "hel" + "lo";
System.out.println(a == b);
结果:
true
为什么?
因为:
"hel" + "lo"
可以作为编译期常量表达式计算。
编译器可以在编译阶段得到:
"hello"
这个结果。
因此不要死记:
“只要 String 使用
+,就一定会产生不同对象。”
这是错误的。
3.4 运行时拼接
再看:
String a = "hello";
String part = "lo";
String b = "hel" + part;
System.out.println(a == b);
这里:
part
是普通变量。
表达式:
"hel" + part
需要运行时完成字符串连接。
因此得到的运行时连接结果并不是简单等同于编译期 interned 字面量引用。
在 JDK 21 环境下:
a == b
结果为:
false
3.5 对比三个经典表达式
String a = "hello";
String b = "hello";
String c = "hel" + "lo";
String part = "lo";
String d = "hel" + part;
String e = new String("hello");
对比:
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == d);
System.out.println(a == e);
结果:
true
true
false
false
理解模型:
a == b
→ 相同字面量
a == c
→ 编译期常量表达式
a == d
→ 运行时拼接
a == e
→ new 创建新对象
这一组代码比单纯背:
字面量池
new 堆
更接近 Java 语言真正的规则。
四、原理与进阶
4.1 字符串池真正的关键字:intern
字符串池的核心概念可以理解为:
对相同字符串内容维护一个规范化 String 实例。
String 提供:
intern()
方法。
例如:
String a = "hello";
String part = "lo";
String b = "hel" + part;
System.out.println(a == b);
System.out.println(a == b.intern());
在这一案例中:
false
true
b.intern() 得到:
与该字符串内容对应的规范化引用。
4.2 String.intern() 的基本模型
假设:
String s = new String("Java");
然后:
String canonical = s.intern();
intern() 关注的不是:
“复制一个新的字符串。”
而是寻找或建立:
与这个字符串内容相对应的 canonical representation。
因此:
"Java".intern() == "Java"
成立。
4.3 字符串池不等于“所有 String 对象”
下面对象:
new String("abc")
和:
"abc"
不能因为内容一样就直接认为:
它们永远是同一个对象。
实际上:
new
会创建新的 String 实例。
字符串池解决的是:
字符串字面量及被 intern 的字符串规范化共享问题。
它不是:
JVM 中所有 String 对象的唯一存储区域。
4.4 不要把“常量池”混成一个概念
学习 JVM 时你还会遇到:
class 文件常量池
运行时常量池
字符串 intern 池
这些概念相关,但不是完全同一个东西。
本章只需要建立:
字符串字面量
↓
String 实例
↓
intern / canonical representation
↓
相同字符串可以共享引用
至于 class 文件常量池的结构、运行时常量池等 JVM 底层内容,不需要在 JavaSE String 入门阶段无限下钻。
4.5 String 为什么声明为 final
String 类本身是:
public final class String
这意味着普通开发者不能通过:
class MyString extends String
继承 String。
这与 String 整体不可变设计相配合,使其行为更加稳定。
不过需要注意:
final class本身并不自动意味着“对象一定不可变”。
String 的不可变性来自其整体 API 和内部设计,而不是仅仅因为类写了 final。
4.6 不可变不等于“没有任何性能问题”
String 可以安全共享,非常适合:
- 配置文本;
- 用户名;
- URL;
- JSON 字段名;
- Map key;
- 日志文本;
- 常量字符串。
但是如果不断:
s = s + "a";
s = s + "b";
s = s + "c";
每次都要产生新的字符串结果。
大量循环拼接时效率可能较低。
因此后续 03-03 会专门学习:
StringBuilder
解决大量可变字符串构建问题。
五、实践应用
5.1 用户登录为什么不能使用 ==
假设:
String correctName = "admin";
String inputName = scanner.next();
不能依赖:
correctName == inputName
因为业务真正想比较的是:
字符内容
而不是:
是否恰好引用同一个 String 对象
应该按照字符串内容比较语义处理。
完整的 equals()、equalsIgnoreCase() 等 API 会在下一章展开。
5.2 字符串不可变为什么适合作为 Map Key
以后学习 HashMap 时会经常看到:
Map<String, User>
String 作为 key 极其常见。
其中一个重要原因就是:
String 的内容不会在对象创建后突然被原地修改。
这对稳定的:
equals
hashCode
语义非常重要。
相关哈希表机制会在集合框架章节完整学习。
5.3 常量文本可以安全共享
例如:
public static final String SUCCESS = "SUCCESS";
系统中的多个位置都可能引用:
"SUCCESS"
由于字符串不可变,共享相同字符串对象不会出现:
一个模块修改字符串,另一个模块的数据突然跟着改变
这种问题。
六、常见问题
6.1 String 是基本数据类型吗?
不是。
String 是:
java.lang.String
类,是引用类型。
6.2 String 对象不可变,那为什么变量还能重新赋值?
因为:
String s
是引用变量。
例如:
s = "A";
s = "B";
变化的是:
s 的引用
而不是:
"A" 对象被修改成了 "B"
6.3 String s = "abc" 有没有创建对象?
字符串字面量:
"abc"
本身表示 String 实例。
如果相同内容已经有对应 interned 实例,则可以复用已有对象,而不必为每一次相同字面量都创建独立 String 对象。
6.4 new String("abc") 一定只有一个对象吗?
不要简单用:
“这一行创建几个对象?”
这种脱离运行上下文的方式死记。
因为:
"abc"
字面量对应的 interned String 是否此前已经存在,与类加载及运行上下文有关。
但可以确定:
new String(...)
这次类实例创建表达式会创建新的 String 对象。
因此学习重点应该放在:
new 得到的新 String 引用
和
字面量 interned 引用
不是同一个引用
而不是机械背“固定创建 N 个对象”。
6.5 相同内容的字符串 == 一定为 true 吗?
不一定。
例如:
String a = "abc";
String b = new String("abc");
虽然内容一样:
a.equals(b)
为真。
但:
a == b
为假。
所以:
内容相同
≠
引用一定相同
6.6 两个相同字面量 == 为什么为 true?
因为字符串字面量被 intern,同内容字面量引用规范化共享实例。
例如:
String a = "abc";
String b = "abc";
因此:
a ─┐
├→ canonical "abc"
b ─┘
6.7 "a" + "b" 一定产生新 String 吗?
不能这么绝对地说。
例如:
String a = "ab";
String b = "a" + "b";
"a" + "b" 是编译期常量表达式,可以在编译阶段求值得到 "ab"。
而:
String x = "b";
String c = "a" + x;
属于运行时字符串连接。
所以判断时要先问:
这是编译期常量表达式,还是运行时拼接?
6.8 业务字符串应该用 == 还是 equals?
通常业务关注内容:
username.equals(input)
而不是:
username == input
== 更适合判断:
两个引用是否指向同一个对象。
6.9 StringBuilder 和 String 谁更好?
没有绝对谁更好。
String
→ 表示最终字符串
→ 不可变
StringBuilder
→ 适合频繁构建、修改字符串
→ 可变
本章先掌握 String。
03-03 会系统学习 StringBuilder。
七、练习与验收
7.1 知识问答
- String 是基本类型还是引用类型?
- 字符串字面量是什么类型?
- String 为什么被称为不可变对象?
s = s + "abc"是否修改了原 String 对象?- 什么是字符串池?
- 相同字符串字面量为什么能够共享对象?
==比较 String 时主要比较什么?equals()比较 String 时关注什么?new String("abc")与"abc"有什么对象身份差异?- 什么是
intern()? "a" + "b"与"a" + variable为什么可能具有不同对象行为?- 为什么不能简单认为“所有字符串都在字符串池”?
- 为什么 String 不可变适合共享?
- 为什么 String 很适合作为 HashMap key?
- 为什么大量字符串拼接后续需要学习 StringBuilder?
7.2 代码阅读
禁止运行,判断结果:
String a = "abc";
String b = "abc";
System.out.println(a == b);
说明:
- 输出什么?
- 为什么?
分析:
String a = "abc";
String b = new String("abc");
System.out.println(a == b);
System.out.println(a.equals(b));
回答:
- 两行结果分别是什么?
- 两种比较分别比较什么?
分析:
String a = "hello";
String b = "hel" + "lo";
String x = "lo";
String c = "hel" + x;
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == c.intern());
回答:
- 三个结果分别是什么?
b为什么与普通运行时拼接不同?intern()在第三个判断中发挥什么作用?
7.3 手写代码
- 分别使用字符串字面量和
new String()创建 String。 - 使用
char[]创建 String。 - 编写程序验证两个相同字符串字面量的引用关系。
- 编写程序验证两个
new String("Java")的==与equals()结果。 - 编写编译期字符串拼接案例。
- 编写运行时字符串拼接案例。
- 使用
intern()观察引用比较结果。 - 编写案例证明:
- String 对象不可变;
- String 引用变量可以重新赋值。
7.4 Debug
下面代码用于登录:
String correctName = "admin";
String inputName = scanner.next();
if (correctName == inputName) {
System.out.println("登录成功");
}
要求:
- 判断业务逻辑存在什么风险。
- 解释
==比较的是什么。 - 修改为正确的字符串内容比较方案。
分析:
String a = "Java";
a.concat("SE");
System.out.println(a);
要求:
- 不运行程序,预测结果。
- 从 String 不可变性角度解释。
- 思考如果要保存新字符串结果应该如何处理。
concat() 的完整 API 使用方式将在下一章学习。
7.5 综合训练
编写 StringIdentityDemo。
要求同时创建:
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + "lo";
String part = "lo";
String e = "hel" + part;
完成:
- 分别判断
a == b。 - 判断
a == c。 - 判断
a == d。 - 判断
a == e。 - 判断
a == e.intern()。 - 使用
equals()比较所有字符串内容。 - 为每一个结果画出简化引用关系图。
- 解释:
- 字面量;
new;- 编译期常量拼接;
- 运行时拼接;
- intern; 五者之间的区别。
7.6 本章验收
闭卷完成:
- [ ] 解释 String 为什么是引用类型。
- [ ] 解释字符串字面量。
- [ ] 手写两种 String 创建方式。
- [ ] 解释 String 不可变。
- [ ] 区分“引用改变”和“对象改变”。
- [ ] 解释字符串池。
- [ ] 正确判断
"abc" == "abc"。 - [ ] 正确判断
"abc" == new String("abc")。 - [ ] 正确判断
"a" + "bc"的常量表达式场景。 - [ ] 区分
==与equals()。 - [ ] 解释
intern()。 - [ ] 不再使用“String 内容相同,所以 == 一定 true”这样的错误判断。
- [ ] 不再死记“new String 一行固定创建几个对象”,而能够分析具体上下文。
如果你能够对字符串代码做到:
先判断表达式类型
→ 再判断对象来源
→ 再判断是否 intern
→ 最后分析 == / equals
说明 String 对象模型已经基本建立。