背景与动机
字符串几乎出现在每一个 Java 程序里:用户输入、日志、文件名、接口参数、数据库字段,全都离不开它。
看起来字符串只是文本,但 Java 里的 String 有几件非常重要的事:
- 它是不可变对象
- 比较内容时不能只看
== - 字面量和
new String()背后的对象关系不一样 - 频繁拼接时通常不该一直用
+
把这些点理清楚,后面看集合、IO、异常处理、框架参数绑定时,很多细节都会顺很多。
核心原理拆解
1. String 是不可变对象
String text = "hello";text = text + " java";这里不是把原来的 "hello" 原地改掉,而是创建了一个新的字符串对象,再让 text 指向新对象。
也就是说,String 的不可变,指的是:
- 字符串对象一旦创建
- 它内部代表的字符内容就不能再被原地修改
看起来像“修改字符串”的代码,很多时候其实只是:
- 生成了一个新字符串
- 变量重新指向了它
1.1 为什么 String 要设计成不可变
这不是为了“故意限制你”,而是为了换来很多好处:
- 更适合复用
- 更适合做常量池缓存
- 更适合作为哈希键使用
- 更安全,不容易被意外改掉
比如一个字符串如果已经被当作 Map 的 key,或者被多个地方共用,如果它还能随便改内容,很多行为都会变得不可靠。
所以 String 的不可变性,其实是 Java 设计里非常基础的一块。

2. 字面量和 new String() 不是一回事
String a = "java";String b = "java";String c = new String("java");a 和 b 可能引用的是同一个字符串常量池对象;c 通常会额外创建一个新的字符串对象。
这也是为什么字符串比较时,== 和 equals 的结果经常不一样。
2.1 字符串常量池在做什么
你可以先把字符串常量池理解成:
- JVM 帮你维护的一块可复用字符串区域
- 相同字面量字符串,通常不需要反复新建对象
比如:
String a = "java";String b = "java";这里两个 "java" 通常会复用同一个常量池对象,所以很多时候 a == b 会是 true。
但如果你写:
String c = new String("java");那通常表示你显式要求创建一个新的字符串对象,所以 c 和常量池里的 "java" 往往不是同一个引用。

2.2 intern() 知道它的存在就够了
intern() 的作用,可以先粗略理解为:
- 尝试把字符串和常量池里的等值字符串关联起来
比如:
String s1 = new String("java");String s2 = s1.intern();String s3 = "java";
System.out.println(s2 == s3); // 通常是 true初学阶段不用把它抠得太深,但知道:
- 它和字符串常量池有关
- 面试里偶尔会问到
就够了。
3. 内容比较用 equals
String a = new String("java");String b = new String("java");
System.out.println(a == b); // falseSystem.out.println(a.equals(b)); // true== 比较的是引用是否指向同一个对象,equals 比较的是字符串内容。
所以判断两个字符串“内容一样不一样”,通常要优先想到 equals。
3.1 == 和 equals 为什么容易混
因为在部分场景下,== 恰好也可能返回 true:
String a = "java";String b = "java";System.out.println(a == b); // 可能是 true这不是因为 == 开始比较内容了,而是因为:
a和b恰好指向了同一个常量池对象
所以不要把“偶尔结果一样”误以为“比较逻辑一样”。

4. 常用字符串方法
String 提供了很多常见方法,但最好不要把它们当作一串零散 API 去背,可以按用途来记。
4.1 获取信息
String text = "hello java";System.out.println(text.length()); // 10System.out.println(text.charAt(1)); // elength():看长度charAt(index):取某个位置字符
4.2 查找和判断
String text = "hello java";System.out.println(text.contains("java")); // trueSystem.out.println(text.startsWith("he")); // trueSystem.out.println(text.endsWith("java")); // trueSystem.out.println(text.indexOf("java")); // 6这组方法适合做:
- 关键字判断
- 前缀后缀判断
- 简单定位
4.3 截取和替换
String text = "hello java";System.out.println(text.substring(6)); // javaSystem.out.println(text.replace("java", "world")); // hello world这里要特别记住:
substring()replace()
它们通常都不会修改原字符串,而是返回新的结果字符串。
4.4 大小写和去空白
String text = " Hello Java ";System.out.println(text.toLowerCase());System.out.println(text.toUpperCase());System.out.println(text.trim());这组方法很常见,尤其在做:
- 用户输入处理
- 简单标准化
- 日志整理
时会经常用到。
4.5 拆分和拼接
String csv = "Tom,Alice,Grace";String[] arr = csv.split(",");System.out.println(String.join(" | ", arr));split():按规则拆开String.join():按分隔符拼回去
String.join() 很适合简单拼接一组已有字符串。
5. 频繁拼接时优先用 StringBuilder
StringBuilder sb = new StringBuilder();sb.append("hello");sb.append(" ");sb.append("java");System.out.println(sb.toString());如果在循环里大量用 + 拼接字符串,通常会产生很多临时对象。大量拼接时,StringBuilder 更合适。
5.1 为什么循环里不建议一直用 +
比如:
String result = "";for (int i = 0; i < 3; i++) { result = result + i;}这个过程看起来只是不断追加文本,但背后往往会不断产生新的字符串对象。
所以拼接次数一多,代价就会上来。
5.2 StringBuilder、StringBuffer、StringJoiner 怎么区分
可以先这样记:
StringBuilder最常用,适合单线程下频繁拼接StringBuffer和StringBuilder很像,但它的方法更偏同步,通常认为线程更安全StringJoiner更适合“按分隔符把一组值拼起来”
比如 StringJoiner:
import java.util.StringJoiner;
public class Main { public static void main(String[] args) { String[] names = {"Bob", "Alice", "Grace"};
StringJoiner sj = new StringJoiner(", ", "Hello ", "!"); for (String name : names) { sj.add(name); } System.out.println(sj.toString()); }}输出大致是:
Hello Bob, Alice, Grace!5.3 String.join() 和 StringJoiner 的区别
可以这样粗略区分:
String.join()适合快速把现成字符串集合拼起来StringJoiner更适合你一点点add(),并且还想指定前缀、后缀
所以它们不是谁替代谁,而是使用场景略有区别。
5.4String 拼接 vs StringBuilder 追加流程图

6. String 也会有空值问题
String text = null;// text.length(); // 会抛出空指针异常字符串变量可以是 null,但 null 不是空字符串 "",这两个要分清。
6.1 null、""、" " 不一样
它们至少有这几个区别:
null没有引用到任何字符串对象""是长度为 0 的空字符串" "里面有空格字符,长度通常不是 0
这三个在判空、表单校验、接口参数处理中很容易混。
6.2 判空时的常见写法
比如:
String text = "java";
System.out.println(text == null); // falseSystem.out.println(text.isEmpty()); // falseSystem.out.println(text.isBlank()); // falseSystem.out.println("java".equals(text)); // true可以先这样理解:
isEmpty():长度是不是 0isBlank():去掉空白语义后是不是空
如果你面对的是用户输入,isBlank() 往往更贴近“有没有真正填内容”。
6.3 为什么很多人喜欢写 "abc".equals(text)
因为这样更稳:
String text = null;System.out.println("abc".equals(text)); // false如果反过来写:
// text.equals("abc");当 text 是 null 时,就会直接抛空指针异常。
所以这个写法本质上是在防空指针。
最小可运行代码示例
import java.util.StringJoiner;
public class StringDemo { public static void main(String[] args) { String a = "java"; String b = "java"; String c = new String("java");
System.out.println(a == b); System.out.println(a == c); System.out.println(a.equals(c));
String text = "hello"; text = text + " world"; System.out.println(text);
System.out.println(text.substring(6)); System.out.println(text.replace("world", "java"));
StringBuilder sb = new StringBuilder(); sb.append("hello"); sb.append(" "); sb.append("builder"); System.out.println(sb.toString());
StringJoiner sj = new StringJoiner(", ", "[", "]"); sj.add("Tom"); sj.add("Alice"); sj.add("Grace"); System.out.println(sj.toString());
String blank = " "; System.out.println(blank.isEmpty()); System.out.println(blank.isBlank()); }}常见陷阱与错误示例
1. 用 == 比较字符串内容
String a = new String("java");String b = new String("java");System.out.println(a == b);应该用:
System.out.println(a.equals(b));2. 把空字符串和 null 混为一谈
"" 是长度为 0 的字符串,null 是根本没有引用到对象。
3. 在循环里频繁用 + 拼接
如果拼接次数很多,优先考虑 StringBuilder。
4. 误以为所有 String 方法都会修改原对象
substring、replace、trim 这类方法通常都会返回新字符串,原对象不会被改掉。
5. 忽略 null 导致空指针
比如直接写:
String text = null;// System.out.println(text.equals("java"));这类代码很容易抛空指针异常。做比较时要么先判空,要么用常量在前的写法。
面试高频问题
1. 为什么 String 是不可变的
因为它一旦创建,内容就不能原地修改。很多看起来像“修改”的操作,本质上是创建了新字符串对象并重新引用它。
2. == 和 equals 有什么区别
== 比较引用是否相同,equals 比较内容是否相同。
3. 字符串常量池是做什么的
它主要用于复用相同字面量字符串,避免同样内容反复创建对象。
4. String、StringBuffer、StringBuilder 有什么区别
String不可变,适合表示稳定文本StringBuilder可变,适合单线程下频繁拼接StringBuffer也是可变字符串工具,但通常认为同步开销更大,更多是历史兼容或特定并发场景下会见到
5. null 和空字符串有什么区别
null 表示没有引用对象;空字符串 "" 表示引用了一个长度为 0 的字符串对象。
6. 什么时候用 StringJoiner
当你想按分隔符逐步拼接多个字符串,并且还希望带前缀、后缀时,StringJoiner 会比较顺手。
一句话总结
String 是不可变文本对象,比较内容用 equals,理解常量池能帮你看懂 ==,大量拼接优先 StringBuilder,同时别把 null、"" 和 " " 混在一起。