CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
数组
这一课你将能做到
用三种方式创建数组并正确初始化,理解默认值规则,避免越界异常
熟练使用 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.lengthint[] b = a; b[0]=9; 以为改的是 ba 也被改 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 实体类。