二、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;
-
private final byte[] value真正存储字符的底层数组:private:外部类无法直接访问、修改;final:数组引用地址不可修改,只能指向当前数组;- 结合访问权限 + 类设计:字符串内容一旦创建,就无法被修改(String 不可变的核心原因)。
补充:JDK8 及以前使用
char[],JDK9 优化为byte[],根据编码自动适配,节省内存。 -
coder:标记编码格式(LATIN1 / UTF16),JDK 内部编码优化使用。 -
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
}
四种方式说明
- 字面量
String s = "xxx"开发首选,会走字符串常量池,复用对象,节省内存。 new String("xxx")强制在堆内存创建新 String 对象,哪怕内容相同,也是不同实例。new String(char[])把字符数组转为字符串,底层会拷贝数组内容,后续修改原字符数组,不会影响字符串。new String(byte[])根据 ASCII 字节码转成对应字符,常用于编码、网络数据解析场景。
额外:更多构造方法可查阅你提供的 Java 17 String 官方文档。
三、字符串常量池(StringTable)核心讲解
3.1 什么是「池化技术」
池 :提前创建一批资源放在 "容器" 中,后续使用直接取用,不用重复创建、销毁,提升效率、减少资源消耗。
生活化举例:
- 每次用钱临时去取(频繁创建对象,效率低);
- 一次性把钱存入银行卡,随取随用(池化思想,复用资源)。
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
}
内存执行流程
- 执行
String str1 = "abc": JVM 先去字符串常量池 查找有没有"abc";- 没有:在常量池中创建
"abc"对象,栈中str1引用指向常量池地址0x112233。
- 没有:在常量池中创建
- 执行
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 产生两个对象)
-
执行
new String("abc"): ① 先处理双引号字面量"abc":去常量池查找,不存在则在常量池 创建"abc"; ②new关键字:一定会在 堆内存 开辟空间,创建一个全新的 String 对象; ③ 堆内 String 对象的底层value数组,引用常量池里"abc"的字符数据。 -
str1:栈引用 → 堆对象地址0x998877 -
第二次
new String("abc"): ① 常量池已有"abc",直接复用; ② 再次在堆中创建一个全新 String 对象 ,地址为0x556677; -
str2:栈引用 → 新堆对象地址0x556677
结果解释
str1 == str2:两个引用指向堆中不同对象 ,地址不一样,结果为 false。
核心结论总结
- 字面量
""创建字符串:优先使用常量池,对象复用,地址相同; new String()创建字符串:必定在堆中新建对象,哪怕内容一致,地址也不同;new String("内容")执行后:堆中对象 + 常量池字面量对象,一共 2 个对象;==比较引用类型:只判断内存地址 ,不判断字符串内容。
四、补充易错点 & 拓展
1. 为什么 String 不可变?
- 底层
private final byte[] value:final保证数组引用不能更换,private外部无法修改数组内容; - String 没有提供任何方法可以修改
value数组; - 所有 "修改" 操作(拼接、替换、截取)都会生成新 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 == 运算符
核心规则
- 基本数据类型(int、double、boolean 等) :
==比较变量存储的数值是否相等。 - 引用数据类型(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;
}
执行逻辑总结:
- 地址相同 → 内容必然相同,返回
true; - 不是 String 类型 → 直接
false; - 长度不一致 →
false; - 长度一致,逐个字符对比 ,全相同才返回
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 类型(不是布尔值)。
比较规则
- 从第一个字符 开始逐个对比 Unicode 值:
- 遇到不同字符:返回 当前字符的差值 (
本字符 - 参数字符),结束比较;
- 遇到不同字符:返回 当前字符的差值 (
- 前面所有字符全部相等:返回 两个字符串的长度差值 (
本串长度 - 参数串长度); - 两个字符串完全一样:返回
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
}
关键注意点
- 字符串下标从 0 开始,和数组规则一致;
charAt(index):如果传入负数 / 大于等于字符串长度,直接抛出IndexOutOfBoundsException数组越界异常;indexOf / lastIndexOf找不到目标时,固定返回 -1(不会抛异常);fromIndex参数:indexOf(..., fromIndex):向后搜索lastIndexOf(..., fromIndex):向前搜索
三、知识点总结(背诵 + 做题要点)
1 比较方法选用场景
- 判断是不是同一个对象 → 用
== - 判断内容是否完全相等(区分大小写) → 用
equals()(业务最常用) - 大小排序、字典序对比 → 用
compareTo() - 字典序对比 + 忽略大小写 → 用
compareToIgnoreCase()
2 查找方法速记
- 取单个字符:
charAt(下标) - 正向查找(从头往后):
indexOf - 反向查找(从尾往前):
lastIndexOf - 指定起点查找:方法里加第二个参数
fromIndex - 查找失败统一返回:
-1
3 高频易错点
-
不要用
==判断字符串内容,只用来判断地址; -
equals区分大小写,忽略大小写请结合toUpperCase/toLowerCase或直接用compareToIgnoreCase; -
下标操作务必防止越界(
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):字符串转 intDouble.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. 转换方法速查表
- 任意类型 → 字符串
- 首选:
String.valueOf(数据)
- 首选:
- 字符串 → 数字
- 整数:
Integer.parseInt() - 小数:
Double.parseDouble() - 注意:非数字串会抛
NumberFormatException
- 整数:
- 大小写转换
- 转大写:
toUpperCase() - 转小写:
toLowerCase() - 原字符串不变,接收返回值
- 转大写:
- 字符串 ↔ 字符数组
- 串 → 数组:
toCharArray() - 数组 → 串:
new String(字符数组)
- 串 → 数组:
- 格式化拼接
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. 两个重载方法
String[] split(String regex)全部分割,忽略末尾空字符串。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 的参数是正则表达式,. | * + \ 都是正则特殊字符,不能直接写,必须转义:
- 分割
.|*+→ 写法:\\.、\\|、\\*、\\+ - 分割反斜杠
\→ 写法:\\\\(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 开始。
两个重载方法
String substring(int beginIndex)从beginIndex下标一直截取到字符串末尾。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
}
易错提醒
- 下标为负数 / 超过字符串长度 → 抛出
IndexOutOfBoundsException 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():只去除末尾空白。
整体核心总结
-
统一规则 String 不可变:
replace/split/substring/trim都不会修改原串,必须接收返回值。 -
替换方法区分
replaceAll:全部替换;replaceFirst:只换第一个;- 参数为正则,特殊字符注意转义。
-
拆分 split 必考
- 特殊符
. | * + \必须转义:\\.、\\\\; |在正则中代表 "或",可实现多分隔符拆分;split(分隔符, limit)控制拆分组数。
- 特殊符
-
截取 substring
- 单参数:从下标截到末尾;
- 双参数:左闭右开区间,牢记不包含结束下标。
-
去空格
-
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()把引用推入常量池 →s1、s2最终指向同一个常量池地址 ,因此==结果为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);s2和s3指向同一个常量池地址 ✅ 结果:true2.
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地址:0x0001s3地址: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); // 开始-1233. 删除方法
① 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); // bcdef4. 修改字符 setCharAt ()
修改指定下标的单个字符,只改字符,不增删长度
java
运行
StringBuilder sb = new StringBuilder("test"); sb.setCharAt(0, 'T'); System.out.println(sb); // Test5. 替换 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")); // 127. 截取 substring ()
截取子串,返回新 String 对象 ,不会修改原
StringBuilderjava
运行
StringBuilder sb = new StringBuilder("abcdef"); String str = sb.substring(1, 4); // [1,4) System.out.println(str); // bcd System.out.println(sb); // 原对象不变:abcdef8. 反转 reverse ()
原地反转字符序列,经典面试题
java
运行
StringBuilder sb = new StringBuilder("12345"); sb.reverse(); System.out.println(sb); // 543219. 互转方法(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 +=,性能灾难。
-