Java String 全面解析:从源码到常量池,再到面试高频题
目录
一、源码分析(JDK8)
成员变量
构造函数
常见方法
二、String常量池
String 常量池到底是什么?
什么字符串会进常量池?
常量池的核心规则
new String() 为什么不会进池?
JDK 7 以后常量池的重大变化
intern() 到底做了什么?
字符串拼接进不进池?
总结
三、常见面试点
String 核心
创建 String 的两种方式
== 和 equals 的区别
字符串常量池重点
字符串拼接重点
String 常用方法核心特点
String 为什么不可变?
StringBuilder 与 StringBuffer 区别
String拼接原理
String常量池、运行时常量池、Class常量池区别
四、常见面试题
1、创建对象 & 常量池
2、字符串拼接
一、源码分析(JDK8)
成员变量
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; // Default to 0 }public final class String
public:任何地方都能用final:String 不能被继承,不能写个子类继承它class String:字符串类
implements
- 实现了三个接口:
Serializable:可以序列化(网络传输 / 存文件)Comparable<String>:可以比较大小(a.compareTo (b))CharSequence:字符串的标准接口
- 实现了三个接口:
private final char value[];private:外部不能访问、不能修改final:数组地址一旦赋值就不能改char value[]:真正存字符串内容的字符数组
private int hash; // Default to 0- 缓存哈希值
- 第一次调用
hashCode()时计算一次,之后直接用,不用重复算 - 提升 HashMap 性能
构造函数
非常的多,我们之说下面的常见几种
1. 空参构造:public String()
public String() { this.value = "".value; }- 作用:创建一个空字符串
"" - 底层:直接复用常量池里空字符串的
char[],不新建数组 - 使用:
String s = new String();→ 等价于String s = "" - 注意:直接写
""更高效
2. 字符串参数构造:public String(String original)
public String(String original) { this.value = original.value; this.hash = original.hash; }- 作用:根据已有字符串创建一个新 String 对象
- 底层:直接复用原字符串的 char 数组(不复制),只新建 String 外壳
- 使用:
String s = new String("abc"); - 重点:
- 常量池字符串 +
new会创建两个对象(常量池 1 个 + 堆 1 个) - 日常开发不要这么写,直接
String s = "abc";最优
- 常量池字符串 +
3. char 数组完整构造:public String(char value[])
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }- 作用:把整个 char 数组转成字符串
- 底层:复制一份新数组(保护原数组不被修改)
- 使用:
char[] arr = {'a','b','c'}; String s = new String(arr);
4. char 数组截取构造:public String(char value[], int offset, int count)
public String(char value[], int offset, int count) { // 边界校验:offset、count 不能为负、不能越界 this.value = Arrays.copyOfRange(value, offset, offset+count); }- 作用:从 char 数组中截取一段生成字符串
- 参数:
offset:起始下标count:截取长度
- 底层:复制截取范围的数组,严格校验越界
- 使用:
char[] arr = {'a','b','c','d'}; String s = new String(arr, 1, 2); // 从下标1开始,取2个 → "bc"
5. StringBuilder 构造:public String(StringBuilder builder)
public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }- 作用:把
StringBuilder转成 String(最常用) - 底层:无锁,直接复制数组,效率高
- 使用场景:拼接字符串后转 String
StringBuilder sb = new StringBuilder(); sb.append("a").append("b"); String s = new String(sb); // → "ab"
对比
| 构造方法 | 核心用途 | 特点 |
|---|---|---|
new String() | 空字符串 | 等价 "" |
new String(String) | 复制字符串 | 会创建新对象 |
new String(char[]) | char 数组转字符串 | 复制数组,安全 |
new String(char[],off,len) | 截取 char 数组 | 常用 |
new String(StringBuilder) | 拼接后转字符串 | 常用 |
常见方法
基础信息获取方法
这些方法直接操作value字符数组,最简单高效
// 返回字符串长度 = 字符数组长度 public int length() { return value.length; } // 判断是否为空:长度为0 public boolean isEmpty() { return value.length == 0; } // 获取指定索引的字符,越界抛异常 public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }字符 / 字节拷贝方法
底层用System.arraycopy(native 本地方法,速度极快)实现数据拷贝
// 拷贝字符串到目标字符数组 public void getChars(...) { System.arraycopy(value, 源起始, 目标数组, 目标起始, 长度); } // 字符串转字节数组(支持编码) public byte[] getBytes(String charsetName) { return StringCoding.encode(...); }字符串比较方法
1. equals ()
判断两个字符串内容是否完全相同:
- 先判断引用地址是否相同(
==),相同直接返回 true - 再判断类型是否是 String
- 最后逐字符比较
public boolean equals(Object anObject) { if (this == anObject) return true; // 地址相同,直接相等 if (anObject instanceof String) { String another = (String) anObject; int n = value.length; if (n == another.value.length) { // 长度不同,直接不等 char[] v1 = value; char[] v2 = another.value; int i = 0; while (n-- != 0) { // 逐字符比较 if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }2. compareTo()
字典序比较:逐字符比较 ASCII 码值,返回差值
3. 其他比较
equalsIgnoreCase():忽略大小写比较startsWith()/endsWith():判断开头 / 结尾regionMatches():比较指定区域字符
hashCode () 方法
String 的哈希算法:31 倍哈希法(经典高效)
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; // 公式:h = 31 * h + val[i] for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }为什么用 31?
- 31 是质数,减少哈希冲突
31 * i = (i << 5) - i,JVM 会自动优化,运算极快
查找字符 / 子串方法
1. indexOf ()
朴素字符串匹配算法:先找首字符,再匹配剩余字符,效率满足日常使用
static int indexOf(...) { char first = 目标首字符; // 遍历源数组,找到首字符后逐位匹配 }2. lastIndexOf()
反向查找,逻辑和 indexOf 一致
截取、拼接、替换方法
1. substring()
不修改原字符串,创建新 String 对象返回:
public String substring(int begin, int end) { return new String(value, begin, 长度); }2. concat()
字符串拼接:拷贝数组 → 追加内容 → 返回新字符串
3. replace()
替换字符:先找到第一个匹配字符,再统一替换,返回新字符串
大小写、去空格方法
toLowerCase()/toUpperCase():考虑地区、特殊字符(如希腊字母)的大小写转换trim():删除首尾 <= 空格的字符(空格、制表符、换行符)
valueOf 系列
静态方法,将任意类型转为字符串:
public static String valueOf(int i) { return Integer.toString(i); } public static String valueOf(Object obj) { return obj == null ? "null" : obj.toString(); }安全转换,避免空指针
intern () 本地方法
public native String intern();字符串常量池核心方法:
- 调用时,将字符串放入常量池
- 返回常量池中的引用 → 用于节省内存,实现字符串复用
二、String常量池
String 常量池到底是什么?
一句话:String 常量池 = 一块专门放字符串的缓存区域目的:复用字符串,少创建对象,省内存,提高效率
它是JVM 专门给 String 做的优化
什么字符串会进常量池?
双引号括起来的字符串字面量,会自动进常量池
String s = "abc"; // 自动进池下面这些绝对不会自动进池:
new String("abc")- 字符串拼接
a + b - 从文件、配置、数据库、网络读取
- 方法返回的字符串
这些都在堆里,不进池
常量池的核心规则
- 创建字符串前,先去池里找有没有相同内容
- 有 → 直接复用池里对象,不新建
- 没有 → 创建后放进池里
所以:
String s1 = "abc"; String s2 = "abc"; s1 == s2 → true因为复用了同一个对象
new String()为什么不会进池?
String s = new String("abc");执行过程:
"abc"→ 进常量池new String(...)→在堆里创建一个新对象- 堆对象 ≠ 池对象
"abc" == new String("abc") → falseJDK 7 以后常量池的重大变化
JDK 6:
- 常量池在永久代(PermGen)
- 空间小,容易 OOM
intern()会把字符串复制到常量池
JDK 7+(包括 8、11、17):
- 常量池移到堆(Heap)
intern()不再复制对象- 池里存的是堆对象的引用
这就是为什么:
String s = new String("1") + new String("1"); s.intern(); String s2 = "11"; s == s2 → true(JDK8)intern()到底做了什么?
s.intern();作用:把当前字符串手动加入常量池
规则:
- 池中有相同内容 →返回池中的对象
- 池中没有 →把当前对象存入池,返回自己
intern () 就是让堆字符串也能享受常量池复用
字符串拼接进不进池?
1. 纯常量拼接(进池)
String s = "a" + "b" + "c";编译器优化成"abc"→进池
2. 变量拼接(不进池)
String s = a + b;底层new StringBuilder()→堆对象,不进池
总结
"abc" → 常量池
new String("abc") → 堆
a + b → 堆
读取文件/配置/DB → 堆
常量池对象 == 常量池对象 → true
堆对象 == 常量池对象 → false
堆对象 == 堆对象 → false
- 常量池 = 字符串缓存,用来复用对象
- 双引号字面量自动进池
- new / 拼接 / 读取 → 不进池
- intern () 手动把堆字符串丢进池
- == 比地址,equals 比内容
- JDK7+ 常量池在堆里,intern 不复制,存引用
三、常见面试点
String 核心
- String 是不可变类(Immutable)
- 底层是
private final char[] value(JDK9 是 byte []) - 所有字符串操作(截取、替换、拼接)都不会修改原字符串,只会返回新字符串
- 不可变 = 线程安全 = 可以安全缓存 = 可以常量池共享
String 一旦创建,内容永远不能改,改了就是新对象
创建 String 的两种方式
1. 字面量创建
String s = "abc";特点:
- 自动进入字符串常量池
- 重复创建会复用池里对象,不新建
- 内存最省、最快
2. new 创建
String s = new String("abc");特点:
- 一定在堆里创建新对象
- 不会自动入池
- 即使内容一样,
==也不相等
== 和 equals 的区别
==:比较地址equals():比较内容
规则:
- 字面量之间 == 可以用
- 只要有一个是堆对象(new / 读取 / 拼接),== 大概率 false
- 比较字符串内容永远用 equals
"abc" == "abc" → true new String("abc") == "abc" → false字符串常量池重点
作用
复用字符串对象,减少内存,提高速度
什么时候自动入池?
只有代码里写的双引号字面量会自动入池
什么不会自动入池?
- new String()
- 字符串拼接
- 配置文件读取
- 数据库读取
- 网络读取
- 文件读取
手动入池:intern ()
- 把堆字符串丢进常量池
- 内容相同则复用池里对象
- 目的:省内存 + 让 == 可以用
字符串拼接重点
String s = "a" + "b" + "c";- 编译期优化 → 直接变成
"abc"→ 入池
String s = a + b;- 运行期拼接
- 底层 new StringBuilder ()
- 结果一定是堆对象,不入池
String 常用方法核心特点
substring()→ 不修改原串,返回新串replace()→ 不修改原串,返回新串trim()→ 不修改原串,返回新串toLowerCase()→ 不修改原串,返回新串hashCode()→ 使用 31 倍哈希算法,缓存起来不重复计算
String 为什么不可变?
好处:
- 线程安全
- 可以缓存 hashCode
- 可以安全用作 HashMap key
- 可以进常量池共享,省内存
- 安全(防止被意外篡改)
StringBuilder 与 StringBuffer 区别
String
s += "a";
本质 创建新对象 性能差
StringBuilder
可变
StringBuilder sb = new StringBuilder();源码:char[] value;
append:直接修改数组
无锁,线程不安全,性能最高
StringBuffer
public synchronized大量同步锁,虽然保证了线程安全,但是性能低于Builder
单线程 使用StringBuilder,多线程使用StringBuffer
String拼接原理
String s = a + b + c;编译后结果
new StringBuilder() .append(a) .append(b) .append(c) .toString();反编译结果
StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); sb.append(c); return sb.toString();String常量池、运行时常量池、Class常量池区别
| 名称 | 存储内容 |
|---|---|
| Class常量池 | 编译后的字面量和符号引用 |
| 运行时常量池 | JVM加载类后产生 |
| String常量池 | 专门缓存字符串对象 |
四、常见面试题
1、创建对象 & 常量池
String s1 = "java"; String s2 = "java"; String s3 = new String("java"); String s4 = new String("java");1.1、s1 == s2、s1 == s3、s3 == s4结果是true/false?
s1 == s2
System.out.println(s1 == s2);结果:true
原因:
String s1 = "java"; String s2 = "java";字符串字面量:
"java"会进入字符串常量池
执行 s1 时:
常量池不存在 "java"
↓
创建 "java"
↓
s1 指向常量池对象
执行 s2 时:
发现常量池已有 "java"
↓
直接复用
↓
s2 指向同一个对象
s1 == s2s1 == s2比较的是地址:所以结果是true
s1 == s3
System.out.println(s1 == s3);结果:false
分析如下
String s3 = new String("java");执行过程:
第一步
检查常量池:"java"已经存在
第二步
new String()在堆中创建新的 String 对象
源码:
public String(String original) { this.value = original.value; this.hash = original.hash; }会创建一个新的 String 外壳对象
虽然s1.equals(s3)为true,因为比较的是值
但是s1 == s3比较的是引用地址,两个对象,所以结果为false
s3 == s4
System.out.println(s3 == s4);结果:false
执行:
String s3 = new String("java"); String s4 = new String("java");每次new String(...)都会创建新的堆对象
因此s3 == s4
比较的是:StringA的地址和StringB的地址
所以结果是false
1.2、一共创建了几个对象?
答案:3个对象
对象1:常量池中
"java"
对象2:产生的堆对象(s3)
new String("java")对象3:产生的堆对象(s4)
new String("java")String Pool : 1个 Heap : s3对应对象 1个 s4对应对象 1个 总计: 3个对象2、字符串拼接
// 代码片段1 String a = "ab" + "cd"; // 代码片段2 String x = "ab"; String y = "cd"; String b = x + y;两段代码最终a和b是否进入常量池?a == "abcd"、b == "abcd"结果分别是什么?
解释:+拼接字符串,编译期常量和变量拼接底层实现有什么区别?
String a = "ab" + "cd";结果
a == "abcd" // true编译期发生了什么
因为:"ab"、"cd"都是字面量常量
编译器在编译阶段直接优化为
String a = "abcd";这叫:常量折叠(Constant Folding)
相当于字节码:
LDC "abcd"直接从常量池加载
a == "abcd"比较的是同一个常量池对象
结果是true
String x = "ab"; String y = "cd"; String b = x + y;结果:
b == "abcd" // false为什么?
虽然:x 内容是"ab"
但是x是变量
编译器无法保证运行时一定还是:"ab"
所以不能做常量折叠
编译器会生成:
String b = new StringBuilder() .append(x) .append(y) .toString();JDK8 反编译基本类似
StringBuilder sb = new StringBuilder(); sb.append(x); sb.append(y); String b = sb.toString();执行时:
String Pool "ab" "cd" "abcd"Heap new StringBuilder() ↓ new String("abcd")注意:
StringBuilder.toString()返回的是:new String(...)新的堆对象,不会自动进入常量池
因此:
b == "abcd"变成了堆对象==常量池对象
结果是false
