Java 中初始化 String 主要有两种方式:

- 字面量赋值(推荐):
String str = "hello"; new关键字创建:String str = new String("hello");
这两种方式在底层机制和内存使用上有根本区别,理解这个区别至关重要。
字面量赋值
这是最常见、最简单、也最推荐的创建字符串的方式。
String str1 = "hello"; String str2 = "hello";
底层机制:字符串常量池
Java 为了提高性能和减少内存开销,引入了一个叫做字符串常量池 的机制。
- 第一次赋值:当执行
String str1 = "hello";时,JVM 会首先在字符串常量池中查找是否存在值为"hello"的字符串。 - 不存在:如果不存在,JVM 就会在常量池中创建一个
"hello"字符串对象,然后将str1引用指向这个对象。 - 第二次赋值:当执行
String str2 = "hello";时,JVM 再次在字符串常量池中查找值为"hello"的字符串。 - 存在:这次找到了,JVM 就不会创建新的对象,而是直接将
str2引用指向常量池中已经存在的"hello"对象。
内存布局示意图
+-------------------------+
| 栈内存 |
| +---------------------+ |
| | str1 (引用) | | ---+
| +---------------------+ | |
| | str2 (引用) | | |
| +---------------------+ | |
+-------------------------+ |
|
+-------------------------+ |
| 堆内存 | |
| | |
+-------------------------+ |
|
+-------------------------+ |
| 字符串常量池 | <---+
| +---------------------+ |
| | "hello" (对象) | |
| +---------------------+ |
+-------------------------+
特点
- 高效:避免了重复创建相同的字符串对象,节省了内存。
- 共享:所有使用字面量
"hello"的变量都指向同一个对象。 - 不可变性:
String对象是不可变的,一旦在常量池中创建,内容就不能被改变,任何修改操作(如str1 = str1 + " world";)都会创建一个新的字符串对象,而str1的引用会指向这个新对象。
new 关键字创建
这种方式通过显式地调用 String 类的构造器来创建字符串对象。

String str3 = new String("hello");
String str4 = new String("hello");
底层机制:堆内存
new操作:new关键字的作用是在堆内存 中创建一个新的对象。- 创建过程:当执行
String str3 = new String("hello");时,JVM 会做两件事:- 它会在字符串常量池中检查并创建(如果不存在)一个值为
"hello"的字符串对象。 - 它会在堆内存 中,根据常量池中的
"hello"再创建一个新的String对象。 - 将
str3引用指向这个堆内存中的新对象。
- 它会在字符串常量池中检查并创建(如果不存在)一个值为
内存布局示意图
+-------------------------+
| 栈内存 |
| +---------------------+ |
| | str3 (引用) | | ---+
| +---------------------+ | |
| | str4 (引用) | | |
| +---------------------+ | |
+-------------------------+ |
|
+-------------------------+ |
| 堆内存 | |
| +---------------------+ | |
| | "hello" (str3指向) | | <---+
| +---------------------+ | |
| +---------------------+ | |
| | "hello" (str4指向) | | <---+
| +---------------------+ | |
+-------------------------+ |
|
+-------------------------+ |
| 字符串常量池 | |
| +---------------------+ | |
| | "hello" (对象) | | ---+
| +---------------------+ |
+-------------------------+
特点
- 低效:每次
new都会在堆内存中创建一个新对象,即使内容相同,也会造成内存浪费和性能开销。 - 不共享:
str3和str4指向的是堆内存中两个完全独立、互不相干的对象。 - 仍然受常量池影响:虽然最终对象在堆中,但其内容来源于常量池。
| 特性 | 字面量赋值 () | new 关键字 (new String(...)) |
|---|---|---|
| 创建位置 | 字符串常量池 | 堆内存 |
| 内存使用 | 高效,共享对象 | 较低效,每次都创建新对象 |
| 性能 | 更快 | 较慢 |
| 推荐度 | 强烈推荐 | 不推荐,除非有特殊需求 |
| 示例 | String s = "abc"; |
String s = new String("abc"); |
其他初始化方式
除了以上两种主要方式,String 类还提供了多个构造器,允许从不同的数据源初始化字符串。
// 1. 从字符数组初始化
char[] chars = {'J', 'a', 'v', 'a'};
String fromCharArray = new String(chars); // 结果: "Java"
// 2. 从字符数组的指定部分初始化
String fromCharSubArray = new String(chars, 1, 2); // 从索引1开始,取2个字符,结果: "av"
// 3. 从字节数组初始化 (使用平台默认字符集)
byte[] bytes = {65, 66, 67}; // A, B, C 的ASCII码
String fromByteArray = new String(bytes); // 结果: "ABC"
// 4. 从字节数组的指定部分初始化 (使用指定字符集)
byte[] utf8Bytes = {(byte) 0xE4, (byte) 0xBD, (byte) 0xA0}; // "你"的UTF-8编码
String fromUtf8Bytes = new String(utf8Bytes, "UTF-8"); // 结果: "你"
// 5. 创建一个空字符串
String emptyString = new String(); // 或者 String emptyString = "";
// 6. 格化化字符串初始化 (非常常用)
String formatted = String.format("你好, %s! 今天是 %tF.", "张三", new java.util.Date());
// 结果类似: "你好, 张三! 今天是 2025-10-27."
intern() 方法:在两种方式间转换
intern() 是一个特殊的方法,它可以将一个堆上的字符串对象“拉”到字符串常量池中。
- 如果常量池中已经存在该字符串,则返回常量池中对象的引用。
- 如果常量池中不存在,则将该堆中的字符串对象复制一份到常量池中,并返回常量池中对象的引用。
示例:
String s1 = new String("hello"); // 在堆上创建一个 "hello",常量池也创建一个 "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都在常量池,是同一个对象
这个方法在某些需要极致优化内存的场景下会用到,但日常开发中很少需要手动调用。

最佳实践
- 优先使用字面量:在绝大多数情况下,使用
String str = "hello";来创建字符串。 - 避免不必要的
new:除非你有明确的理由(需要创建一个内容与常量池中某个字符串相同但必须是独立新对象的实例),否则不要使用new String()。 - 利用 号进行字符串拼接:Java 编译器对 号做了优化,在编译时, 号连接的都是字符串常量,它会自动合并成一个字符串。
String s = "hel" + "lo"; // 编译后等同于 String s = "hello";
如果在循环中使用 拼接字符串,会产生大量临时对象,性能很差,此时应使用
StringBuilder或StringBuffer。
