Arrays & Strings
为什么数组与字符串很重要
数组和字符串是后端系统中高效数据处理的基础:
- 内存效率:数组提供 O(1) 访问,开销极低——对高性能缓存层至关重要
- 缓存局部性:顺序内存访问利用 CPU 缓存行,处理速度比链式结构快 10-100 倍
- 字符串处理:每个 HTTP 请求、JSON 负载和数据库查询都涉及字符串操作
- 面试频率:80%+ 的编程面试以数组/字符串题目开场
实际影响:处理每秒 10K 请求的后端 API,通过优化字符串拼接可以节省每个请求 200ms——相当于同一硬件可以多处理 2,000 个并发请求。
核心概念
内存布局与访问模式
数组是连续的内存块,每个元素占用相同大小。这种布局实现了:
- O(1) 随机访问:
arr[i]的地 址计算为base_address + i * element_size - CPU 缓存优化:加载一个缓存行(64 字节)会获取多个相邻元素
- 可预测的迭代:顺序访问最大化缓存命中率
Java 中的 Array 与 ArrayList
| 操作 | Array (int[]) | ArrayList<Integer> | 说明 |
|---|---|---|---|
| 访问 | O(1) | O(1) | 两者都使用直接索引 |
| 插入(末尾) | 不适用 | O(1) 摊销 | ArrayList 动态增长 |
| 插入(中间) | O(n) 移动 | O(n) 移动 | 两者都需要移动元素 |
| 内存 | 每个 int 4 字节 | 每个 Integer 16+ 字节 | 对象开销 + 引用 |
| 基本类型支持 | 是 | 否(包装对象) | int[] vs Integer[] |
何时使用哪种:
- 基本类型数组:数值计算、性能关键代码、内存受限环境
- ArrayList:需要动态调整大小、频繁插入/删除、存储对象
String 不可变性
Java 字符串是不可变的——一旦创建,其值不能改变。这个设计选择有深 远的影响:
String s1 = "hello"; // 字符串池
String s2 = "hello"; // 复用池中的实例 (s1 == s2)
String s3 = new String("hello"); // 新对象 (s1 != s3)
// 拼接会创建新对象
String s4 = s1 + " world"; // 创建 "hello world" 对象
// s1 仍然是 "hello"
优势:
- 线程安全:字符串访问无需同步
- 哈希缓存:
hashCode()只计算一次并缓存 - 字符串池:JVM 通过复用字符串字面量来优化内存
劣势:
- 内存开销:每次拼接都创建新对象
- 性能代价:每次拼接 O(n)(需要复制整个字符串)
StringBuilder 与 StringBuffer
对于可变字符串操作,使用 StringBuilder(非同步)或 StringBuffer(同步):
// ❌ 错误:循环中使用字符串拼接 - O(n²)
public String badConcat(String[] words) {
String result = "";
for (String word : words) {
result += word; // 每次迭代都创建新对象
}
return result; // 1 万个单词:约创建 5000 万个对象
}
// ✅ 正确:使用 StringBuilder - O(n)
public String goodConcat(String[] words) {
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word); // 修改内部缓冲区
}
return sb.toString(); // 只创建一个对象
}
性能对比(10,000 次拼接):
- 字符串拼接:约 1,200ms
- StringBuilder:约 2ms
- 快 600 倍
二维数组
Java 中的二维数组是数组的数组——不是连续的内存块:
int[][] matrix = new int[3][4]; // 3 行 4 列
// 不规则数组(每行可以有不同的长度)
int[][] ragged = new int[3][];
ragged[0] = new int[2]; // 第 0 行:2 列
ragged[1] = new int[5]; // 第 1 行:5 列
ragged[2] = new int[3]; // 第 2 行:3 列
内存布局影响:行主序(Java、C)意味着 matrix[i][j] 和 matrix[i][j+1] 相邻,但 matrix[i][j] 和 matrix[i+1][j] 可能相距很远。逐行迭代以获得缓存效率。
深入理解
数组扩容策略
ArrayList 使用以下策略增长:
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍增长
// 为什么是 1.5 倍而不是 2 倍?
// - 平衡内存浪费与扩容频率
// - 2 倍对大数组会浪费内存
// - 1.5 倍仍然提供摊销 O(1) 的插入
摊销分析:扩容发生在增长的幂次处(1, 1.5, 2.25, 3.375...)。n 次插入的总开销为 O(n),因此每次插入的平均开 销为 O(1)。
字符串池优化
// 编译时常量进入字符串池
String s1 = "hello" + " world"; // 单个池化字符串
String s2 = "hello world"; // 复用同一个池条目
System.out.println(s1 == s2); // true
// 运行时拼接创建新对象
String s3 = "hello";
String s4 = s3 + " world"; // 未池化
String s5 = "hello world";
System.out.println(s4 == s5); // false
// 显式 intern
String s6 = s4.intern(); // 添加到字符串池
System.out.println(s6 == s5); // true
后端启示:对于频繁使用的字符串(HTTP 头、配置键),谨慎使用 intern()——它会增加 GC 压力。