Java String 详解
String 是 Java 中最常用的引用类型之一,用于表示不可变的字符序列。其设计特性(不可变性、常量池缓存)直接影响程序的性能和安全性,以下从核心特性、创建方式、常量池、常用方法、与其他字符串类的对比等方面全面解析。
一、核心特性:不可变性(Immutability)
1. 定义
String 被声明为 final class,且内部存储字符的数组 value 也是 final 的,这意味着:
-
String 对象创建后,其内容(字符序列)无法被修改;
-
任何看似 “修改” String 的操作(如拼接、替换、截取),本质都是创建新的 String 对象,原对象内容不变。
2. 底层实现(JDK 版本差异)
-
JDK 8 及之前:底层用
private final char[] value存储字符(每个char占 2 字节,基于 UTF-16 编码); -
JDK 9 及之后:底层改为
private final byte[] value+ 编码标记(LATIN1 或 UTF16):-
若字符串仅包含 ASCII 字符(占 1 字节),使用 LATIN1 编码,节省 50% 内存;
-
含非 ASCII 字符时,仍用 UTF16 编码(兼容旧版本)。
-
3. 不可变性的优缺点
优点:
-
线程安全:不可变对象天然线程安全,无需加锁,可在多线程中直接共享;
-
常量池复用:相同内容的字符串可复用常量池中的对象,减少内存占用;
-
哈希值缓存:String 的
hashCode()会缓存首次计算的哈希值(因内容不变,哈希值不会变),作为 HashMap 等集合的键时,查询效率更高。
缺点:
-
频繁修改字符串(如循环拼接)会创建大量临时对象,导致内存开销大、效率低(需用
StringBuilder/StringBuffer替代)。
二、String 的创建方式(2 种核心方式)
String 的创建方式决定了对象的存储位置(常量池 vs 堆),直接影响 == 比较结果。
1. 直接赋值(推荐)
String s1 = "abc"; // 优先从字符串常量池获取对象 String s2 = "abc"; // 复用常量池中的同一个对象 System.out.println(s1 == s2); // true(地址相同) System.out.println(s1.equals(s2)); // true(内容相同)
-
原理:JVM 会先检查字符串常量池(String Pool)中是否存在 "abc":
-
若存在,直接返回常量池中的对象引用;
-
若不存在,在常量池创建 "abc" 对象,再返回引用。
-
-
特点:高效、节省内存(复用常量池对象)。
2. new String () 方式
String s3 = new String("abc"); // 堆中创建新对象,同时检查常量池
String s4 = new String("abc"); // 堆中创建另一个新对象
System.out.println(s3 == s4); // false(堆中不同对象,地址不同)
System.out.println(s3.equals(s4)); // true(内容相同)
System.out.println(s3 == s1); // false(堆对象 vs 常量池对象,地址不同)
-
原理:
-
先检查常量池:若没有 "abc",则在常量池创建 "abc" 对象;
-
无论常量池是否存在,都会在堆内存中创建一个新的 String 对象,其
value数组引用常量池中的字符序列。
-
-
特点:创建 1 个或 2 个对象(取决于常量池是否已有目标字符串),内存开销大,不推荐直接使用。
3. 关键方法:intern ()
intern() 用于手动将字符串对象 “入池”,返回常量池中的对象引用(JDK 1.7+ 优化):
-
JDK 1.6 及之前:若常量池无该字符串,会复制堆中字符串的内容到常量池,返回常量池新对象;
-
JDK 1.7 及之后:若常量池无该字符串,不会复制内容,而是将堆中对象的引用存入常量池,返回堆对象的引用(节省内存)。
示例(JDK 8+):
String s5 = new String("a") + new String("b"); // 堆中创建 "ab" 对象(常量池无 "ab")
String s6 = s5.intern(); // 常量池存入 s5 的引用,返回 s5
String s7 = "ab"; // 直接获取常量池中的 s5 引用
System.out.println(s5 == s6); // true(s6 是 s5 的引用)
System.out.println(s5 == s7); // true(s7 复用常量池中的 s5 引用)
三、字符串常量池(String Pool)
1. 定义与作用
字符串常量池是 JVM 为 String 设计的缓存机制,本质是一个哈希表(HashTable),存储字符串常量的引用(JDK 1.7+)或内容(JDK 1.6-)。
-
作用:避免重复创建相同内容的字符串,减少内存占用,提高访问效率。
2. 存储位置变化
-
JDK 1.6 及之前:位于方法区(永久代);
-
JDK 1.7 及之后:移至堆内存(因永久代空间有限,易导致 OOM,堆空间更大且可动态扩展)。
3. 常量池触发时机
-
直接赋值字符串(如
"abc"):编译期将字符串存入常量池(.class文件的Constant Pool表),类加载时加载到 JVM 常量池; -
字符串字面量拼接(如
"a" + "b"):编译期优化为"ab",直接存入常量池; -
动态拼接(如
s1 + "b",s1 是变量):编译期无法优化,运行时通过StringBuilder拼接,结果存入堆(需手动intern()入池)。
四、String 常用方法(按功能分类)
String 提供了大量操作字符序列的方法,以下是高频使用场景:
1. 字符串比较
| 方法 | 功能 | 注意事项 |
|---|---|---|
equals(Object obj) | 比较字符串内容是否相同(区分大小写) | 重写自 Object,核心方法 |
equalsIgnoreCase(String str) | 比较内容(不区分大小写) | 如 "ABC".equalsIgnoreCase ("abc") → true |
compareTo(String str) | 按字典序比较(返回 int) | 相等返回 0;前小后大返回负数;反之正数 |
== | 比较对象地址是否相同 | 切勿用于内容比较(仅直接赋值的相同字符串返回 true) |
示例:
String a = "abc";
String b = new String("abc");
System.out.println(a.equals(b)); // true(内容)
System.out.println(a == b); // false(地址)
System.out.println("a".compareTo("b")); // -1(字典序 a < b)
2. 查找与判断
| 方法 | 功能 |
|---|---|
indexOf(String str) | 查找 str 首次出现的索引(无则返回 -1) |
lastIndexOf(String str) | 查找 str 最后出现的索引(无则返回 -1) |
contains(CharSequence s) | 判断是否包含指定字符序列 |
startsWith(String prefix) | 判断是否以指定前缀开头 |
endsWith(String suffix) | 判断是否以指定后缀结尾 |
isEmpty() | 判断长度是否为 0(JDK 6+) |
isBlank() | 判断是否全为空白字符(JDK 11+,含空格、制表符等) |
示例:
String str = "hello world";
System.out.println(str.indexOf("o")); // 4
System.out.println(str.contains("world")); // true
System.out.println(str.startsWith("he")); // true
System.out.println(" ".isBlank()); // true(JDK 11+)
3. 截取与分割
| 方法 | 功能 | 注意事项 |
|---|---|---|
substring(int beginIndex) | 从 beginIndex 截取到末尾 | |
substring(int beginIndex, int endIndex) | 截取 [beginIndex, endIndex)(左闭右开) | JDK 6 复制字符数组,JDK 7+ 复用原数组偏移量(可能内存泄漏) |
split(String regex) | 按正则表达式分割,返回字符串数组 | 正则需转义(如分割 "." 需用 \\.) |
split(String regex, int limit) | 限制分割次数(limit 为分割后数组长度) |
示例:
String str = "a.b.c";
String[] arr1 = str.split("\\."); // ["a", "b", "c"](正则转义)
String[] arr2 = str.split("\\.", 2); // ["a", "b.c"](限制分割 1 次)
String sub = str.substring(2, 4); // "b."(索引 2 是 'b',4 是 '.')
4. 替换
| 方法 | 功能 | 注意事项 |
|---|---|---|
replace(char oldChar, char newChar) | 替换所有指定字符 | 非正则 |
replace(CharSequence oldSeq, CharSequence newSeq) | 替换所有指定字符序列 | 非正则 |
replaceAll(String regex, String replacement) | 按正则替换所有匹配内容 | 正则(如替换空格用 \\s+) |
replaceFirst(String regex, String replacement) | 按正则替换首次匹配内容 | 正则 |
示例:
String str = "hello 123 world 456";
System.out.println(str.replace('l', 'x')); // "hexxo 123 worxd 456"
System.out.println(str.replaceAll("\\d+", "num")); // "hello num world num"(替换所有数字)
5. 转换与格式化
| 方法 | 功能 |
|---|---|
toUpperCase() | 转为大写(受默认 Locale 影响) |
toLowerCase() | 转为小写(受默认 Locale 影响) |
toCharArray() | 转为 char 数组 |
valueOf(任意类型) | 静态方法,将其他类型转为 String(推荐,避免空指针) |
trim() | 去除首尾空白字符(仅 ASCII 空格,JDK 6+) |
strip() | 去除首尾空白字符(支持 Unicode,JDK 11+) |
format(String format, Object... args) | 静态方法,格式化字符串(如 String.format("年龄:%d", 20)) |
示例:
String numStr = String.valueOf(123); // "123"(避免 new String(123) 错误)
String str = " Hello ";
System.out.println(str.trim()); // "Hello"(仅去除 ASCII 空格)
System.out.println(str.strip()); // "Hello"(支持 Unicode 空白)
String fmt = String.format("%.2f", 3.1415); // "3.14"(保留 2 位小数)
6. 其他常用方法
-
length():返回字符串长度(字符数,非字节数); -
charAt(int index):返回指定索引的字符(索引从 0 开始); -
join(CharSequence delimiter, CharSequence... elements):静态方法,用分隔符拼接字符串数组(JDK 8+)。
示例:
String[] arr = {"a", "b", "c"};
String joined = String.join("-", arr); // "a-b-c"
System.out.println(joined.charAt(1)); // '-'(索引 1)
System.out.println(joined.length()); // 5
五、String 与 StringBuilder、StringBuffer 的对比
因 String 不可变,频繁修改(如循环拼接)需用可变字符串类,三者核心区别如下:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 天然安全(不可变) | 非线程安全(无同步锁) | 线程安全(方法加 synchronized) |
| 效率 | 低(频繁修改创建新对象) | 高(直接操作字符数组) | 中(同步开销) |
| 底层实现 | byte[](JDK9+)/ char[](JDK8-) | char [](可扩容) | char [](可扩容) |
| 适用场景 | 字符串不修改(如常量、配置) | 单线程下频繁修改(如拼接、替换) | 多线程下频繁修改(如日志拼接) |
关键注意点:
-
字符串拼接效率:
StringBuilder > StringBuffer > String(String 循环拼接等价于new StringBuilder().append().toString(),但每次循环创建新对象); -
StringBuilder 扩容机制:默认初始容量 16,扩容时按
oldCapacity * 2 + 2计算,不足则直接用目标长度,扩容会复制数组,建议初始化时指定容量(如new StringBuilder(1024))减少扩容次数。
示例(高效拼接):
// 推荐:单线程下指定容量的 StringBuilder
StringBuilder sb = new StringBuilder(100);
for (int i = 0; i < 100; i++) {
sb.append(i).append(",");
}
String result = sb.toString(); // 仅创建 1 个 StringBuilder 和 1 个 String
六、常见问题与面试题
1. == 与 equals() 的区别?
-
==:比较对象的内存地址(基本类型比较值,引用类型比较地址); -
equals():String 重写后比较内容,默认 Object 类的equals()等价于==。
2. 为什么 String 是 final 的?
-
保证不可变性:避免被继承后修改
value数组,破坏常量池复用和线程安全; -
安全:作为 HashMap 等集合的键时,不可变性确保哈希值不变,避免查找失效;
-
高效:常量池复用依赖不可变性,否则缓存的字符串可能被篡改。
3. 以下代码创建了几个对象?(JDK 8+)
String s1 = "abc";
String s2 = new String("abc");
String s3 = new String("a") + new String("b");
-
分析:
-
s1:常量池无 "abc",创建 1 个常量池对象; -
s2:常量池已有 "abc",仅创建 1 个堆对象; -
s3:new String("a")创建 1 个堆对象(常量池创建 "a"),new String("b")创建 1 个堆对象(常量池创建 "b"),拼接时创建StringBuilder对象,最终toString()创建 1 个堆对象("ab",常量池无),共 5 个对象(常量池 2 个:"a"、"b";堆 3 个:"a"、"b"、"ab" + StringBuilder)。
-
4. JDK 9 为什么把 String 底层从 char [] 改为 byte []?
-
节省内存:大部分字符串是 ASCII 字符(占 1 字节),char [] 每个字符占 2 字节,byte [] + LATIN1 编码可减少 50% 内存占用;
-
兼容旧版本:非 ASCII 字符仍用 UTF16 编码,通过
coder标记区分编码方式。
七、总结
String 的核心是不可变性,基于常量池实现高效复用,适用于字符串不修改的场景;若需频繁修改,优先使用 StringBuilder(单线程)或 StringBuffer(多线程)。
关键要点:
-
直接赋值复用常量池,
new String()创建堆对象; -
equals()比==更安全(比较内容); -
频繁修改用可变字符串类,初始化指定容量提升效率;
-
注意 JDK 版本差异(底层存储、
intern()、substring()等)。
掌握 String 的设计原理和使用场景,能有效优化程序性能,避免常见坑(如内存泄漏、正则转义错误)。

2761

被折叠的 条评论
为什么被折叠?



