A13 String 详解:不可变、常量池、equals 与 ==、性能与常见坑
【本节目标】
学完这一篇,你应该能做到:
1)理解 String 的核心特性:不可变(immutable)
2)搞清楚 String 常量池、字符串字面量、new String() 的区别
3)彻底分清 equals 与 ==(以及如何避免空指针)
4)知道字符串拼接的性能坑:循环里不要疯狂 "+"
5)掌握常用 API:length、charAt、substring、split、replace、trim/strip 等
6)避开 String 的高频踩坑点(面试与实战都会遇到)
一、为什么 String 这么重要?
在 Java 业务开发里,String 是使用频率最高的类型之一:
-
用户名、手机号、订单号、JSON、SQL、日志、路径、URL......几乎全是 String
String 学得不牢,会出现:
-
比较不相等(==/equals 坑)
-
性能很差(拼接坑)
-
正则误用(split/replaceAll 坑)
-
编码和字符长度理解错误(中文、emoji)
二、String 的核心特性:不可变(Immutable)
"不可变"意思是:String 一旦创建,内容就不能被修改。
你看起来像是在"修改",其实是创建了一个新字符串。
java
String s = "abc";
s = s + "d";
System.out.println(s); // abcd
这里 s 不是把原来的 "abc" 改成 "abcd",而是:
1)原来指向 "abc"
2)拼接产生新对象 "abcd"
3)s 指向新对象
不可变带来的好处:
1)线程安全(多线程读同一个 String 不会乱)
2)安全性更高(比如作为 Map key 更可靠)
3)可以做常量池复用(省内存,性能更好)
三、String 常量池:字面量为什么"看起来像同一个对象"?
先看一个现象:
java
String a = "java";
String b = "java";
System.out.println(a == b); // true
原因:
-
"java" 这种写法叫"字符串字面量"
-
字面量会放进"字符串常量池"
-
同内容的字面量通常复用同一个对象引用,所以 a 和 b 指向同一个
但是注意:这不代表以后都能用 == 比较内容!这只是常量池复用带来的"假象"。
四、new String():为什么它通常不是同一个对象?
java
String a = "java";
String b = new String("java");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
原因:
-
a 指向常量池中的对象
-
new String("java") 会在堆上创建一个新的 String 对象(通常内容相同,但引用不同)
结论:
比较字符串内容,一律用 equals(或 equalsIgnoreCase)。
五、equals 与 ==:必须一次性讲透
1)==
-
基本类型:比较值
-
引用类型:比较引用地址(是不是同一个对象)
2)equals
- 对于 String:比较内容是否相同
所以:
-
比内容:用 equals
-
比是不是同一个对象:才用 ==(字符串业务里几乎不用)
六、如何避免空指针:equals 的正确写法
最经典 NPE:
java
String s = null;
// System.out.println(s.equals("abc")); // 会 NPE
推荐写法(把常量放前面):
java
String s = null;
System.out.println("abc".equals(s)); // false(安全)
如果你要忽略大小写:
java
String s = "Java";
System.out.println("java".equalsIgnoreCase(s)); // true
七、字符串拼接性能:什么时候用 +,什么时候用 StringBuilder?
结论先给:
-
少量拼接(1~3 次):"+" 可以,代码更简洁
-
循环拼接、批量拼接:一定用 StringBuilder
看这个循环拼接(不推荐):
java
String s = "";
for (int i = 0; i < 10000; i++) {
s += i;
}
原因:每次拼接都会生成新 String,对象数量爆炸,性能很差。
推荐写法:
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String s = sb.toString();
补充:
-
StringBuilder:线程不安全,但快(绝大多数场景用它)
-
StringBuffer:线程安全,但慢(老项目或确实需要线程安全才用)
八、常用 API(你写业务每天都用)
1)长度与字符
-
length():字符数量(注意:对 emoji 等可能不等于"人眼字符数",后面再扩展)
-
charAt(i):取某个位置字符
-
substring(begin, end):截取子串(左闭右开)
java
String s = "hello";
System.out.println(s.length()); // 5
System.out.println(s.charAt(1)); // e
System.out.println(s.substring(1, 4)); // ell
2)查找与判断
-
contains、startsWith、endsWith
-
indexOf、lastIndexOf
java
String s = "hello java";
System.out.println(s.contains("java")); // true
System.out.println(s.startsWith("he")); // true
System.out.println(s.indexOf("java")); // 6
3)去空格
-
trim():去掉两端空白(传统)
-
strip():Java 11+,对 Unicode 空白处理更全面(了解即可)
九、String 的高频坑(面试 + 实战重灾区)
坑 1:用 == 比较内容(偶尔"看起来对",其实不可靠)
正确:用 equals/equalsIgnoreCase。
坑 2:循环里用 + 拼接导致性能差
正确:StringBuilder。
坑 3:split 和 replaceAll 其实是"正则"
这点非常多人踩坑!
例子:按点号分割 "a.b.c",很多人写:
java
String s = "a.b.c";
String[] parts = s.split("."); // 错!"." 在正则里表示"任意字符"
System.out.println(parts.length); // 结果会非常诡异
正确写法(点号要转义):
java
String s = "a.b.c";
String[] parts = s.split("\\.");
System.out.println(parts.length); // 3
同理:竖线 |、括号 ()、加号 +、星号 * 都是正则特殊字符,想按字面意义处理要转义。
坑 4:replace 与 replaceAll 的区别
-
replace:按"字面字符"替换(更安全、更常用)
-
replaceAll:按"正则"替换(强大但容易误用)
java
String s = "a.b.c";
System.out.println(s.replace(".", "#")); // a#b#c(推荐)
System.out.println(s.replaceAll("\\.", "#")); // a#b#c(正则写法)
坑 5:判空只判断 null 不够
很多业务里需要判断"空字符串/全空格":
java
String s = " ";
System.out.println(s.isEmpty()); // false(因为长度不为 0)
常见处理:
-
先 trim 再判断 empty
(Java 11+ 也有 isBlank,但你可以后面再补)
javascript
String s = " ";
boolean blank = s.trim().isEmpty();
System.out.println(blank); // true
十、一个小综合例子:用户名校验(更像真实业务)
需求:
用户名必须满足:
1)不为 null
2)去掉两端空格后长度 3~16
3)只能包含字母、数字、下划线(先用简单循环判断,别急着上正则)
java
public class UserNameCheck {
public static boolean isValid(String name) {
if (name == null) return false;
name = name.trim();
if (name.length() < 3 || name.length() > 16) return false;
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
boolean ok = (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| (c == '_');
if (!ok) return false;
}
return true;
}
public static void main(String[] args) {
System.out.println(isValid(null)); // false
System.out.println(isValid(" ab ")); // false(长度不足)
System.out.println(isValid("abc_123")); // true
System.out.println(isValid("a#c")); // false
}
}
十一、本节小练习(从易到难)
练习 1:写一个方法 safeEquals(String a, String b),安全比较两个字符串内容是否相等(不能 NPE)
练习 2:统计一个字符串中某个字符出现次数(例如统计 'a')
练习 3:把一句话按空格分割成单词数组(注意连续空格的情况:先 trim 再 split)
练习 4:实现一个 join:把 String[] 用逗号拼接成一行(要求用 StringBuilder)
练习 5(进阶):实现一个方法 reverse(String s),反转字符串(提示:StringBuilder 或 char 数组)
本节小结
1)String 不可变,修改本质上是创建新对象
2)字面量走常量池,new String 会创建新对象
3)比较内容用 equals,别用 ==
4)循环拼接用 StringBuilder
5)split/replaceAll 是正则,特殊字符要转义
6)写业务时要考虑:null、空字符串、全空格、大小写
下一篇预告(A14)
《A14 常用工具类与包装类:Integer/Long、自动装箱拆箱、Objects、Arrays(配坑点)》
从这一篇开始,你会明显感觉"写业务更顺手":很多常见需求都有现成工具类可用。