【Java】流程控制、数组、字符串进阶篇--语言根基(二)
·
【Java】流程控制、数组、字符串进阶篇
流程控制、数组、字符串进阶篇
本文适合:已学过 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
- 底层原因:
- switch 被编译成
lookupswitch/tableswitch指令. - case 只是 入口标记 ,不是块边界。
- 没有 break 就继续执行下一条指令 → 穿透。
二、数组的底层补充
1. 数组在内存中如何存储?
栈内存(局部变量) 堆内存
┌─────────────┐ ┌─────────────────┐
│ arr (引用) │ ──────→ │ [0] [1] [2] [3] │ 连续存储
└─────────────┘ └─────────────────┘
↑
每个元素固定大小
- 核心知识点:
- 数组是 连续内存 ,通过
基地址 + 索引 × 元素大小直接访问 → O(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] 相邻
// 正确:行与行之间可能相隔很远
- 误区二:试图修改数组长度
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字节连续空间
- 无法改变长度的原因:
- 扩容需要更大的连续空间,但前后可能已被占用。
- 如果要变,需要:分配新空间 → 拷贝数据 → 更新引用 → 回收旧空间。
- 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" │ │ 不可变对象 │
└─────────────┘ └────────────┘
更多推荐




所有评论(0)