ElIauk

Java 字符串:String、StringBuilder 与 equals

String 是不可变对象 Java 里的 String 一旦创建就不能修改。s += "x" 看起来是在原字符串后面追加,实际上是新造了一个字符串对象,再把变量指向它。原来的对象如果没有别的引用,就只能等 GC 回收。 不可变带来两个好处:可以安全地共享(字符串常量池)、可以作为 HashMap

评论
Java 字符串:String、StringBuilder 与 equals

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。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者