作者:没有四次元口袋的蓝胖
日期:2026-07-12
标签:Java, String
Java String知识梳理
一、String 的不可变性
String 是不可变的(immutable),一旦创建,其值就不能修改。
1.1 为什么设计成不可变?
原因:
- 线程安全:不可变对象天然线程安全,无需同步
- 字符串常量池:可以共享相同字符串,节省内存
- hashCode 缓存:创建时计算一次 hashCode,后续直接使用(HashMap 的 key)
- 安全性:作为参数传递时不会被篡改(数据库连接、网络地址等)
1.2 源码分析
java
public final class String {
private final char value[]; // final 修饰,引用不可变
// Java 9+ 改为:private final byte[] value;
public String(String original) {
this.value = original.value; // 直接复用
// ...
}
}
关键:
final class String:String 类不可继承final char[] value:数组引用不可变(但数组内容理论上可变,Java 通过不提供修改方法保证)
1.3 "不可变"的陷阱
java
String str = "hello";
str = str + " world"; // 看起来修改了,实际上创建了新对象
// 实际过程:
// 1. 创建 "hello"
// 2. 创建 " world"
// 3. 创建新字符串 "hello world"
// 4. str 引用指向新字符串
// 5. 原来的 "hello" 还在(可能被其他引用使用)
💡 String 的"修改"都是创建新对象,原对象不变。
二、字符串常量池
2.1 常量池的作用
字符串常量池(String Pool)是 JVM 堆内存中的一块特殊区域,用于存储字符串字面量。
优点:
- 节省内存:相同字符串共享
- 提高效率:避免重复创建
2.2 创建方式的区别
java
// 方式1:字面量(常量池)
String s1 = "hello";
// 方式2:new 对象(堆内存)
String s2 = new String("hello");
// 方式3:拼接(编译器优化)
String s3 = "hel" + "lo"; // 编译期优化为 "hello"
2.3 == vs equals
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s1 == s2); // true(同一个常量池对象)
System.out.println(s1 == s3); // false(s3 在堆中)
System.out.println(s3 == s4); // false(两个不同的对象)
System.out.println(s1.equals(s3)); // true(内容相同)
规则:
==:比较引用(内存地址)equals():比较内容
⚠️ String 必须用 equals() 比较内容,不能用 ==。
2.4 intern() 方法
intern() 会将字符串放入常量池(如果不存在),并返回常量池中的引用。
java
String s1 = new String("hello"); // 堆中创建
String s2 = s1.intern(); // 将 "hello" 放入常量池(如果不存在)
String s3 = "hello"; // 从常量池获取
System.out.println(s1 == s2); // false(s1 在堆,s2 在常量池)
System.out.println(s2 == s3); // true(都是常量池中的 "hello")
Java 6 vs Java 7+:
- Java 6:常量池在永久代(PermGen),intern() 会复制字符串
- Java 7+:常量池移到堆中,intern() 不会复制,只是添加引用
三、字符串拼接
3.1 拼接方式对比
| 方式 | 类型 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|---|
+ |
语法糖 | 安全 | 低(每次创建新对象) | 少量拼接 |
StringBuilder |
可变 | 不安全 | 高 | 单线程大量拼接 |
StringBuffer |
可变 | 安全 | 中(有同步开销) | 多线程拼接 |
String.concat() |
方法 | 安全 | 中 | 不常用 |
3.2 编译器优化
java
// 编译期常量拼接(优化)
String s1 = "hel" + "lo"; // 编译为 "hello"
// 运行期拼接(不优化)
String a = "hel";
String b = "lo";
String s2 = a + b; // 运行期拼接,创建新对象
// final 修饰(优化)
final String a = "hel";
final String b = "lo";
String s3 = a + b; // 编译期优化为 "hello"
3.3 StringBuilder vs StringBuffer
java
// StringBuilder(非线程安全,性能高)
StringBuilder sb1 = new StringBuilder();
sb1.append("hello");
sb1.append(" ");
sb1.append("world");
String result1 = sb1.toString();
// StringBuffer(线程安全,性能低)
StringBuffer sb2 = new StringBuffer();
sb2.append("hello");
sb2.append(" ");
sb2.append("world");
String result2 = sb2.toString();
底层实现:
java
// StringBuffer 的 append 方法(有 synchronized)
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
// StringBuilder 的 append 方法(无 synchronized)
public StringBuilder append(String str) {
super.append(str);
return this;
}
选择建议:
- 单线程:用 StringBuilder
- 多线程:用 StringBuffer
- 少量拼接:直接用
+
四、String 常见方法
4.1 常用方法速查
java
String str = "Hello World";
// 长度
str.length(); // 11
// 字符操作
str.charAt(0); // 'H'
str.toCharArray(); // ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd']
// 截取
str.substring(0, 5); // "Hello"
// 查找
str.indexOf("World"); // 6
str.lastIndexOf("l"); // 9
str.contains("World"); // true
str.startsWith("Hello"); // true
str.endsWith("World"); // true
// 大小写
str.toLowerCase(); // "hello world"
str.toUpperCase(); // "HELLO WORLD"
// 去除空白
" hello ".trim(); // "hello"
// 替换
str.replace("World", "Java"); // "Hello Java"
str.replaceAll("[aeiou]", "*"); // "H*ll* W*rld"
// 分割
"a,b,c".split(","); // ["a", "b", "c"]
// 比较
str.equals("Hello World"); // true
str.equalsIgnoreCase("hello world"); // true
str.compareTo("Hello Java"); // 正数(字典序)
// 格式化
String.format("Name: %s, Age: %d", "Tom", 20); // "Name: Tom, Age: 20"
4.2 性能陷阱
java
// 错误:循环中用 + 拼接(每次都创建新对象)
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 创建 1000 个 String 对象
}
// 正确:用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
五、String 与其他类型转换
5.1 String ↔ 基本类型
java
// String → int
int num1 = Integer.parseInt("123"); // 123
int num2 = Integer.valueOf("123"); // 123
// int → String
String s1 = String.valueOf(123); // "123"
String s2 = Integer.toString(123); // "123"
String s3 = 123 + ""; // "123"(不推荐)
// String → double
double d = Double.parseDouble("3.14");
// String → boolean
boolean b = Boolean.parseBoolean("true");
5.2 String ↔ 字符数组
java
// String → char[]
char[] chars = "hello".toCharArray();
// char[] → String
String s1 = new String(chars);
String s2 = String.valueOf(chars);
// String → byte[](编码)
byte[] bytes = "你好".getBytes("UTF-8");
// byte[] → String(解码)
String s3 = new String(bytes, "UTF-8");
六、常见面试题
6.1 String、StringBuffer、StringBuilder 的区别?
| 对比 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 安全(synchronized) | 不安全 |
| 性能 | 低(每次创建新对象) | 中 | 高 |
| 适用 | 少量操作 | 多线程拼接 | 单线程拼接 |
6.2 String 为什么是不可变的?
- 安全性:作为参数传递时不会被篡改
- 线程安全:不可变对象天然线程安全
- hashCode 缓存:创建时计算一次,HashMap 的 key 效率高
- 字符串常量池:可以共享相同字符串
6.3 String s = new String("hello") 创建了几个对象?
答案:可能创建 1 个或 2 个对象。
情况 1:常量池中已有 "hello"
- 只创建 1 个对象(堆中的 String 对象)
- "hello" 字面量在常量池,new 的对象在堆中
情况 2:常量池中没有 "hello"
- 创建 2 个对象
- 1 个在常量池("hello" 字面量)
- 1 个在堆中(new String)
6.4 如何判断 String 是否为空?
java
// 错误:会 NPE
if (str.equals("")) {}
// 推荐
if (str == null || str.isEmpty()) {} // Java 8
if (str == null || str.length() == 0) {} // 兼容老版本
// Java 11+
if (str == null || str.isBlank()) {} // 包含空白字符
6.5 String.intern() 的作用?
- 检查常量池中是否存在该字符串
- 存在则返回常量池中的引用
- 不存在则将字符串放入常量池,返回引用
应用场景:
- 节省内存(大量重复字符串)
- 字符串比较(用 == 代替 equals)
七、思维导图速览
┌───────────────────────────────────────────────────┐
│ String 高频考点 │
├───────────────────────────────────────────────────┤
│ │
│ 不可变性 │
│ • final class String │
│ • final char[] value │
│ • 原因:安全/线程安全/hashCode缓存/常量池 │
│ │
│ 常量池 │
│ • 字符串字面量存储在常量池 │
│ • new String() 在堆中创建 │
│ • intern() 放入常量池 │
│ • == vs equals │
│ │
│ 拼接 │
│ • +:语法糖,编译器优化 │
│ • StringBuilder:单线程,性能高 │
│ • StringBuffer:多线程,性能中 │
│ │
│ 常见方法 │
│ • length/charAt/substring/indexOf/contains │
│ • replace/replaceAll/split/trim │
│ • equals/equalsIgnoreCase/compareTo │
│ │
│ 面试题 │
│ • String vs StringBuffer vs StringBuilder │
│ • 为什么不可变 │
│ • new String("hello") 创建几个对象 │
│ • intern() 的作用 │
│ │
│ 陷阱 │
│ ✗ 用 == 比较 String │
│ ✗ 循环中用 + 拼接 │
│ ✗ str.equals("") 会 NPE │
│ ✓ 用 equals() 比较内容 │
│ ✓ 用 StringBuilder 拼接 │
│ ✓ str == null || str.isEmpty() 判空 │
└───────────────────────────────────────────────────┘
八、写在最后
- String 不可变性 是核心,要能说出 4 个原因和源码实现
- 字符串常量池 是高频考点,要理解
new String()和字面量的区别 - StringBuilder vs StringBuffer 要知道什么时候用哪个
- 常见陷阱 :
==vsequals()、循环中用+拼接、空指针判断