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