数组和 String 的
"魔鬼细节"
这节我们啃两块"看着简单,面试爱问"的内容:数组(多维、ArrayList 区别)和 String(不可变性、常量池、StringBuilder 性能)。后者大厂面试命中率极高。
学完这节:你能解释清楚
String s = "abc" 在内存里到底发生了什么、为什么 String 要设计成 final、为什么循环里拼字符串要用 StringBuilder。
1
数组基础:固定长度的"数据集装箱"
💡 动机:批量存一组同类型数据的最简单方式
数组是 Java 里最古老的数据结构——长度固定、类型固定、按下标访问。
// 三种声明方式(推荐第一种)
int[] scores = new int[5]; // 声明 + 创建(默认全 0)
int[] nums = {10, 20, 30, 40}; // 声明 + 直接初始化
int[] arr = new int[]{1, 2, 3}; // 匿名数组写法
// 遍历:传统 for(能用下标) vs 增强 for(更简洁)
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
for (int n : nums) { // 增强 for(只读遍历)
System.out.println(n);
}
三个最容易踩的坑:
arr.length(不是arr.length(),数组用属性,String 才用方法)- 数组下标越界抛
ArrayIndexOutOfBoundsException - 数组是对象,
int[] arr = null;之后访问会空指针
2
多维数组:其实就是"数组的数组"
💡 动机:表格 / 矩阵 / 棋盘这种"二维数据"怎么存?
Java 没有真正的多维数组,所谓"二维数组"其实是每个元素都是另一个数组的对象。
// 标准二维数组(3 个学生,每人 4 门课)
int[][] grades = {
{85, 90, 78, 92}, // 张三
{70, 88, 95, 80}, // 李四
{60, 75, 82, 70} // 王五
};
// 访问:grades[行][列]
System.out.println(grades[1][2]); // → 95(李四的第三门课)
// 遍历:用嵌套 for
for (int i = 0; i < grades.length; i++) {
for (int j = 0; j < grades[i].length; j++) {
System.out.print(grades[i][j] + " ");
}
System.out.println();
}
锯齿数组:Java 允许"每行长度不一样"的二维数组:
int[][] triangle = new int[3][];
triangle[0] = new int[]{1};
triangle[1] = new int[]{2, 3};
triangle[2] = new int[]{4, 5, 6};
这种数据结构面试偶尔问,实际开发用得不多,知道就行。
3
数组 vs ArrayList:什么时候用哪个 ⭐ 面试常问
💡 动机:都能存一组数据,区别在哪?
数组和 ArrayList 都能存一组元素,但定位完全不同:
| 对比项 | 数组 int[] | ArrayList List<Integer> |
|---|---|---|
| 长度 | 固定,创建后不能改 | 动态,自动扩容 |
| 类型 | 基本类型(int、double) |
只能对象(Integer、Double) |
| 性能 | 更快(无装箱/拆箱) | 稍慢(int ↔ Integer 转换) |
| API | 只有 .length,方法极少 |
丰富:add、remove、contains、size… |
| 使用场景 | 长度已知、追求性能 | 长度未知、需要增删查 |
小白记法:
- 数组 = 固定车位的停车场(建好就那几个位)
- ArrayList = 智能停车场(车多了自动扩建)
- 99% 的业务代码用
ArrayList就行;只有"我确定长度不变且追求极致性能"才用数组
4
String 不可变性:为什么 String 是
String 不可变性:为什么 String 是 final 的? ⭐ 面试必问
💡 动机:Java 里天天用 String,但它有个反直觉的设计
先看一段代码,会颠覆你的认知:
String s = "hello";
s.toUpperCase(); // 看似"修改"了 s
System.out.println(s); // → "hello"(居然没变!)
// 真正"修改"的写法
s = s.toUpperCase(); // 重新赋值
System.out.println(s); // → "HELLO"
为什么 toUpperCase() 没改原字符串?因为 String 是不可变的(immutable)—— 一旦创建,内容永远不变。
🔐 生活类比:景区门票
你买了一张印着"原价 100 元"的门票,售票处不能在你的票上把 100 改成 200。要改?只能重打一张新的,把旧票作废。
→ toUpperCase() 就是"重打一张新票",原来那张 "hello" 还躺在常量池里,没人动它。
源码里 String 的定义
public final class String implements Serializable, Comparable<String> {
private final char[] value; // ① 类是 final(不能继承)
// ② 数组是 final(不能换引用)
// (数组本身内容能改,但 value 不能指向新数组)
}
⭐ 面试题:为什么 String 要设计成不可变?
- 安全性:String 常被当参数(URL、文件路径、密码),不可变防止被偷偷改
- String 常量池(下一节讲):只有不可变才能安全地共享
- 线程安全:不可变 = 天然线程安全,不用加锁
- HashMap key:String 当 key 是最稳的,hashCode 不会变
5
StringBuilder / StringBuffer:循环拼字符串的救星
💡 动机:循环里
s += x 其实在疯狂创建新对象因为 String 不可变,每次 s += x 都会创建新对象、丢弃旧对象。循环 1000 次 = 创建 1000 个临时对象。
// ❌ 反面教材:循环里用 String 拼接
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + i; // 每次都创建新 String
}
// ✅ 正确做法:StringBuilder(在循环外建一次,循环里只 append)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 改内部数组,不创建新对象
}
String s = sb.toString(); // 最后一次性生成 String
性能差距有多大?
同样是循环 10000 次拼接:
- String
+=:约 800ms - StringBuilder:约 2ms
- 差距:400 倍
StringBuilder vs StringBuffer
| 对比 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | ❌ 不安全(更快) | ✅ 安全(方法都加了 synchronized) |
| 性能 | ⭐ 更快(无锁) | 稍慢(有锁) |
| 使用场景 | ⭐ 99% 的情况(单线程) | 多线程共享同一个 builder(极少) |
| API | 完全一样 | 完全一样 |
小白记法:看到 StringBuilder / StringBuffer,默认选 StringBuilder,除非你明确在多线程里共享同一个对象。
6
字符串常量池 +
字符串常量池 + intern() ⭐ 面试高频
💡 动机:为什么两个看起来一样的 String,用
== 比有时候相等有时候不等?Java 为了节省内存,设计了一个叫字符串常量池的共享区域。
// 字面量赋值:会放进常量池
String a = "hello";
String b = "hello";
System.out.println(a == b); // → true(指向常量池同一个对象)
// new String():强制在堆里 new 一个新的
String c = new String("hello");
System.out.println(a == c); // → false(地址不同)
// intern():手动把字符串塞进常量池,返回池里的引用
String d = c.intern();
System.out.println(a == d); // → true(d 现在指向池里那个"hello")
字符串常量池(共享)
"hello"
堆(每个 new 一份)
c → "hello"(独立副本)
经典面试题:下面输出什么?
String s1 = "abc";
String s2 = "ab" + "c";
String s3 = new String("abc");
String s4 = s3.intern();
System.out.println(s1 == s2); // ① ?
System.out.println(s1 == s3); // ② ?
System.out.println(s1 == s4); // ③ ?
答案:①
true(编译期就把 "ab"+"c" 优化成 "abc",进池)· ② false(new String 强制在堆里 new)· ③ true(intern() 找到池里那个返回)
这题大厂一面 80% 会问,背也要背下来。
你的作业(分步走)
- 数组基础:写个
int[] scores = {85, 92, 78, 95, 88},用传统 for 和增强 for 分别遍历打印,然后求平均分 - 二维数组:写个 3x4 的二维数组存 3 个学生的 4 门课成绩,遍历打印"学生 X 平均分 Y"(用二维数组,不是写死)
- String 不可变验证:写个
String s = "hello",调用s.concat(" world"),打印 s——证明 s 没变;然后再用replace方法验证一次 - StringBuilder 性能:写一个循环 10000 次的方法,用
String +=拼接;另一个用StringBuilder;用System.currentTimeMillis()计时,体会性能差距 - 常量池题:把第 6 节的"经典面试题"在 IDE 里敲出来跑一遍,把 3 个输出结果写注释,贴给我 review