期末java复习--string

二、String 底层原理 & 构造方法

2.1 类结构与底层存储

1. 类修饰与实现接口

java

运行

复制代码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc
  • final 修饰类 :String 不能被继承,防止子类破坏原有逻辑;
  • 实现多个接口:支持序列化、字符串比较、字符序列规范等能力。

2. 底层核心成员(JDK9+)

java

运行

复制代码
@Stable
private final byte[] value;
private final byte coder;
private int hash;
  1. private final byte[] value 真正存储字符的底层数组:

    • private:外部类无法直接访问、修改;
    • final数组引用地址不可修改,只能指向当前数组;
    • 结合访问权限 + 类设计:字符串内容一旦创建,就无法被修改(String 不可变的核心原因)。

    补充:JDK8 及以前使用 char[],JDK9 优化为 byte[],根据编码自动适配,节省内存。

  2. coder:标记编码格式(LATIN1 / UTF16),JDK 内部编码优化使用。

  3. hash:缓存字符串的哈希码,避免重复计算,提升效率。

总结:String 本身不直接存字符,底层依靠一个被 final 修饰的字节数组存储字符数据

2.2 四种常用字符串构造方式

结合你提供的代码,逐行讲解,并修正原代码笔误:

java

运行

复制代码
public static void main(String[] args) {
    // 1. 字符串常量赋值(字面量方式,最常用)
    String s1 = "hello bit";
    System.out.println(s1);

    // 2. new String(String 原串) 构造对象
    String s2 = new String("hello bit");
    System.out.println(s2);

    // 3. 通过字符数组构造字符串
    char[] array = {'h','e','l','l','o','b','i','t'};
    String s3 = new String(array);
    System.out.println(s3);

    // 4. 通过字节数组构造(字节对应 ASCII 码)
    byte[] bytes = {97,98,99,100}; // 97=a 98=b 99=c 100=d
    String s4 = new String(bytes);
    System.out.println(s4); // 输出 abcd
}

四种方式说明

  1. 字面量 String s = "xxx" 开发首选,会走字符串常量池,复用对象,节省内存。
  2. new String("xxx") 强制在堆内存创建新 String 对象,哪怕内容相同,也是不同实例。
  3. new String(char[]) 把字符数组转为字符串,底层会拷贝数组内容,后续修改原字符数组,不会影响字符串。
  4. new String(byte[]) 根据 ASCII 字节码转成对应字符,常用于编码、网络数据解析场景。

额外:更多构造方法可查阅你提供的 Java 17 String 官方文档


三、字符串常量池(StringTable)核心讲解

3.1 什么是「池化技术」

:提前创建一批资源放在 "容器" 中,后续使用直接取用,不用重复创建、销毁,提升效率、减少资源消耗

生活化举例:

  1. 每次用钱临时去取(频繁创建对象,效率低);
  2. 一次性把钱存入银行卡,随取随用(池化思想,复用资源)。

Java 中常见池:字符串常量池、线程池、数据库连接池。 字符串常量池(StringTable) 就是 JVM 为字符串准备的 "共享池",本质是一个 HashTable,用来缓存字符串,实现对象复用。

3.2 不同 JDK 版本常量池位置 & 大小

表格

JDK 版本 常量池位置 大小规则
Java 6 永久代(方法区) 固定大小,默认 1009
Java 7 堆内存 可配置,默认 60013,无上限
Java 8 堆内存 可配置,最小值限制为 1009

重点记忆:JDK7 之后,字符串常量池从方法区迁移到了堆内存

3.3 两种创建方式的内存分布(重中之重)

场景 1:字面量赋值 String str = "abc"

代码:

java

运行

复制代码
public static void main(String[] args) {
    String str1 = "abc";
    String str2 = "abc";
    System.out.println(str1 == str2); // 输出 true
}
内存执行流程
  1. 执行 String str1 = "abc": JVM 先去字符串常量池 查找有没有 "abc"
    • 没有:在常量池中创建 "abc" 对象,栈中 str1 引用指向常量池地址 0x112233
  2. 执行 String str2 = "abc": 再次去常量池查找,发现已存在 "abc"
    • 不再新建对象 ,栈中 str2 直接复用常量池地址 0x112233
结果解释

str1 == str2== 对于引用类型比较内存地址 ,两者地址完全一致,结果为 true


场景 2:new String("abc") 创建对象

代码:

java

运行

复制代码
public static void main(String[] args) {
    String str1 = new String("abc");
    String str2 = new String("abc");
    System.out.println(str1 == str2); // 输出 false
}
内存执行流程(一次 new 产生两个对象
  1. 执行 new String("abc"): ① 先处理双引号字面量 "abc":去常量池查找,不存在则在常量池 创建 "abc"; ② new 关键字:一定会在 堆内存 开辟空间,创建一个全新的 String 对象; ③ 堆内 String 对象的底层 value 数组,引用常量池里 "abc" 的字符数据。

  2. str1:栈引用 → 堆对象地址 0x998877

  3. 第二次 new String("abc"): ① 常量池已有 "abc",直接复用; ② 再次在堆中创建一个全新 String 对象 ,地址为 0x556677

  4. str2:栈引用 → 新堆对象地址 0x556677

结果解释

str1 == str2:两个引用指向堆中不同对象 ,地址不一样,结果为 false

核心结论总结

  1. 字面量 "" 创建字符串:优先使用常量池,对象复用,地址相同;
  2. new String() 创建字符串:必定在堆中新建对象,哪怕内容一致,地址也不同;
  3. new String("内容") 执行后:堆中对象 + 常量池字面量对象,一共 2 个对象;
  4. == 比较引用类型:只判断内存地址 ,不判断字符串内容。

四、补充易错点 & 拓展

1. 为什么 String 不可变?

  1. 底层 private final byte[] valuefinal 保证数组引用不能更换,private 外部无法修改数组内容;
  2. String 没有提供任何方法可以修改 value 数组;
  3. 所有 "修改" 操作(拼接、替换、截取)都会生成新 String 对象,原对象保持不变。

示例:

java

运行

复制代码
String s = "hello";
s = s + " world"; // 不是修改原对象,而是新建一个 "hello world" 对象

2. 常量池使用建议

  • 日常定义字符串、接收固定文本:优先使用字面量 "",利用常量池优化性能;
  • 只有需要手动根据数组 / 字节创建字符串时,才使用 new String(xxx) 构造方法。

3. 拓展:内容比较用 equals()

前面讲了 == 比较地址,如果想单纯比较字符串内容是否相同 ,必须使用 String 重写的 equals() 方法:

java

运行

一、String 对象的四种比较方式

核心区分:== 比地址equals 比内容、compareTo 字典序比较、compareToIgnoreCase 忽略大小写字典序比较。

3.1.1 == 运算符

核心规则

  1. 基本数据类型(int、double、boolean 等)== 比较变量存储的数值是否相等。
  2. 引用数据类型(String、数组、自定义对象等)== 比较两个引用的内存地址 ,判断是否指向同一个对象
代码逐行解析

java

运行

复制代码
public static void main(String[] args) {
    int a = 10;
    int b = 20;
    int c = 10;
    // 基本类型:比较值
    System.out.println(a == b); // 10 != 20 → false
    System.out.println(a == c); // 10 == 10 → true

    // 引用类型:比较内存地址
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("world");
    String s4 = s1; // s4 和 s1 指向同一个对象

    System.out.println(s1 == s2); 
    // 两个 new 出来的不同堆对象,地址不同 → false
    System.out.println(s2 == s3); 
    // 内容、地址都不同 → false
    System.out.println(s1 == s4); 
    // 同一地址,指向同一个对象 → true
}

补充结合常量池

java

运行

复制代码
String x = "hello";
String y = "hello";
System.out.println(x == y); // true  常量池复用,地址相同

3.1.2 equals() 方法

作用

专门比较两个字符串的内容是否完全一致不关心内存地址

Object 原生 equals 底层就是 ==String 重写了该方法,改为逐字符对比内容。

JDK17 源码解读

java

运行

复制代码
public boolean equals(Object anObject) {
    // 1. 先判断地址:如果是同一个对象,直接返回 true(短路优化)
    if (this == anObject) {
        return true;
    }
    // 2. 判断是否是 String 类型 + 编码一致 + 底层字节数组逐位对比
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}

// 底层数组逐字符比较
public static boolean equals(byte[] value, byte[] other) {
    // 长度不同 → 直接 false
    if (value.length == other.length) {
        // 逐个字符遍历对比
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) {
                return false;
            }
        }
        return true;
    }
    return false;
}

执行逻辑总结:

  1. 地址相同 → 内容必然相同,返回 true
  2. 不是 String 类型 → 直接 false
  3. 长度不一致 → false
  4. 长度一致,逐个字符对比 ,全相同才返回 true

示例代码解析

java

运行

复制代码
public static void main(String[] args) {
    String s1 = new String("hello");
    String s2 = new String("hello");
    String s3 = new String("Hello"); // 首字母大写

    // == 比较地址:三个都是不同对象
    System.out.println(s1 == s2);  // false
    System.out.println(s1 == s3);  // false

    // equals 比较内容
    System.out.println(s1.equals(s2)); // 字符完全一致 → true
    System.out.println(s1.equals(s3)); // h != H → 内容不同 → false
}

开发使用规范

登录、密码校验、文本匹配一律使用 equals ,禁止用 ==


3.1.3 compareTo() 字典序比较

作用

Unicode 字典顺序 比较字符串,返回 int 类型(不是布尔值)。

比较规则

  1. 第一个字符 开始逐个对比 Unicode 值:
    • 遇到不同字符:返回 当前字符的差值本字符 - 参数字符),结束比较;
  2. 前面所有字符全部相等:返回 两个字符串的长度差值本串长度 - 参数串长度);
  3. 两个字符串完全一样:返回 0

示例代码解析

java

运行

复制代码
public static void main(String[] args) {
    String s1 = new String("abc");
    String s2 = new String("ac");
    String s3 = new String("abc");
    String s4 = new String("abcdef");

    // s1: a b c
    // s2: a c
    // 第0位 a == a;第1位 b(98) vs c(99) → 98 - 99 = -1
    System.out.println(s1.compareTo(s2)); // -1

    // 内容、长度完全相同 → 0
    System.out.println(s1.compareTo(s3)); // 0

    // 前3个字符全部相等,比较长度:3 - 6 = -3
    System.out.println(s1.compareTo(s4)); // -3
}

结果含义

  • 返回 负数:当前字符串 < 参数字符串
  • 返回 0:两个字符串完全相等
  • 返回 正数:当前字符串 > 参数字符串

3.1.4 compareToIgnoreCase()

作用

逻辑和 compareTo 完全一致 ,唯一区别:忽略大小写进行字典序比较。

示例代码解析

java

运行

复制代码
public static void main(String[] args) {
    String s1 = new String("abc");
    String s2 = new String("ac");
    String s3 = new String("ABc"); // 大小写混合
    String s4 = new String("abcdef");

    System.out.println(s1.compareToIgnoreCase(s2)); // 字符差值 -1
    // 忽略大小写后 abc == ABc → 0
    System.out.println(s1.compareToIgnoreCase(s3)); // 0
    // 前序字符相同,长度差 3-6=-3
    System.out.println(s1.compareToIgnoreCase(s4)); // -3
}

适用场景

验证码、用户名、英文关键词匹配等不区分大小写的场景。


二、字符串查找常用方法(3.2)

一共 6 个高频查找方法,结合统一示例串:

示例字符串:"aaabbbcccaaabbbccc"

方法总览

表格

方法 功能 返回值
char charAt(int index) 获取指定下标处的字符 对应字符,下标越界抛异常
int indexOf(int ch) 头部 向后查找字符 / 子串第一次出现位置 找到返回下标,找不到返回 -1
int indexOf(int ch, int fromIndex) fromIndex 下标开始向后查找 同上
int lastIndexOf(int ch) 尾部 向前查找字符 / 子串最后一次出现位置 同上
int lastIndexOf(int ch, int fromIndex) fromIndex 下标开始向前查找 同上

逐行代码解析

java

运行

复制代码
public static void main(String[] args) {
    String s = "aaabbbcccaaabbbccc";
    // 下标:0 1 2 3 4 5 6 7 8 9 ...

    // 1. charAt(下标):获取对应位置字符
    System.out.println(s.charAt(3)); // 下标3 → 'b'

    // 2. indexOf(字符):从头往后找,第一次出现的下标
    System.out.println(s.indexOf('c')); // 第一个 c 在 6 号位 → 6

    // 3. indexOf(字符, 起始下标):从下标10开始往后找 c
    System.out.println(s.indexOf('c', 10)); // 15

    // 4. indexOf(子串):查找子串第一次出现位置
    System.out.println(s.indexOf("bbb")); // "bbb" 起始下标 3 → 3

    // 5. 从下标10开始,往后找 "bbb"
    System.out.println(s.indexOf("bbb", 10)); // 12

    // 6. lastIndexOf(字符):从后往前找,最后一次出现的下标
    System.out.println(s.lastIndexOf('c')); // 最后一个 c 在 17 → 17

    // 7. lastIndexOf(字符, 起始下标):从下标10开始往前找 c
    System.out.println(s.lastIndexOf('c', 10)); // 8

    // 8. lastIndexOf(子串):子串最后一次出现位置
    System.out.println(s.lastIndexOf("bbb")); // 12

    // 9. 从下标10开始往前找 "bbb"
    System.out.println(s.lastIndexOf("bbb", 10)); // 3
}

关键注意点

  1. 字符串下标从 0 开始,和数组规则一致;
  2. charAt(index):如果传入负数 / 大于等于字符串长度,直接抛出 IndexOutOfBoundsException 数组越界异常;
  3. indexOf / lastIndexOf 找不到目标时,固定返回 -1(不会抛异常);
  4. fromIndex 参数:
    • indexOf(..., fromIndex)向后搜索
    • lastIndexOf(..., fromIndex)向前搜索

三、知识点总结(背诵 + 做题要点)

1 比较方法选用场景

  1. 判断是不是同一个对象 → 用 ==
  2. 判断内容是否完全相等(区分大小写) → 用 equals()(业务最常用)
  3. 大小排序、字典序对比 → 用 compareTo()
  4. 字典序对比 + 忽略大小写 → 用 compareToIgnoreCase()

2 查找方法速记

  • 取单个字符:charAt(下标)
  • 正向查找(从头往后):indexOf
  • 反向查找(从尾往前):lastIndexOf
  • 指定起点查找:方法里加第二个参数 fromIndex
  • 查找失败统一返回:-1

3 高频易错点

  1. 不要用 == 判断字符串内容,只用来判断地址;

  2. equals 区分大小写,忽略大小写请结合 toUpperCase/toLowerCase 或直接用 compareToIgnoreCase

  3. 下标操作务必防止越界(charAt 会报错,indexOf 不会)。

    String s1 = new String("abc");
    String s2 = new String("abc");
    System.out.println(s1 == s2); // false 地址不同
    System.out.println(s1.equals(s2)); // true 内容相同

3.3 字符串转换方法

3.3.1 数值 / 对象 ↔ 字符串

分为两大方向:任意类型转字符串字符串转基本数值

1. 任意类型 → 字符串:String.valueOf()

valueOf()String 静态方法 ,支持:数字、布尔、字符、对象、数组等,是推荐写法

代码逐行解析

java

运行

复制代码
public static void main(String[] args) {
    // 整数转字符串
    String s1 = String.valueOf(1234);
    // 浮点数转字符串
    String s2 = String.valueOf(12.34);
    // 布尔值转字符串
    String s3 = String.valueOf(true);
    // 自定义对象转字符串(会调用对象的 toString())
    String s4 = String.valueOf(new Student("Hanmeimei", 18));

    System.out.println(s1);    // 输出:1234
    System.out.println(s2);    // 输出:12.34
    System.out.println(s3);    // 输出:true
    System.out.println(s4);    // 输出 学生对象的 toString 结果
}
补充写法(不推荐)

也可以用 "" + 数据 拼接实现转换:

plaintext

复制代码
int num = 567;
String str = "" + num;

缺点:可读性差,底层会产生额外临时对象,正式开发优先 String.valueOf()

2. 字符串 → 基本数值(包装类静态方法)

依赖 包装类Integer/Double/Float/Long

  • Integer.parseInt(String s):字符串转 int
  • Double.parseDouble(String s):字符串转 double
代码解析

java

运行

复制代码
// 字符串转整数
int data1 = Integer.parseInt("1234");
// 字符串转小数
double data2 = Double.parseDouble("12.34");

System.out.println(data1);  // 1234
System.out.println(data2);  // 12.34
⚠️ 重要异常(高频坑)

如果字符串不是合法数字格式 ,会抛出 NumberFormatException 数字格式异常:

java

运行

复制代码
Integer.parseInt("abc");   // 报错!无法转数字
Integer.parseInt("12a3");  // 报错!

使用场景:做表单、接口参数校验时,必须做好格式判断。


3.3.2 大小写转换

两个实例方法:

  • String toUpperCase()全部转为大写
  • String toLowerCase()全部转为小写

关键:String 不可变!调用后原字符串不变,只会返回新字符串。

代码解析

java

运行

复制代码
public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "HELLO";

    // 小写 → 大写
    System.out.println(s1.toUpperCase()); // HELLO
    // 大写 → 小写
    System.out.println(s2.toLowerCase()); // hello

    // 验证原字符串不变
    System.out.println(s1); // 仍然是 hello
}

常用场景

验证码、账号匹配、关键词检索(结合 equals 使用):

java

运行

复制代码
String code = "AbCd";
String input = "abcd";
// 统一小写后再比较
if(code.toLowerCase().equals(input.toLowerCase())){
    System.out.println("验证码正确");
}

3.3.3 字符串 ↔ 字符数组

两组核心用法,常用于遍历字符、字符修改、加密 / 解密等场景。

1. 字符串 → 字符数组:toCharArray()

2. 字符数组 → 字符串:new String(char[])

代码逐行解析

java

运行

复制代码
public static void main(String[] args) {
    String s = "hello";

    // 1. 字符串 转 字符数组
    char[] ch = s.toCharArray();
    // 遍历数组
    for (int i = 0; i < ch.length; i++) {
        System.out.print(ch[i]); // 输出 h e l l o
    }
    System.out.println();

    // 2. 字符数组 转 字符串
    String s2 = new String(ch);
    System.out.println(s2); // 输出 hello
}

补充特性

new String(char[]) 底层会拷贝数组内容,后续修改原字符数组,不会影响原字符串:

java

运行

复制代码
char[] arr = {'a','b','c'};
String str = new String(arr);
arr[0] = 'x'; // 修改数组
System.out.println(str); // 依然输出 abc,不受影响

3.3.4 字符串格式化 String.format()

作用

按照指定格式 拼接、组合字符串,类似 C 语言 printf,是静态方法

常用占位符

表格

占位符 含义
%d 整数(int/long)
%f 浮点数
%s 字符串
%c 单个字符

示例代码(日期格式化)

java

运行

复制代码
public static void main(String[] args) {
    // 格式:年-月-日
    String s = String.format("%d-%d-%d", 2019, 9, 14);
    System.out.println(s); // 输出:2019-9-14
}

拓展示例(多占位符练习)

java

运行

复制代码
// 拼接姓名+年龄
String name = "张三";
int age = 20;
String info = String.format("姓名:%s,年龄:%d", name, age);
System.out.println(info); // 姓名:张三,年龄:20

三、整体总结 & 做题要点

1. 转换方法速查表

  1. 任意类型 → 字符串
    • 首选:String.valueOf(数据)
  2. 字符串 → 数字
    • 整数:Integer.parseInt()
    • 小数:Double.parseDouble()
    • 注意:非数字串会抛 NumberFormatException
  3. 大小写转换
    • 转大写:toUpperCase()
    • 转小写:toLowerCase()
    • 原字符串不变,接收返回值
  4. 字符串 ↔ 字符数组
    • 串 → 数组:toCharArray()
    • 数组 → 串:new String(字符数组)
  5. 格式化拼接
    • String.format(格式串, 参数...),常用 %d %s %f

2. 通用铁律(String 所有方法)

String 不可变 : 所有转换、修改、截取、大小写方法,都不会改变原字符串,必须用变量接收返回结果。

错误示范:

java

运行

复制代码
String s = "abc";
s.toUpperCase(); 
System.out.println(s); // 还是 abc,没有变化

正确写法:

java

运行

复制代码
String s = "abc";
String newStr = s.toUpperCase();
System.out.println(newStr); // ABC

.4 字符串替换

两个核心方法

表格

方法 作用
String replaceAll(String regex, String replacement) 全局替换:匹配到的内容全部替换
String replaceFirst(String regex, String replacement) 单次替换 :只替换第一个匹配到的内容

重要说明:两个方法的第一个参数是正则表达式,普通字符直接写即可。

示例代码解析

java

运行

复制代码
public static void main(String[] args) {
    String str = "helloworld";
    // 把所有 l 替换成 _
    System.out.println(str.replaceAll("l", "_")); // he__owor_d
    // 只把第一个 l 替换成 _
    System.out.println(str.replaceFirst("l", "_")); // he_loworld

    // 原字符串不变
    System.out.println(str); // helloworld
}

补充拓展

还有一个纯文本替换方法 replace(CharSequence target, CharSequence replacement)不使用正则,只做普通文本替换,适合纯字符场景:

java

运行

复制代码
String s = "a-b-c";
System.out.println(s.replace("-", "#")); // a#b#c

3.5 字符串拆分 split()

将字符串按照指定分隔符切割成 字符串数组,是开发高频用法。

1. 两个重载方法

  1. String[] split(String regex) 全部分割,忽略末尾空字符串。
  2. String[] split(String regex, int limit) 限定分割后的组数 ,最多拆成 limit 份。

示例 1:基础拆分(按空格)

java

运行

复制代码
public static void main(String[] args) {
    String str = "hello world hello bit";
    // 全部拆分
    String[] result = str.split(" ");
    for (String s : result) {
        System.out.println(s);
    }
}

输出:

plaintext

复制代码
hello
world
hello
bit

示例 2:限定拆分组数

java

运行

复制代码
String str = "hello world hello bit";
// 只拆成 2 组
String[] result = str.split(" ", 2);
for (String s : result) {
    System.out.println(s);
}

输出:

plaintext

复制代码
hello
world hello bit

🔴 重点:特殊分隔符转义规则

split 的参数是正则表达式,. | * + \ 都是正则特殊字符,不能直接写,必须转义:

  1. 分割 . | * + → 写法:\\.\\|\\*\\+
  2. 分割反斜杠 \ → 写法:\\\\(Java 字符串两层转义 + 正则一层转义)
示例 3:拆分 IP 地址(. 转义)

java

运行

复制代码
String str = "192.168.1.1";
String[] result = str.split("\\.");
for (String s : result) {
    System.out.println(s);
}

输出:

plaintext

复制代码
192
168
1
1

示例 4:多分隔符(| 表示 "或")

| 连接多个分隔符,实现多符号同时拆分

多层拆分实战(经典键值对)

java

运行

复制代码
public static void main(String[] args) {
    String str = "name=zhangsan&age=18";
    // 第一步:按 & 拆分
    String[] result = str.split("&");
    for (int i = 0; i < result.length; i++) {
        // 第二步:按 = 继续拆分
        String[] temp = result[i].split("=");
        System.out.println(temp[0] + " = " + temp[1]);
    }
}

输出:

plaintext

复制代码
name = zhangsan
age = 18

拆分常见坑

连续空格拆分:"a b c".split(" ") 会产生空元素,建议使用 split("\\s+") 匹配任意空白


3.6 字符串截取 substring()

从原字符串中截取子串,下标从 0 开始

两个重载方法

  1. String substring(int beginIndex)beginIndex 下标一直截取到字符串末尾
  2. String substring(int beginIndex, int endIndex) 截取区间:左闭右开 [beginIndex , endIndex) ✅ 包含起始下标,不包含结束下标。

代码示例

java

运行

复制代码
public static void main(String[] args) {
    String str = "helloworld";
    // 从下标 5 截取到末尾
    System.out.println(str.substring(5));  // world

    // 截取 [0,5) 下标:0、1、2、3、4
    System.out.println(str.substring(0, 5)); // hello
}

易错提醒

  1. 下标为负数 / 超过字符串长度 → 抛出 IndexOutOfBoundsException
  2. beginIndex > endIndex 也会报越界异常。

3.7 去除首尾空格 trim()

方法作用

String trim() 只删除字符串左右两侧的空格中间空格保留

代码示例

java

运行

复制代码
public static void main(String[] args) {
    String str = " hello world ";
    // 原字符串(带前后空格)
    System.out.println("[" + str + "]");        // [ hello world ]
    // 去除首尾空格
    System.out.println("[" + str.trim() + "]"); // [hello world]
}

拓展(JDK11+ 新增)

  • strip():去除首尾所有空白字符 (空格、制表符、全角空格等),比 trim 功能更强;
  • stripLeading():只去除开头空白;
  • stripTrailing():只去除末尾空白。

整体核心总结

  1. 统一规则 String 不可变:replace / split / substring / trim不会修改原串,必须接收返回值。

  2. 替换方法区分

    • replaceAll:全部替换;replaceFirst:只换第一个;
    • 参数为正则,特殊字符注意转义。
  3. 拆分 split 必考

    • 特殊符 . | * + \ 必须转义:\\.\\\\
    • | 在正则中代表 "或",可实现多分隔符拆分;
    • split(分隔符, limit) 控制拆分组数。
  4. 截取 substring

    • 单参数:从下标截到末尾;
    • 双参数:左闭右开区间,牢记不包含结束下标。
  5. 去空格

    • trim():只清首尾普通空格 ,中间不动。

      3.8 intern() 方法 完整讲解

      一、方法定义

      String intern()String 实例方法 ,作用是手动将字符串对象存入 / 复用字符串常量池

      核心规则(两条)

    • 调用 s.intern() 时,先去字符串常量池 查找:

      • 若池中已存在 内容相同的字符串(用 equals 判断相等):直接返回常量池里该字符串的引用
    • 若池中不存在 相同内容:

      • 把当前 String 对象放入常量池,再返回池内这个对象的引用
    • 一句话记:intern() 最终一定返回常量池中的引用


      二、结合代码分步解析

      原始代码(未开启 intern()

      java

      运行

      复制代码
      public static void main(String[] args) {
          char[] ch = new char[]{'a', 'b', 'c'};
          // 1. 通过字符数组 new 对象:s1 在【堆】中,不在常量池
          String s1 = new String(ch);
          
          // s1.intern();  // 代码暂时注释
          
          // 2. 字面量赋值:s2 直接指向【常量池】中的 "abc"
          String s2 = "abc";
      
          // 3. == 比较内存地址
          System.out.println(s1 == s2); // 输出 false
      }

      内存分析

    • String s1 = new String(ch): 底层在堆内存 创建新 String 对象,不会自动进入常量池

    • String s2 = "abc": 字面量形式,JVM 先查常量池,没有则创建 "abc"s2 指向常量池地址

    • 对比: s1 → 堆地址,s2 → 常量池地址,地址不同 ,所以 == 结果为 false


    开启 s1.intern() 之后(解除注释)

    java

    运行

    复制代码
      public static void main(String[] args) {
          char[] ch = new char[]{'a', 'b', 'c'};
          String s1 = new String(ch);
    
          // 手动调用 intern()
          s1.intern();
    
          String s2 = "abc";
          System.out.println(s1 == s2); // 输出 true
      }

    执行流程 & 内存变化

    • s1 = new String(ch):堆中创建对象,不在常量池。
    • 执行 s1.intern()
      • 去常量池查找 "abc"目前不存在
      • s1 对应的字符串引用存入常量池
    • 执行 String s2 = "abc"
      • 字面量去常量池查找,发现已经存在 "abc"
      • s2 直接复用常量池中的引用
    • 现在: s1.intern() 把引用推入常量池 → s1s2 最终指向同一个常量池地址 ,因此 == 结果为 true

    三、补充关键细节 & 易错点

    1. 注意:intern() 有返回值

    上面代码是简化写法,标准写法建议接收返回值,语义更清晰:

    java

    运行

    复制代码
      char[] ch = {'a','b','c'};
      String s1 = new String(ch);
      // 接收 intern 返回的常量池引用
      String s1Pool = s1.intern(); 
    
      String s2 = "abc";
      System.out.println(s1Pool == s2); // true

    误区:很多人以为 intern()修改原 s1 的地址 ,本质不会修改原对象,只是返回常量池引用

    2. 两种创建方式 + intern 组合对比

    场景 1:先字面量,再 new + intern

    java

    运行

    复制代码
      String s2 = "abc";        // 常量池先创建 abc
      String s1 = new String(ch);
      String s1Pool = s1.intern(); 
      System.out.println(s1Pool == s2); // true

    流程:常量池已有 "abc"intern 直接返回池内已有引用。

    场景 2:重复 new 调用 intern

    java

    运行

    复制代码
      String a = new String("test");
      String b = new String("test");
      String a1 = a.intern();
      String b1 = b.intern();
      System.out.println(a1 == b1); // true

    两个堆对象,intern 后都指向常量池同一地址。

    3. 使用场景

    • 大量重复字符串的场景(日志、报文、批量文本),手动调用 intern() 复用常量池对象,减少堆内存占用
    • 面试高频考点:区分 new String、字面量、intern 三者的内存地址关系。

    四、总结

    • intern() 作用:将字符串尝试放入 / 复用字符串常量池,返回池内引用;

    • 规则:池有就复用,池无就存入再返回;

    • new String(字符数组/字符串) 创建的对象默认在堆,不会进常量池 ,必须手动 intern()

    • 字面量 "" 创建字符串,默认直接走常量池

    • == 永远比较地址,这是判断结果的核心依据。

      先回顾核心知识点:

    • new String("hello"):会在 创建新对象,同时把字面量 hello 存入字符串常量池

    • intern():返回常量池中同内容字符串的引用;

    • ==:引用类型比较内存地址


    一、代码执行分步拆解

    java

    运行

    复制代码
      String s1 = new String("hello");
    • 执行流程

      • 先处理双引号字面量 "hello":检查常量池,不存在则在常量池中创建 hello
      • 再执行 new:在堆内存 创建一个全新 String 对象;
      • 栈变量 s1 指向堆对象地址
    • java

      运行

      复制代码
      String s2 = s1.intern();
    • 调用 s1.intern():去常量池查找内容为 hello 的字符串;

    • 此时常量池已经存在 该对象,所以直接返回常量池的引用

    • 栈变量 s2 接收该返回值 → s2 指向常量池地址

    • java

      运行

      复制代码
      String s3 = "hello";
    • 字面量赋值,优先走常量池;

    • 常量池中已有 hello,直接复用已有引用;

    • 栈变量 s3 也指向常量池地址


    二、两行输出结果

    1. System.out.println(s2 == s3);

    s2s3 指向同一个常量池地址 ✅ 结果:true

    2. System.out.println(s1 == s3);

    s1 指向堆对象地址s3 指向常量池地址 ,地址不同 ✅ 结果:false


    三、完整内存示意图(简化)

    plaintext

    复制代码
      常量池:"hello"  地址0x001
      堆内存:new String对象  地址0x101
    
      s1 → 0x101(堆)
      s2 → 0x001(常量池)
      s3 → 0x001(常量池)
    
      s2 == s3 → 地址相同 → true
      s1 == s3 → 地址不同 → false

    四、补充易错提醒

    • intern() 不会改变原对象 s1 的地址 ,它只是返回常量池引用,必须用变量接收;

    • new String("xxx") 一上来就会把字面量放入常量池,和之前 new String(字符数组) 的场景有区别;

    • 只要是双引号字面量,一律优先使用字符串常量池。

    • 画个图给我理解一下

      结合代码、内存分区(栈、堆、字符串常量池),用文字示意图 + 分步图解,清晰展示内存地址与引用关系,通俗易懂。

      待分析代码

      java

      运行

      复制代码
      public static void main(String[] args) {
          String s1 = new String("hello");
          String s2 = s1.intern();
          String s3 = "hello";
          System.out.println(s2 == s3);  // true
          System.out.println(s1 == s3);  // false
      }

      前置说明

    • 栈(Stack) :存放局部变量(s1/s2/s3 这些引用变量);

    • 堆(Heap)new 出来的对象都存在这里;

    • 字符串常量池:独立区域,存放双引号字面量字符串,实现对象复用;

    • 引用变量:存的是内存地址== 比较就是比地址。


    第一步:执行 String s1 = new String("hello");

    执行逻辑

    new String("hello")产生两个对象

    • 字面量 "hello" → 存入字符串常量池
    • new 新建对象 → 存入堆内存

    内存图示

    plaintext

    复制代码
      ==================== 字符串常量池 ====================
      地址:0x0001
      内容:"hello"
      ====================================================
    
      ==================== 堆内存 ==========================
      地址:0x1001
      对象:String 对象(内部引用常量池的 "hello")
      ====================================================
    
      ==================== 栈(main方法局部变量)============
      s1  ------>  地址 0x1001  (指向堆中的对象)
      ====================================================
    • s1 是栈里的变量,保存堆对象地址 0x1001
    • 常量池已有 "hello"(地址 0x0001

    第二步:执行 String s2 = s1.intern();

    intern() 规则

    字符串常量池查找同内容字符串:

    • 找到了(0x0001 位置的 "hello"
    • 直接返回常量池的地址 ,赋值给 s2

    内存图示(更新后)

    plaintext

    复制代码
      ==================== 字符串常量池 ====================
      地址:0x0001
      内容:"hello"
      ====================================================
    
      ==================== 堆内存 ==========================
      地址:0x1001
      对象:String 对象
      ====================================================
    
      ==================== 栈 ==============================
      s1  ------>  0x1001 (堆地址,不变)
      s2  ------>  0x0001 (指向常量池地址)
      ====================================================

    ⚠️ 重点: intern() 不会修改 s1 的地址 ,只是把常量池地址返回给 s2


    第三步:执行 String s3 = "hello";

    执行逻辑

    字面量赋值,优先去常量池查找:

    • 常量池已经存在 "hello"0x0001
    • 直接复用该地址,赋值给 s3

    最终完整内存图

    plaintext

    复制代码
      ==================== 字符串常量池 ====================
      地址:0x0001
      内容:"hello"
      ====================================================
    
      ==================== 堆内存 ==========================
      地址:0x1001
      对象:new String 生成的对象
      ====================================================
    
      ==================== 栈(局部变量)===================
      s1  ------>  0x1001   【堆地址】
      s2  ------>  0x0001   【常量池地址】
      s3  ------>  0x0001   【常量池地址】
      ====================================================

    第四步:分析两行打印结果

    • System.out.println(s2 == s3);

      • s2 地址:0x0001
      • s3 地址:0x0001
      • 地址相同 → 输出 true
    • System.out.println(s1 == s3);

      • s1 地址:0x1001(堆)
      • s3 地址:0x0001(常量池)
      • 地址不同 → 输出 false

    补充对比(拓展:字符数组 new 的场景)

    回顾之前代码:

    java

    运行

    复制代码
      char[] ch = {'a','b','c'};
      String s1 = new String(ch); // 仅堆对象,常量池暂无 "abc"
      s1.intern();                // 把引用放入常量池
      String s2 = "abc";

    对应简易图:

    plaintext

    复制代码
      初始:
      堆:0x2001 → s1
      常量池:无
    
      执行 s1.intern() 后:
      常量池:0x0002 (存入 abc)
      栈 s1 → 0x2001
      栈 s2 → 0x0002
      s1.intern() 返回 0x0002,所以 s2 和 intern 结果地址一致 → true

    核心总结(看图记忆)

    • new String("xxx")堆 + 常量池 各一个对象

    • intern():只返回常量池地址,原变量引用不变;

    • 双引号字面量 "":永远优先使用常量池;

    • == 全程只比较栈变量保存的内存地址

      StringBuilder 完整用法

      2.1 四种构造方法

      用于创建 StringBuilder 对象,共 4 个常用构造器:

      java

      运行

      复制代码
      // 1. 无参构造:默认初始容量 16(最多存16个字符)
      StringBuilder sb1 = new StringBuilder();
      
      // 2. 指定初始容量(提前预估长度,减少扩容,提升性能)
      StringBuilder sb2 = new StringBuilder(50);
      
      // 3. 使用字符串初始化,容量 = 字符串长度 + 16
      StringBuilder sb3 = new StringBuilder("Java");
      
      // 4. 通过字符序列初始化(了解即可)
      StringBuilder sb4 = new StringBuilder(new CharSequence(){});

      两个概念区分(高频考点)

    • length()实际字符个数(有效长度)

    • capacity()底层数组总容量(最大可存储字符数,满了自动扩容)

    • 示例:

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("abc");
      System.out.println(sb.length());   // 3  实际字符数
      System.out.println(sb.capacity());// 3+16 = 19  底层容量

      2.2 核心常用方法(附代码 + 释义)

      所有方法都会直接修改原对象 ,大部分支持链式调用

      1. 追加 append () 【最常用】

      字符串末尾追加内容,支持任意数据类型(字符、数字、字符串、布尔等)

      java

      运行

      复制代码
      public static void main(String[] args) {
          StringBuilder sb = new StringBuilder();
          sb.append("Hello");
          sb.append(' ');
          sb.append(123);
          sb.append(true);
      
          System.out.println(sb); // Hello 123true
      
          // 链式调用(写法更简洁)
          StringBuilder sb2 = new StringBuilder();
          sb2.append("A").append("B").append(666);
          System.out.println(sb2); // AB666
      }

      2. 插入 insert ()

      指定下标位置 插入内容,下标从 0 开始 语法:insert(int 下标, 内容)

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("123");
      sb.insert(0, "开始"); // 下标0位置插入
      System.out.println(sb); // 开始123
      
      sb.insert(3, "-");
      System.out.println(sb); // 开始-123

      3. 删除方法

      ① delete (int start, int end) 删除区间(左闭右开 [start,end)

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("abcdef");
      sb.delete(1, 4); // 删除下标1、2、3
      System.out.println(sb); // aef
      ② deleteCharAt (int index) 删除单个下标字符

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("abcdef");
      sb.deleteCharAt(0); // 删除第一个字符
      System.out.println(sb); // bcdef

      4. 修改字符 setCharAt ()

      修改指定下标的单个字符,只改字符,不增删长度

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("test");
      sb.setCharAt(0, 'T');
      System.out.println(sb); // Test

      5. 替换 replace ()

      替换指定区间的内容,语法:replace(start, end, 新字符串)

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("我是学生");
      sb.replace(2, 4, "程序员");
      System.out.println(sb); // 我是程序员

      6. 查找 indexOf /lastIndexOf

    • indexOf(字符串):从左往右找,返回首次下标 ,找不到返回 -1

    • lastIndexOf(字符串):从右往左找,返回最后一次下标

    • java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("java python java");
      System.out.println(sb.indexOf("java"));    // 0
      System.out.println(sb.lastIndexOf("java")); // 12

      7. 截取 substring ()

      截取子串,返回新 String 对象 ,不会修改原 StringBuilder

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("abcdef");
      String str = sb.substring(1, 4); // [1,4)
      System.out.println(str); // bcd
      System.out.println(sb);  // 原对象不变:abcdef

      8. 反转 reverse ()

      原地反转字符序列,经典面试题

      java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("12345");
      sb.reverse();
      System.out.println(sb); // 54321

      9. 互转方法(String ↔ StringBuilder)

    • String → StringBuilder

    • java

      运行

      复制代码
      String str = "Java";
      // 方式1:构造方法
      StringBuilder sb1 = new StringBuilder(str);
      // 方式2:append
      StringBuilder sb2 = new StringBuilder();
      sb2.append(str);
    • StringBuilder → String

    • java

      运行

      复制代码
      StringBuilder sb = new StringBuilder("Hello");
      String res = sb.toString(); // 转为普通字符串

      2.3 辅助容量方法(了解)

    • ensureCapacity(int minCap):手动扩容,保证底层容量至少为指定值

    • trimToSize():收缩容量,把底层数组大小精简为实际字符长度,节省内存

    • java

      运行

      复制代码
      StringBuilder sb = new StringBuilder(100); // 初始容量100
      sb.append("abc");
      sb.trimToSize(); // 容量收缩为3

      三、StringBuilder & StringBuffer 核心区别

      二者方法名、功能、用法完全一致 ,唯一差异集中在 线程安全、性能、使用场景

      3.1 核心对比表

      表格

      特性 StringBuilder StringBuffer
      线程安全 ❌ 非线程安全(无同步锁) ✅ 线程安全(加synchronized锁)
      运行性能 速度最快 速度略慢(锁消耗性能)
      诞生版本 JDK 1.5 JDK 1.0
      底层实现 可变字符数组 可变字符数组
      推荐使用场景 单线程(绝大多数场景) 多线程并发操作字符串

      3.2 源码差异(直观理解)

      打开二者 append 方法源码:

      java

      运行

      复制代码
      // 1. StringBuffer 方法:带 synchronized 同步锁
      @Override
      public synchronized StringBuffer append(String str) {
          super.append(str);
          return this;
      }
      
      // 2. StringBuilder 方法:无锁
      @Override
      public StringBuilder append(String str) {
          super.append(str);
          return this;
      }
    • synchronized:Java 同步关键字,保证多线程下同一时间只有一个线程执行该方法,数据不会错乱

    • 加锁会产生额外开销,所以 StringBuffer 性能更低。

    3.3 场景举例

    示例 1:单线程(优先 StringBuilder)

    日常业务、普通循环拼接,单线程环境:

    java

    运行

    复制代码
      // 单线程拼接 1~100
      public static void main(String[] args) {
          StringBuilder sb = new StringBuilder();
          for (int i = 1; i <= 100; i++) {
              sb.append(i).append(",");
          }
          System.out.println(sb);
      }

    示例 2:多线程(必须用 StringBuffer)

    多个线程同时修改同一个字符串,为防止数据错乱:

    java

    运行

    复制代码
      public class Test {
          public static void main(String[] args) {
              // 多线程共享对象,使用 StringBuffer 保证安全
              StringBuffer sbf = new String();
    
              // 线程1
              new Thread(() -> {
                  for (int i = 0; i < 1000; i++) {
                      sbf.append("A");
                  }
              }).start();
    
              // 线程2
              new Thread(() -> {
                  for (int i = 0; i < 1000; i++) {
                      sbf.append("B");
                  }
              }).start();
          }
      }

    多线程场景如果误用 StringBuilder,会出现数据丢失、内容错乱


    四、String / StringBuilder / StringBuffer 三者总对比

    表格

    可变性 线程安全 性能 适用场景
    String 不可变 安全 差(频繁拼接极差) 字符串内容固定、仅查询、常量
    StringBuilder 可变 不安全 最优 单线程、频繁增删改(首选
    StringBuffer 可变 安全 一般 多线程并发操作字符串

    五、高频易错点总结

    • 不可变区分 String 修改产生新对象;StringBuilder/StringBuffer 直接修改原对象,不新建。
    • 区间规则 delete() / substring() / replace() 都是 左闭右开 [start, end)
    • 容量 & 长度 length() 是实际字符数;capacity() 是底层数组总容量。
    • 互转不能直接赋值 必须用构造器 / toString(),不能 String s = sb 直接赋值。
    • 选型口诀 单线程用 StringBuilder,多线程用 StringBuffer,内容不变就用 String
    • 循环拼接禁忌 绝对不要在大循环中使用 String +=,性能灾难。
相关推荐
Survivor0011 小时前
高并发系统流量治理的底层算法
java·开发语言
凡人叶枫1 小时前
Effective C++ 条款35:考虑 virtual 函数以外的其他选择
java·c++·spring
郝学胜-神的一滴1 小时前
CMake 017:彩色日志输出实战
linux·c语言·开发语言·c++·软件工程·软件构建·cmake
garmin Chen1 小时前
从 Transformer 到 Agent:大模型技术全景解析
java·人工智能·python·深度学习·transformer
m0_547486662 小时前
《数字图像处理:使用MATLAB分析与实现》全套课件PPT
开发语言·matlab·powerpoint
愚公移码2 小时前
蓝凌EKP18产品:流程引擎技术篇之流程核心概念模型
java·人工智能·流程引擎·蓝凌
没有钱的钱仔2 小时前
pytorch_cuda安装
人工智能·pytorch·python
Full Stack Developme2 小时前
Apache Tika 教程
java·开发语言·python·apache
luj_17682 小时前
FreeDOS vs MS-DOS PC-DOS 对比解析
服务器·c语言·开发语言·经验分享·算法