LESSON 04 · 数组 + 字符串

数组和 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);
}
三个最容易踩的坑:
  1. arr.length(不是 arr.length(),数组用属性,String 才用方法)
  2. 数组下标越界抛 ArrayIndexOutOfBoundsException
  3. 数组是对象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>
长度 固定,创建后不能改 动态,自动扩容
类型 基本类型(intdouble 只能对象(IntegerDouble
性能 更快(无装箱/拆箱) 稍慢(int ↔ Integer 转换)
API 只有 .length,方法极少 丰富:addremovecontainssize
使用场景 长度已知、追求性能 长度未知、需要增删查
小白记法:
  • 数组 = 固定车位的停车场(建好就那几个位)
  • ArrayList = 智能停车场(车多了自动扩建)
  • 99% 的业务代码用 ArrayList 就行;只有"我确定长度不变且追求极致性能"才用数组
4

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 要设计成不可变?
  1. 安全性:String 常被当参数(URL、文件路径、密码),不可变防止被偷偷改
  2. String 常量池(下一节讲):只有不可变才能安全地共享
  3. 线程安全:不可变 = 天然线程安全,不用加锁
  4. 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

对比StringBuilderStringBuffer
线程安全 ❌ 不安全(更快) ✅ 安全(方法都加了 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",进池)· ② falsenew String 强制在堆里 new)· ③ trueintern() 找到池里那个返回)

这题大厂一面 80% 会问,背也要背下来

你的作业(分步走)

  1. 数组基础:写个 int[] scores = {85, 92, 78, 95, 88},用传统 for 和增强 for 分别遍历打印,然后求平均分
  2. 二维数组:写个 3x4 的二维数组存 3 个学生的 4 门课成绩,遍历打印"学生 X 平均分 Y"(用二维数组,不是写死)
  3. String 不可变验证:写个 String s = "hello",调用 s.concat(" world"),打印 s——证明 s 没变;然后再用 replace 方法验证一次
  4. StringBuilder 性能:写一个循环 10000 次的方法,用 String += 拼接;另一个用 StringBuilder;用 System.currentTimeMillis() 计时,体会性能差距
  5. 常量池题:把第 6 节的"经典面试题"在 IDE 里敲出来跑一遍,把 3 个输出结果写注释,贴给我 review