String 是不可变对象
Java 里的 String 一旦创建就不能修改。s += "x" 看起来是在原字符串后面追加,实际上是新造了一个字符串对象,再把变量指向它。原来的对象如果没有别的引用,就只能等 GC 回收。
不可变带来两个好处:可以安全地共享(字符串常量池)、可以作为 HashMap 的 key 而不担心内容被改。代价是频繁拼接会产生大量临时对象,这也是后面要讲 StringBuilder 的原因。
== 比地址,equals 比内容
这是 Java 面试出现频率最高的问题之一,但真正写代码时栽跟头的人依然不少。先看代码:
public class StringEqualsDemo {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + "lo"; // 编译期就折叠成 "hello"
String e = "hel";
String f = e + "lo"; // 运行时拼接,产生新对象
System.out.println(a == b); // true,指向常量池同一个对象
System.out.println(a == c); // false,new 出来的是新对象
System.out.println(a == d); // true,编译期常量折叠
System.out.println(a == f); // false
System.out.println(a.equals(f)); // true,内容相同
}
}
结论很明确:判断内容一律用 equals。== 只在判断“是不是同一个对象”时才有意义,比如单例、枚举、常量池字面量。还有一个细节,equals 是区分大小写的,忽略大小写用 equalsIgnoreCase。
拼接:循环里别用 +
少量拼接用 + 没问题,可读性最好。但循环里用 + 就是另一回事了:每一轮都创建一个新的 StringBuilder 和新的字符串,循环一万次就产生一万个中间对象。数据量一大,耗时会明显变长,日志里也更容易看到频繁 GC。
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(i).append(',');
}
String result = sb.toString();
System.out.println(result.length());
StringBuilder 不是线程安全的,单线程拼接用它就够;多线程共享同一个拼接结果时才考虑 StringBuffer。另外,已知大概长度时给个初始容量,能少几次扩容。
常用方法速查
字符串的方法很多,新手先记住下面这些,其余用到时查文档即可:
| 方法 | 作用 | 注意 |
|---|---|---|
length() |
字符个数 | 中文也算一个字符,但不等于字节数 |
charAt(i) |
取指定位置的字符 | 下标从 0 开始,越界抛异常 |
indexOf(s) |
查找子串首次出现的位置 | 找不到返回 -1,不是 0 |
substring(begin, end) |
截取子串 | 左闭右开,不含 end |
replace(a, b) |
替换全部匹配 | 返回新字符串,原对象不变 |
split(regex) |
按正则切分 | 正则特殊字符要转义 |
startsWith(s) |
判断前缀 | 常用来判断协议、路径 |
这些方法都不会修改原字符串,而是返回一个新字符串,所以 str.toUpperCase(); 单独写一行没有任何效果,必须把返回值赋回去。substring 的第二个参数是结束下标且不包含该位置,"hello".substring(1, 3) 得到的是 el,记成“左闭右开”就不容易错。
length() 返回的是 UTF-16 码元个数,emoji 这类字符可能占两个码元,所以特殊场景下“字符数”不等于 length()。普通业务不用纠结,真要做字数统计再用 codePointCount。
常见坑
1. equals 调用顺序反了
status.equals("ok") 在 status 为 null 时直接空指针;写成 "ok".equals(status),常量在前,null 也能安全返回 false。这是最便宜的防御性写法。
2. 混淆 null 和 "null"
String.valueOf(null) 得到字符串 "null",Objects.toString(null, "") 才得到空串。前端传 "null" 字符串、后端当成 null 处理,会导致查询条件多一个 name = 'null',数据莫名其妙查不到。
3. split 的特殊字符
split 的参数是正则表达式,.、|、*、+ 都有特殊含义。按点号切分要写 "a.b".split("\\."),否则会切出一个空数组。带结尾空串的场景还要传 -1:"a,b,,".split(",", -1)。
4. trim 去不掉的空白
trim() 只去掉 ASCII 码小于等于空格的字符,全角空格、不间断空格去不掉。JDK 11 之后可以用 strip(),它按 Unicode 空白判断,更符合直觉。
5. 在循环里累加字符串异常信息
很多人写 String msg = ""; 然后在 catch 里 msg += e.getMessage(),循环一大就拖慢主流程。异常信息本来就不该拼进业务返回值,直接抛出去或者记日志更合适。
验证清单
- 字符串内容比较全部使用
equals/equalsIgnoreCase; equals的常量放在前面,避免空指针;- 循环拼接使用
StringBuilder,并给了合理初始容量; split的正则特殊字符已转义,需要保留空串时传-1;- 判断空白使用了
isBlank/strip,而不是只用trim。
评论