A13 String 详解:不可变、常量池、equals 与 ==、性能与常见坑

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(配坑点)》

从这一篇开始,你会明显感觉"写业务更顺手":很多常见需求都有现成工具类可用。

相关推荐
知无不研2 小时前
内存碎片与内存优化
开发语言·c++·内存优化·内存碎片·内存操作
invicinble2 小时前
学习的门道和思路
java·开发语言·学习
m0_561359672 小时前
C++模块接口设计
开发语言·c++·算法
矢志航天的阿洪2 小时前
从GitHub到本地:Python IGRF库环境配置完全指南
开发语言·python·github
从此不归路2 小时前
Qt5 进阶【11】图形视图框架:用 QGraphicsView 搭一个流程图编辑器
开发语言·c++·qt
老骥伏枥~2 小时前
【C# 入门】程序结构与 Main 方法
开发语言·c#
xyq20242 小时前
Scala IF...ELSE 语句
开发语言
wengqidaifeng2 小时前
探索数据结构(二):空间复杂度
c语言·开发语言·数据结构
难得的我们2 小时前
单元测试在C++项目中的实践
开发语言·c++·算法