大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
数组
这一课你将能做到
- 用三种方式创建数组并正确初始化,理解默认值规则,避免越界异常
- 熟练使用
Arrays工具类的打印、排序、复制、填充、比较方法 - 说清数组与
ArrayList的取舍依据,理解「数组是引用类型」带来的复制行为
为什么要学
数组是最原始的批量存储结构,也是几乎所有高级容器(ArrayList、HashMap)的底层积木。后端开发里你更多用 List,但底层碰到的全是数组:
- MySQL 驱动把一行行结果装进数组再组装成 List
main(String[] args)的参数就是数组- Spring 的
@Value("${servers}")注入配置列表、String...可变参数,本质都是数组 - 算法题第一关(两数之和、二分查找)全是数组
不理解数组,集合阶段的扩容、拷贝、性能全都似懂非懂。
先建立图像
数组 = 一排编了号的储物柜:
索引: 0 1 2 3 4
┌──────┬──────┬──────┬──────┬──────┐
值: │ 101 │ 102 │ 103 │ 104 │ 105 │
└──────┴──────┴──────┴──────┴──────┘
▲
arr[0] 从 0 开始编号!最后一个下标 = length - 1
三个铁律:定长(创建后不能加格子)、同型(所有格子装同一类型)、从 0 编号。
跟着做
步骤 1:三种创建方式
新建 ArrayDemo.java:
import java.util.Arrays;
public class ArrayDemo {
public static void main(String[] args) {
// 写法 1:先定长度,后赋值(长度必须给出)
int[] ids = new int[3];
ids[0] = 101;
ids[1] = 102;
ids[2] = 103;
System.out.println(ids[0]); // 101
// 写法 2:字面量直接初始化(最常用)
String[] names = {"mysql", "redis", "spring"};
System.out.println(names.length); // 3
// 写法 3:new + 字面量(适合「先声明后赋值」场景)
double[] prices = new double[]{9.9, 19.9};
System.out.println(Arrays.toString(prices)); // [9.9, 19.9]
}
}
步骤 2:默认值与默认初始化
public class DefaultValues {
public static void main(String[] args) {
int[] a = new int[3];
double[] b = new double[2];
boolean[] c = new boolean[2];
String[] d = new String[2]; // 引用类型默认 null!
System.out.println(a[0]); // 0
System.out.println(b[0]); // 0.0
System.out.println(c[0]); // false
System.out.println(d[0]); // null
System.out.println(d[0].length()); // 炸:NullPointerException
}
}
数组的每个格子都有默认值:整型 0、浮点 0.0、boolean false、引用类型 null。「null 格子直接调方法」是新手 NPE 高发区。
步骤 3:遍历的两种姿势
public class ArrayLoop {
public static void main(String[] args) {
int[] scores = {88, 92, 75, 60};
// 普通 for:需要下标时用
for (int i = 0; i < scores.length; i++) {
System.out.printf("第 %d 个: %d%n", i + 1, scores[i]);
}
// 增强 for:只关心值时用
int sum = 0;
for (int s : scores) {
sum += s;
}
System.out.println("总分: " + sum + ", 均分: " + (double) sum / scores.length);
// 求最值 + 倒序遍历
int max = scores[0];
for (int i = scores.length - 1; i >= 0; i--) {
if (scores[i] > max) max = scores[i];
}
System.out.println("最高分: " + max);
}
}
步骤 4:Arrays 工具类全家桶
import java.util.Arrays;
public class ArraysUtilDemo {
public static void main(String[] args) {
int[] a = {5, 2, 8, 1};
// 打印:直接 println 会输出 [I@1b6d3586(地址),必须用 toString
System.out.println(Arrays.toString(a)); // [5, 2, 8, 1]
// 排序:原地升序(双轴快排)
Arrays.sort(a);
System.out.println(Arrays.toString(a)); // [1, 2, 5, 8]
// 二分查找:必须先排序!返回下标,找不到返回负数
int idx = Arrays.binarySearch(a, 5);
System.out.println(idx); // 2
// 复制:扩容式复制,超出部分补默认值
int[] bigger = Arrays.copyOf(a, 6);
System.out.println(Arrays.toString(bigger)); // [1, 2, 5, 8, 0, 0]
// 区间复制:含头不含尾
int[] part = Arrays.copyOfRange(a, 1, 3);
System.out.println(Arrays.toString(part)); // [2, 5]
// 填充:把所有格子填成同一个值(初始化默认值常用)
int[] filled = new int[4];
Arrays.fill(filled, -1);
System.out.println(Arrays.toString(filled)); // [-1, -1, -1, -1]
// 比较:逐元素比较,数组不能用 == 或 equals 比(见步骤 6)
System.out.println(Arrays.equals(new int[]{1, 2}, new int[]{1, 2})); // true
}
}
步骤 5:数组是引用类型——复制行为实证
import java.util.Arrays;
public class ArrayRefDemo {
public static void main(String[] args) {
int[] original = {1, 2, 3};
int[] alias = original; // 赋值:只是多个「遥控器」,同一个数组
alias[0] = 99;
System.out.println(Arrays.toString(original)); // [99, 2, 3] 被改了!
int[] copy = Arrays.copyOf(original, original.length); // 真拷贝
copy[0] = -1;
System.out.println(Arrays.toString(original)); // [99, 2, 3] 不受影响
System.out.println(original == alias); // true:同一对象
System.out.println(original == copy); // false:不同对象
}
}
步骤 6:二维数组
import java.util.Arrays;
public class TwoDArray {
public static void main(String[] args) {
// 3 行 2 列:想象成「数组的数组」
int[][] matrix = {
{1, 2},
{3, 4},
{5, 6}
};
System.out.println(matrix.length); // 3(行数)
System.out.println(matrix[0].length); // 2(第一行列数)
System.out.println(matrix[1][0]); // 3
// 遍历:外层行,内层列
for (int[] row : matrix) {
System.out.println(Arrays.toString(row)); // [1, 2] ...
}
// 经典坑:打印二维数组不能用 Arrays.toString,要用 deepToString
System.out.println(Arrays.toString(matrix)); // 地址垃圾
System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4], [5, 6]]
}
}
讲清楚要点
- 数组是定长的对象:
length是属性不是方法(arr.length,注意 String 是length()方法——易混)。 - 合法下标 0 ~ length-1:越界抛
ArrayIndexOutOfBoundsException,运行时才炸,编译器不拦。 - 赋值 ≠ 复制:
int[] b = a只复制地址;真复制用Arrays.copyOf或a.clone()。 - 数组比不了内容:
==比地址;数组的equals没重写、等价于==;比内容必须Arrays.equals(多维用deepEquals)。 - 数组转 List 有陷阱:
Arrays.asList(a)返回的是定长视图,不能增删;对象数组才支持,int[]转出来的是「装了一个数组的 List」。 - 性能特征:按下标随机访问 O(1);中间插入/删除要整体挪动 O(n) 且定长做不到——这正是集合(第 9 课)存在的理由。
你可能会懵的地方
-
int[] a和int a[]哪个对? 都合法,后者是 C 风格遗产。行业规范一律int[] a——类型信息集中在左边,可读性好。 -
数组为什么从 0 开始编号? 索引本质是「偏移量」:
a[i]的地址 = 首地址 + i × 元素大小。从 0 开始让地址计算最简单,这是 1960 年代 BCPL 以来的传统。 -
Arrays.asList 之后 add 报错?
Arrays.asList返回java.util.Arrays内部的定长 List 视图,底层还是那个数组,add/remove 直接抛UnsupportedOperationException。要可变的:new ArrayList<>(Arrays.asList(...))。 -
什么时候用数组、什么时候用 List? 长度固定、性能敏感(算法题、底层库)用数组;日常业务几乎一律 List——可变长、API 丰富、能配合泛型。本站后端业务代码 95% 用 List。
-
int[] 能直接赋给 Object[] 吗? 不能。基本类型数组和对象数组没有继承关系,
Object[] o = new int[3]编译错。数组协变只发生在「同一元素类型的引用类型」之间(如String[] → Object[],且这种协变本身有坑)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
int[] a = new int[3]{1,2,3}; | 语法冲突,两者选一 | new int[]{1,2,3} 或 new int[3] |
System.out.println(arr) | 打印出 [I@1b6d3586 | Arrays.toString(arr) |
for (int i = 0; i <= arr.length; i++) | 越界异常 | i < arr.length |
int[] b = a; b[0]=9; 以为改的是 b | a 也被改 | Arrays.copyOf(a, a.length) |
if (arr1 == arr2) 或 arr1.equals(arr2) | 比地址,内容相同也 false | Arrays.equals(arr1, arr2) |
String[] s = new String[2]; s[0].length() | 格子是 null,NPE | 先赋值再使用 |
卡点急救
- ArrayIndexOutOfBoundsException:打印报错行号,检查条件是否
<= length、下标是否从 1 开始数。空数组new int[0]的任何下标都非法。 - 打印数组显示
[I@xxxx/[Ljava.lang.String;@xxxx:[开头加类型字母是数组的类名,说明你打印了对象地址。换Arrays.toString。 - ** NullPointerException(数组版)**:引用类型数组格子默认 null。遍历时先判空,或创建时就用字面量初始化。
- 排序后二分查找结果诡异:
binarySearch前必须Arrays.sort,否则结果无意义(返回负数不一定是「不存在」的位置)。 - 增强 for 里改数组没生效(存的是值类型):
for (int s : scores) s = 0;改的是局部变量。要改原数组必须用下标:scores[i] = 0。
面试常问
-
数组与 ArrayList 的区别? 定长 vs 动态扩容(1.5 倍增长);基本类型友好 vs 只能装对象(泛型擦除);语法朴素 vs API 丰富;连续内存 vs 底层也是数组。数组适合固定规模/性能敏感场景。
-
数组按下标访问为什么是 O(1)? 地址 = 首地址 + 下标 × 元素大小,一次乘加直达,无需遍历。这也是数组有序时二分查找能到 O(log n) 的前提。
-
==、equals、Arrays.equals三者对数组的区别?==比地址;equals未重写等同==;Arrays.equals逐元素比较。二维/嵌套结构用deepEquals。 -
Arrays.copyOf和System.arraycopy的区别?copyOf底层调用System.arraycopy,返回新数组、可扩容;System.arraycopy是 native 方法,写入已有目标数组、性能最优(ArrayList 扩容就用它)。 -
冒泡排序手写一遍(算法面试热身) 双层循环:外层控制轮数,内层相邻比较交换,每轮把最大值「冒」到末尾。写在草稿上,注意内层边界
length - 1 - i。
自测三问
String[] arr = new String[3]后直接arr[0].toUpperCase()会发生什么?为什么?int[] a = {1,2}; int[] b = a; b[0] = 9;后a[0]是多少?想要互不影响怎么做?- 打印一个
int[][]应该用哪个方法?为什么Arrays.toString不行?
小结一句话
数组是一排从 0 编号的定长同型格子:length 无括号、下标到 length-1、赋值只传地址、比内容找 Arrays、打印找 toString——数组是理解一切集合的底座。
延伸
- Oracle:数组
- 下一课:类与封装——把字段和方法装进对象,写出本站后端同款的 User 实体类。