一句话总结:String 在 Java 中是一个对象(引用类型),而不是基本数据类型。 它的值(字符序列)存储在堆内存中,而变量(引用)则存储在栈内存中,指向这个 String 对象。

为什么说 String 是引用类型?
在 Java 中,数据类型分为两大类:
-
基本数据类型
- 共 8 种:
byte,short,int,long,float,double,char,boolean。 - 它们不是对象,变量中直接存储的是值本身。
int a = 10;,变量a的栈空间里直接放着10这个数值。
- 共 8 种:
-
引用数据类型
- 除了 8 种基本类型,其他所有类型都是引用类型,包括:数组、类(如
String,Integer)、接口等。 - 变量中存储的不是对象本身,而是指向对象在内存中地址的引用(可以理解为指针)。
String是 Java 中最常用的一个类,所以它自然也是引用类型。
- 除了 8 种基本类型,其他所有类型都是引用类型,包括:数组、类(如
String 的内存模型:

String str = "Hello";
当这行代码执行时,内存中发生了什么?
- 栈内存:创建一个名为
str的引用变量。str本身不包含 "Hello",它只是一个引用。 - 堆内存:JVM 在堆内存中创建一个
String对象,并将字符序列 "Hello" 存储在这个对象中。 - 关联:将
str这个引用指向堆内存中 "Hello" 对象的地址。
String 的特殊性:不可变性
String 是一个非常特殊的引用类型,它的核心特性是 不可变性。
- 什么是不可变性? 一旦一个
String对象被创建,它的内容(内部的字符数组)就不能被修改。 - 如何实现的?
String类内部使用一个final的char[]数组来存储字符,并且这个数组没有提供任何修改其内容的方法(如setCharAt),所有看似修改字符串的方法(如substring(),replace(),concat()),实际上都创建并返回了一个新的String对象,而原始对象保持不变。
示例:
String s1 = "hello";
String s2 = s1.replace('h', 'H');
System.out.println(s1); // 输出: hello
System.out.println(s2); // 输出: Hello
s1指向 "hello" 对象。s1.replace(...)方法执行后,并没有修改 "hello" 这个对象,而是创建了一个新的String对象 "Hello"。s2被赋值为这个新对象的引用。- 内存中有两个
String对象:"hello" 和 "Hello"。s1仍然指向 "hello"。
String 的两种创建方式及其区别
理解了引用类型和不可变性,就能明白 String 两种创建方式的根本区别。

字面量赋值 (String Literal Pool - 字符串常量池)
String str1 = "hello"; String str2 = "hello";
- 过程:
- 当执行
String str1 = "hello";时,JVM 会首先在字符串常量池 中查找是否存在 "hello" 这个字符串。 - 如果不存在,就在池中创建一个新的 "hello" 对象,并将
str1引用它。 - 当执行
String str2 = "hello";时,JVM 再次在字符串常量池中查找。 - 发现已经存在 "hello" 对象,于是不再创建新对象,直接让
str2引用池中已有的 "hello" 对象。
- 当执行
- 结果:
str1和str2指向的是同一个对象。 - 验证:
System.out.println(str1 == str2); // 输出: true System.out.println(str1.equals(str2)); // 输出: true
- 比较的是两个引用是否指向同一个内存地址(对象)。
equals():String类重写了equals方法,比较的是两个字符串的内容是否相同。
new 关键字创建 (堆内存)
String str3 = new String("hello");
String str4 = new String("hello");
- 过程:
- 当执行
String str3 = new String("hello");时,JVM 会这样做:- 在字符串常量池中检查 "hello" 是否存在,如果不存在,则创建一个并放入池中。(如果已经存在,则跳过此步)。
- 在堆内存中,一定会创建一个新的
String对象,并用池中的 "hello" 来初始化这个新对象。 - 让
str3引用这个堆中的新对象。
str4的创建过程同理,会在堆内存中再创建一个全新的、内容相同的String对象。
- 当执行
- 结果:
str3和str4指向的是两个不同的对象,虽然它们的内容相同,但内存地址不同。 - 验证:
System.out.println(str3 == str4); // 输出: false System.out.println(str3.equals(str4)); // 输出: true
号拼接的底层原理
在 Java 中,使用 号拼接字符串是一个非常常见的操作,它的底层原理与 StringBuilder/StringBuffer 密切相关。
-
在循环外使用 : 号用于连接编译时就能确定的常量字符串,编译器会直接在编译期间完成拼接,优化成一个字符串。
// 编译后会被优化为 String s = "abc"; String s = "a" + "b" + "c";
-
在循环内或使用变量拼接: 号的操作数中包含变量,或者在一个循环中,Java 编译器会将其优化为使用
StringBuilder(在单线程环境下)或StringBuffer(在多线程环境下)来构建字符串。示例:
String s = "hello"; s = s + " world";
这行代码的等效过程(JVM 内部优化后的大致逻辑)是:
- 创建一个
StringBuilder对象。 - 调用
StringBuilder.append("hello")。 - 调用
StringBuilder.append(" world")。 - 调用
StringBuilder.toString(),这个方法会创建一个新的String对象是 "hello world"。 - 将
s的引用指向这个新创建的 "hello world" 对象。
注意:每次使用 拼接,都可能意味着创建一个新的
String对象,在循环中进行大量拼接,如果使用 ,会频繁创建新对象,导致性能下降,在循环中应手动使用StringBuilder。 - 创建一个
intern() 方法
intern() 是 String 类的一个方法,它用于将字符串显式地放入字符串常量池中。
-
作用:
- 如果调用
intern()的字符串内容已经在常量池中存在,则返回池中该字符串的引用。 - 如果不在,则将该字符串的内容复制一份到常量池中,并返回池中这个新复制的字符串的引用。
- 如果调用
-
示例:
String s1 = new String("hello"); // s1 指向堆中的 "hello" String s2 = s1.intern(); // s2 指向常量池中的 "hello" String s3 = "hello"; // s3 也指向常量池中的 "hello" System.out.println(s1 == s2); // false, s1在堆,s2在池 System.out.println(s2 == s3); // true, s2和s3都指向池中的同一个对象intern()方法在某些需要极致优化内存和比较对象身份的场景下会用到,但在日常开发中不常用。
| 特性 | 描述 |
|---|---|
| 类型 | String 是一个引用类型,本质上是 java.lang.String 类的实例。 |
| 内存存储 | 存储在堆内存中,变量(引用)存储在栈内存中,指向堆对象。 |
| 核心特性 | 不可变性,一旦创建,内容不能改变,任何修改操作都会返回一个新对象。 |
| 创建方式 | 字面量 :创建的字符串会进入字符串常量池的字符串只会有一份。 new:每次都会在堆内存中创建一个全新的对象,即使内容相同。 |
| 拼接 | 底层由编译器优化为使用 StringBuilder 或 StringBuffer,在循环中应避免使用 以防性能问题。 |
| 比较 | :比较引用(地址)是否相同。 equals():比较字符串内容是否相同。 |
intern() |
手动将字符串放入字符串常量池,并返回池中引用。 |
理解这些概念,你就能清晰地分析 String 相关代码的内存行为和执行结果,写出更高效、更健壮的代码。
