流程控制、数组、字符串进阶篇

本文适合:已学过 Java 基础,想知道 switch 为什么快、String 为什么不可变、数组在内存中长什么样的读者。

一、流程控制的底层补充

1. switch 语句为什么比大量 if-else 性能好?

  • switch 在编译时会根据 case 值的分布生成两种字节码指令:
指令 适用场景 原理
tableswitch case 值连续(如 1,2,3,4) 生成跳转表,直接通过索引 O(1) 定位
lookupswitch case 值稀疏(如 1,100,1000) switch 语句中 case 值不连续,基于有序表的二分查找

if-else 无论多少分支都是逐个比较 O(n)。

示例:

// case 连续 → tableswitch
switch(i) {
    case 1: ... break;
    case 2: ... break;
    case 3: ... break;
}

// case 稀疏 → lookupswitch  
switch(i) {
    case 10: ... break;
    case 1000: ... break;
    case 10000: ... break;
}

2. switch 穿透的底层原因是什么?

  • switch 在字节码层面只是一个跳转入口标记,不是块结构。
switch(2) {
    case 1: System.out.print("A");
    case 2: System.out.print("B");  // 从这进入
    case 3: System.out.print("C");  // 继续执行,直到遇到 break
}
// 输出:BC
  • 底层原因
  1. switch 被编译成 lookupswitch/tableswitch 指令.
  2. case 只是 入口标记 ,不是块边界。
  3. 没有 break 就继续执行下一条指令 → 穿透。

二、数组的底层补充

1. 数组在内存中如何存储?

栈内存(局部变量)          堆内存
┌─────────────┐          ┌─────────────────┐
│  arr (引用)  │ ──────→  │ [0] [1] [2] [3] │ 连续存储
└─────────────┘          └─────────────────┘
                              ↑
                         每个元素固定大小
  • 核心知识点
  1. 数组是 连续内存 ,通过 基地址 + 索引 × 元素大小 直接访问 → O(1)。
  2. 数组名(引用)存在栈,数组对象存在堆。
  • 常见错误:
  1. 误区一:以为二维数组是矩阵
int[][] arr = new int[2][3];
System.out.println(arr.length);      // 2
System.out.println(arr[0].length);   // 3
// 错误理解:arr[0][2] 和 arr[1][0] 相邻
// 正确:行与行之间可能相隔很远
  1. 误区二:试图修改数组长度
int[] nums = {1,2,3};
// nums.length = 5;  // 编译错误,length 是 final 语义

2. 数组为什么用 0 作为第一个索引?

  • C 语言历史沿袭(Java 继承设计)。
  • 指针运算语义:arr[i] = *(arr + i)
  • 数学区间表达更简洁[0, n)

3. arr.length 是方法还是属性?为什么数组没有 length() 方法?

  • length 是数组类型的隐含属性,JVM 在对象头中存储,通过字节码指令 arraylength 读取。

  • String 的 length() 是方法。

  • 底层原因:

int[] arr = new int[5];
// arr.length 直接读取 JVM 对象头中存储的长度字段

JVM 中数组对象的内存结构:

                          ┌─────────────────────┐
                          │      对象头          │
                          │  ┌───────────────┐   │
                          │  │  Mark Word    │   │
                          │  │   (64位JVM中   │  │
                          │  │    8字节)      │   │
                          │  ├───────────────┤   │
                          │  │ Klass Pointer │   │
                          │  │   (4字节)      │   │
                          │  ├───────────────┤   │
                          │  │ Array Length  │   │  ← 数组独有
                          │  │   (4字节)      │   │
                          │  └───────────────┘   │
                          ├─────────────────────┤
                          │    实例数据           │
                          │  ┌───────────────┐   │
                          │  │  element[0]   │   │
                          │  ├───────────────┤   │
                          │  │  element[1]   │   │
                          │  ├───────────────┤   │
                          │  │      ...      │   │
                          │  └───────────────┘   │
                          ├─────────────────────┤
                          │  对齐填充(可选)      │
                          └─────────────────────┘

length 在数组创建时就确定,存储在对象头中,直接读取不需要方法调用。

4. 二维数组在内存中是连续的吗?

  • 严格来说,Java 二维数组的每一行内部是连续的。
  • 行与行之间不保证连续。
  • 不是单块连续内存,而是多块独立连续内存
  • Java 的二维数组是数组的数组:
int[][] matrix = new int[2][3];

实际内存结构:

栈              堆
matrix ──→ [引用0, 引用1]  ← 一维数组(存引用)
              ↓        ↓
           [0][1][2] [0][1][2]  ← 两个独立的一维数组
  • 第一维数组存储的是指向第二维数组的引用。
  • 每个第二维数组是独立的内存块,不一定连续。
  • 支持锯齿数组:
int[][] jagged = new int[2][]; 
jagged[0] = new int[3]; 
jagged[1] = new int[5];

5. 为什么数组长度不可变?

  • 数组在堆上分配的是固定大小的连续内存块:
int[] arr = new int[5];  // JVM 分配 5×4字节 = 20字节连续空间
  • 无法改变长度的原因:
  1. 扩容需要更大的连续空间,但前后可能已被占用。
  2. 如果要变,需要:分配新空间 → 拷贝数据 → 更新引用 → 回收旧空间。
  3. JVM 设计上不支持原地扩容。
  • ArrayList 的扩容原理:
// ArrayList 内部也是数组,扩容时:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
Object[] newArray = new Object[newCapacity];
System.arraycopy(oldArray, 0, newArray, 0, oldCapacity);
// 抛弃旧数组

三、字符串的底层补充

1. String 为什么设计成不可变?

  • 四个核心原因:
原因 解释
字符串常量池复用 可变字符串无法安全共享,修改一个会影响所有引用
线程安全 不可变对象天然线程安全,无需同步
哈希码缓存 hashCode 只计算一次并缓存,作为 HashMap 的 key 性能高
安全 类加载、数据库连接 URL 等参数不能被篡改
  • 代码证明不可变:
public final class String {
    private final char[] value;  // final + private
    // 没有任何方法可以修改 value 数组的内容
}

2. String Pool(字符串常量池)

String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");

System.out.println(s1 == s2);  // true(同一个池中对象)
System.out.println(s1 == s3);  // false(堆中新对象)

底层

  • 字面量 "abc" 存在字符串常量池(堆中,Java 7+)
  • new String("abc"):先在池中创建/查找,再在堆中创建新对象
  • intern() 方法可手动入池

3. == 和 equals() 的区别?为什么字符串比较要用 equals()?

  • 比较:
内容 使用
== 内存地址(引用) 基本类型:比较值;引用类型:比较地址
equals() 内容(由类定义) 引用类型:比较内容
String s1 = "abc";
String s2 = new String("abc");

s1 == s2        // false(不同对象)
s1.equals(s2)   // true(内容相同)

4. 字符串常量池在内存的哪个区域?(Java 7/8/17 变化)

JDK版本 字符串常量池位置 原因
Java 6 及以前 永久代 方法区的一部分,大小固定
Java 7 永久代 GC 效率低,移入堆可被 GC 回收
Java 8+ 永久代被元空间(Metaspace)替代,字符串常量池仍在堆
  • 验证代码:
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1 == s2.intern());  // true,intern() 返回池中引用

5. String StringBuilder StringBuffer 的区别?

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全 不安全 安全(方法加 synchronized)
性能 拼接时最差 最好 中等
适用场景 少量操作 单线程大量操作 多线程大量操作

6. 字符串拼接 + 的底层实现?

  • 编译期优化:
String s = "a" + "b" + "c";
// 编译后变成:
String s = "abc";  // 常量折叠
  • 运行期拼接:
String a = "a";
String b = "b";
String c = a + b;
// 编译后 → 
String c = new StringBuilder().append(a).append(b).toString();

7. intern() 方法的作用和原理?

  • intern() 返回字符串在常量池中的引用。
String s1 = new String("hello");
String s2 = s1.intern();
String s3 = "hello";

System.out.println(s1 == s2);  // false(s1 在堆,s2 在池)
System.out.println(s2 == s3);  // true(都在池中)

四、综合/进阶

1. 说出 Java 中所有不可变的特性

不可变特性 示例
String 任何修改都返回新对象
基本类型包装类 Integer、Long 等,value 是 final
数组长度 创建后不可变(内容可变)
final 修饰的引用 不能指向新对象(但对象内部可变)
Collections.unmodifiableXxx 返回只读视图

总结:补充后的知识层级

层级 内容
应用层 会用 if、for、数组、String 方法
原理层 跳转表、连续内存、不可变性、常量池
字节码层 tableswitch、数组的 length 属性、StringBuilder 优化
内存层 栈/堆分布、对象引用、GC 相关
  • 总结图:
┌─────────────────────────────────────────────┐
│                 Java 程序                    │
│  String s = "hello";                        │
│  int[] arr = new int[3];                    │
└─────────────────┬───────────────────────────┘
                  │
      ┌───────────┴───────────┐
      ▼                       ▼
┌─────────────┐         ┌─────────────┐
│   栈内存    │         │   堆内存    │
│  s 引用 ────┼──────→  │ "hello"     │
│  arr 引用 ──┼──────→  │ 数组对象    │
│  i = 2      │         │  (length,   │
└─────────────┘         │   element0,  │
                        │   element1)  │
                        └─────────────┘
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
             ┌────────────┐      ┌────────────┐
             │ 字符串常量池 │      │  GC 管理   │
             │  "hello"    │      │  不可变对象 │
             └─────────────┘      └────────────┘
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐