Java数组
数组是 Java 中最基础的数据结构之一,用于保存一组类型相同、长度固定的元素。它看起来简单,但涉及内存布局、默认值、多维结构、浅拷贝、边界检查、泛型限制等很多基础知识。
在实际开发中,数组常出现在以下场景:
- 方法参数,例如
main(String[] args)。 - 底层容器实现,例如
ArrayList内部使用数组。 - 批量数据处理,例如缓存、缓冲区、动态规划。
- JVM 和框架底层,例如方法参数、反射调用、字节数组。
# 1. 数组的核心特点
| 特点 | 说明 |
|---|---|
| 类型固定 | 一个数组只能保存同一种元素类型 |
| 长度固定 | 创建后 length 不会变化 |
| 索引访问 | 通过下标访问,时间复杂度 O(1) |
| 下标从 0 开始 | 第一个元素是 array[0] |
| 数组本身是对象 | 数组有对象头、类型信息和 length 属性 |
| 边界检查 | 越界访问会抛出 ArrayIndexOutOfBoundsException |
数组变量保存的是引用,数组对象位于堆中。
int[] numbers = new int[3];
内存关系:
栈帧
┌────────────────┐
│ numbers 引用 │
└───────┬────────┘
│
▼
堆内存
┌─────────────────────────┐
│ int[3] │
│ length = 3 │
│ [0] = 0 │
│ [1] = 0 │
│ [2] = 0 │
└─────────────────────────┘
# 2. 声明与初始化
推荐写法:
int[] numbers;
String[] names;
不推荐但合法的写法:
int numbers[];
创建指定长度数组:
int[] scores = new int[5];
静态初始化:
int[] scores = {90, 85, 70};
动态初始化:
String[] names = new String[3];
names[0] = "Tom";
names[1] = "Jerry";
names[2] = "Alice";
匿名数组:
printScores(new int[] {90, 80, 70});
错误写法:
int[] scores;
scores = {90, 80, 70}; // 编译失败
分开声明和赋值时,应写成:
int[] scores;
scores = new int[] {90, 80, 70};
# 3. 默认值
数组元素会自动初始化为类型默认值。
| 元素类型 | 默认值 |
|---|---|
byte、short、int、long | 0 |
float、double | 0.0 |
char | '\u0000' |
boolean | false |
| 引用类型 | null |
引用类型数组容易出现空指针:
User[] users = new User[3];
users[0].getName(); // NullPointerException
正确做法:
User[] users = new User[3];
users[0] = new User("Tom");
System.out.println(users[0].getName());
引用数组内存:
栈帧
┌─────────────┐
│ users 引用 │
└──────┬──────┘
▼
堆内存
┌───────────────────────┐
│ User[3] │
│ [0] ───────────────┐ │
│ [1] = null │ │
│ [2] = null │ │
└────────────────────┼──┘
▼
┌────────────┐
│ User("Tom")│
└────────────┘
# 4. 访问与遍历
通过下标访问:
int first = scores[0];
scores[1] = 100;
普通 for 循环:
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
增强 for 循环:
for (int score : scores) {
System.out.println(score);
}
两者区别:
| 方式 | 适合场景 | 是否能拿到下标 | 是否适合修改元素 |
|---|---|---|---|
普通 for | 需要下标、需要修改 | 可以 | 可以 |
增强 for | 只读遍历 | 不可以 | 基本类型不能直接修改原数组元素 |
示例:
int[] numbers = {1, 2, 3};
for (int number : numbers) {
number = number * 2;
}
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]
增强 for 中的 number 是元素值的副本,不会改到原数组。
正确修改:
for (int i = 0; i < numbers.length; i++) {
numbers[i] = numbers[i] * 2;
}
# 5. 多维数组
Java 的多维数组本质是“数组的数组”。
int[][] matrix = new int[2][3];
结构:
matrix
│
▼
┌──────────────┐
│ int[][] │
│ [0] ─────┐ │
│ [1] ──┐ │ │
└───────┼──┼───┘
│ │
▼ ▼
┌──────────┐
│ int[3] │ [0, 0, 0]
└──────────┘
┌──────────┐
│ int[3] │ [0, 0, 0]
└──────────┘
二维数组初始化:
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
Java 支持不规则二维数组:
int[][] triangle = new int[3][];
triangle[0] = new int[] {1};
triangle[1] = new int[] {2, 3};
triangle[2] = new int[] {4, 5, 6};
结构:
triangle[0] -> [1]
triangle[1] -> [2, 3]
triangle[2] -> [4, 5, 6]
遍历二维数组:
for (int row = 0; row < matrix.length; row++) {
for (int col = 0; col < matrix[row].length; col++) {
System.out.println(matrix[row][col]);
}
}
不要默认每一行长度都相同,尤其是数组来自外部输入时。
# 6. 数组复制
常见复制方式:
| 方法 | 说明 |
|---|---|
System.arraycopy | 原生复制,性能好 |
Arrays.copyOf | 创建新数组并复制 |
array.clone() | 克隆数组对象 |
| 手动循环 | 可在复制时做转换 |
System.arraycopy:
int[] source = {1, 2, 3, 4};
int[] target = new int[4];
System.arraycopy(source, 0, target, 0, source.length);
参数含义:
System.arraycopy(
source, 源数组
sourcePos, 源数组起始位置
target, 目标数组
targetPos, 目标数组起始位置
length 复制元素个数
)
Arrays.copyOf:
int[] source = {1, 2, 3};
int[] copy = Arrays.copyOf(source, 5);
System.out.println(Arrays.toString(copy)); // [1, 2, 3, 0, 0]
# 6.1 浅拷贝问题
对象数组复制时,复制的是引用,不是对象本身。
User[] users = {new User("Tom")};
User[] copy = Arrays.copyOf(users, users.length);
copy[0].setName("Jerry");
System.out.println(users[0].getName()); // Jerry
内存关系:
users[0] ─────┐
▼
User 对象
▲
copy[0] ─────┘
两个数组指向同一个 User 对象,所以修改对象内容会互相影响。
# 7. Arrays 工具类
java.util.Arrays 提供了大量数组工具方法。
| 方法 | 作用 |
|---|---|
toString | 输出一维数组 |
deepToString | 输出多维数组 |
sort | 排序 |
binarySearch | 二分查找 |
copyOf | 扩容或截断复制 |
fill | 填充 |
equals | 比较一维数组内容 |
deepEquals | 比较多维数组内容 |
asList | 转成固定大小 List |
示例:
int[] numbers = {3, 1, 2};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]
二分查找前必须有序:
int index = Arrays.binarySearch(numbers, 2);
如果数组无序,二分查找结果没有意义。
多维数组输出:
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.toString(matrix));
System.out.println(Arrays.deepToString(matrix));
Arrays.toString(matrix) 输出的是内层数组引用信息;deepToString 才会递归输出元素。
# 8. 数组与可变参数
可变参数本质上是数组。
public static int sum(int... numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
调用:
sum(1, 2, 3);
sum(new int[] {1, 2, 3});
可变参数规则:
- 一个方法最多只能有一个可变参数。
- 可变参数必须放在参数列表最后。
- 方法内部按照数组使用。
错误写法:
void method(String... names, int count) {
}
正确写法:
void method(int count, String... names) {
}
# 9. 数组与集合的区别
| 对比项 | 数组 | ArrayList |
|---|---|---|
| 长度 | 固定 | 可动态扩容 |
| 元素类型 | 可存基本类型和引用类型 | 只能存引用类型 |
| API | 较少 | 丰富 |
| 随机访问 | O(1) | O(1) |
| 插入删除 | 通常需要移动元素 | 底层也可能移动元素 |
| 泛型支持 | 不支持泛型数组创建 | 支持泛型 |
数组适合长度稳定、性能敏感、元素类型明确的场景。集合适合业务数据数量动态变化、需要丰富操作 API 的场景。
# 10. Arrays.asList 的坑
String[] names = {"Tom", "Jerry"};
List<String> list = Arrays.asList(names);
这个 list 是固定大小列表:
list.add("Alice"); // UnsupportedOperationException
如果需要可变列表:
List<String> list = new ArrayList<>(Arrays.asList(names));
基本类型数组更容易踩坑:
int[] numbers = {1, 2, 3};
List<int[]> list = Arrays.asList(numbers);
System.out.println(list.size()); // 1
因为泛型不能使用基本类型,int[] 会被当作一个整体元素。
# 11. 常见异常
# 11.1 数组越界
int[] numbers = {1, 2, 3};
System.out.println(numbers[3]); // ArrayIndexOutOfBoundsException
合法下标范围是 0 到 length - 1。
# 11.2 空指针
int[] numbers = null;
System.out.println(numbers.length); // NullPointerException
数组引用为 null 时,不能访问 length 或元素。
# 11.3 数组存储异常
数组是协变的:
String[] strings = new String[1];
Object[] objects = strings;
objects[0] = 123; // ArrayStoreException
编译期允许把 String[] 赋值给 Object[],但运行期数组仍然知道自己真实元素类型是 String,因此存入 Integer 会失败。
# Tips 快问快答
Q:数组是基本类型还是引用类型? A:数组是引用类型,数组对象在堆中,数组变量保存引用。
Q:数组长度能改变吗? A:不能。扩容本质是创建新数组,再复制旧数组元素。
Q:array.length 和 String.length() 有什么区别?
A:数组的 length 是属性,字符串的 length() 是方法。
Q:为什么数组下标从 0 开始? A:可以把下标理解为相对起始位置的偏移量,第一个元素偏移量是 0。
Q:增强 for 能修改数组元素吗?
A:对基本类型不能修改原元素;对引用类型可以通过引用修改对象内部状态,但不能让原数组元素指向新对象。
Q:二维数组一定是矩阵吗? A:不是。Java 二维数组是数组的数组,每一行长度可以不同。
Q:数组复制是深拷贝吗? A:基本类型数组复制值;引用类型数组复制引用,通常是浅拷贝。
Q:Arrays.asList 返回的列表可以新增元素吗?
A:不能,它是固定大小列表。如果需要新增元素,应包装成 new ArrayList<>(...)。
Q:为什么 Arrays.asList(new int[]{1,2}) 长度是 1?
A:因为 int[] 被当成一个对象元素,而不是被拆成多个 Integer。
Q:数组为什么比很多集合访问快? A:数组结构简单,支持按下标直接定位元素;集合通常在数组之上增加了扩容、泛型、边界处理等逻辑。