Java 字符串

背景与动机#

字符串几乎出现在每一个 Java 程序里:用户输入、日志、文件名、接口参数、数据库字段,全都离不开它。

看起来字符串只是文本,但 Java 里的 String 有几件非常重要的事:

  1. 它是不可变对象
  2. 比较内容时不能只看 ==
  3. 字面量和 new String() 背后的对象关系不一样
  4. 频繁拼接时通常不该一直用 +

把这些点理清楚,后面看集合、IO、异常处理、框架参数绑定时,很多细节都会顺很多。

核心原理拆解#

1. String 是不可变对象#

String text = "hello";
text = text + " java";

这里不是把原来的 "hello" 原地改掉,而是创建了一个新的字符串对象,再让 text 指向新对象。

也就是说,String 的不可变,指的是:

  • 字符串对象一旦创建
  • 它内部代表的字符内容就不能再被原地修改

看起来像“修改字符串”的代码,很多时候其实只是:

  • 生成了一个新字符串
  • 变量重新指向了它

1.1 为什么 String 要设计成不可变#

这不是为了“故意限制你”,而是为了换来很多好处:

  • 更适合复用
  • 更适合做常量池缓存
  • 更适合作为哈希键使用
  • 更安全,不容易被意外改掉

比如一个字符串如果已经被当作 Map 的 key,或者被多个地方共用,如果它还能随便改内容,很多行为都会变得不可靠。

所以 String 的不可变性,其实是 Java 设计里非常基础的一块。

img
img

2. 字面量和 new String() 不是一回事#

String a = "java";
String b = "java";
String c = new String("java");

ab 可能引用的是同一个字符串常量池对象;c 通常会额外创建一个新的字符串对象。

这也是为什么字符串比较时,==equals 的结果经常不一样。

2.1 字符串常量池在做什么#

你可以先把字符串常量池理解成:

  • JVM 帮你维护的一块可复用字符串区域
  • 相同字面量字符串,通常不需要反复新建对象

比如:

String a = "java";
String b = "java";

这里两个 "java" 通常会复用同一个常量池对象,所以很多时候 a == b 会是 true

但如果你写:

String c = new String("java");

那通常表示你显式要求创建一个新的字符串对象,所以 c 和常量池里的 "java" 往往不是同一个引用。

img
img

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); // false
System.out.println(a.equals(b)); // true

== 比较的是引用是否指向同一个对象,equals 比较的是字符串内容。

所以判断两个字符串“内容一样不一样”,通常要优先想到 equals

3.1 ==equals 为什么容易混#

因为在部分场景下,== 恰好也可能返回 true

String a = "java";
String b = "java";
System.out.println(a == b); // 可能是 true

这不是因为 == 开始比较内容了,而是因为:

  • ab 恰好指向了同一个常量池对象

所以不要把“偶尔结果一样”误以为“比较逻辑一样”。

图片预览
图片预览

4. 常用字符串方法#

String 提供了很多常见方法,但最好不要把它们当作一串零散 API 去背,可以按用途来记。

4.1 获取信息#

String text = "hello java";
System.out.println(text.length()); // 10
System.out.println(text.charAt(1)); // e
  • length():看长度
  • charAt(index):取某个位置字符

4.2 查找和判断#

String text = "hello java";
System.out.println(text.contains("java")); // true
System.out.println(text.startsWith("he")); // true
System.out.println(text.endsWith("java")); // true
System.out.println(text.indexOf("java")); // 6

这组方法适合做:

  • 关键字判断
  • 前缀后缀判断
  • 简单定位

4.3 截取和替换#

String text = "hello java";
System.out.println(text.substring(6)); // java
System.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 StringBuilderStringBufferStringJoiner 怎么区分#

可以先这样记:

  • StringBuilder 最常用,适合单线程下频繁拼接
  • StringBufferStringBuilder 很像,但它的方法更偏同步,通常认为线程更安全
  • 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); // false
System.out.println(text.isEmpty()); // false
System.out.println(text.isBlank()); // false
System.out.println("java".equals(text)); // true

可以先这样理解:

  • isEmpty():长度是不是 0
  • isBlank():去掉空白语义后是不是空

如果你面对的是用户输入,isBlank() 往往更贴近“有没有真正填内容”。

6.3 为什么很多人喜欢写 "abc".equals(text)#

因为这样更稳:

String text = null;
System.out.println("abc".equals(text)); // false

如果反过来写:

// text.equals("abc");

textnull 时,就会直接抛空指针异常。

所以这个写法本质上是在防空指针。

最小可运行代码示例#

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 方法都会修改原对象#

substringreplacetrim 这类方法通常都会返回新字符串,原对象不会被改掉。

5. 忽略 null 导致空指针#

比如直接写:

String text = null;
// System.out.println(text.equals("java"));

这类代码很容易抛空指针异常。做比较时要么先判空,要么用常量在前的写法。

面试高频问题#

1. 为什么 String 是不可变的#

因为它一旦创建,内容就不能原地修改。很多看起来像“修改”的操作,本质上是创建了新字符串对象并重新引用它。

2. ==equals 有什么区别#

== 比较引用是否相同,equals 比较内容是否相同。

3. 字符串常量池是做什么的#

它主要用于复用相同字面量字符串,避免同样内容反复创建对象。

4. StringStringBufferStringBuilder 有什么区别#

  • String 不可变,适合表示稳定文本
  • StringBuilder 可变,适合单线程下频繁拼接
  • StringBuffer 也是可变字符串工具,但通常认为同步开销更大,更多是历史兼容或特定并发场景下会见到

5. null 和空字符串有什么区别#

null 表示没有引用对象;空字符串 "" 表示引用了一个长度为 0 的字符串对象。

6. 什么时候用 StringJoiner#

当你想按分隔符逐步拼接多个字符串,并且还希望带前缀、后缀时,StringJoiner 会比较顺手。

一句话总结#

String 是不可变文本对象,比较内容用 equals,理解常量池能帮你看懂 ==,大量拼接优先 StringBuilder,同时别把 null""" " 混在一起。

文章目录

文章目录