判断一个人有没有真用过数组与多维数组,问他踩过什么坑就知道了。默认行为是什么、边界在哪、出错时是什么表现,背资料的人在这里会集体沉默。数组是 Java 里最基础也最容易被低估的结构:它看起来只是"一排格子",但越界、拷贝、与集合互转、多维的内存布局,每一处都能延伸出追问。把概念立住、把机制讲透,再落到项目里怎么用,这道题就能答出层次。
先把结论立住:数组的本质是定长连续容器
一句话:数组是长度在创建后不可变、元素在内存上连续分配的容器,arr.length 是 final 字段的访问,不是方法调用。多维数组在 Java 里是"数组的数组"——外层数组的每个元素都指向一个内层数组,第二维可以长度不齐,各行按引用独立分配。
这里要先立住一个认知前提:数组和 List 不是一回事。数组是语言级结构,协变且运行时类型检查;List 是容器类,提供动态扩容和丰富 API。很多误用都来自把两者混为一谈,比如以为 Arrays.asList 返回的是能随意增删的 List,结果一 add 就抛 UnsupportedOperationException。
机制拆解:分配、越界与拷贝
数组在 new 时由 JVM 在堆上分配连续空间并初始化零值(数字 0、布尔 false、引用 null)。访问 arr[i] 时 JVM 做边界检查,越界直接抛 ArrayIndexOutOfBoundsException——这是运行期检查,编译期拦不住。多维数组 int[][] grid = {
{1,2},{3,4}} 实际是:一个长度为 2 的外层数组,元素 0 指向 [1,2]、元素 1 指向 [3,4],两个内层数组各自独立分配,长度不必相等。
拷贝要分清浅拷贝和深拷贝。Arrays.copyOf 和 System.arraycopy 都是浅拷贝:对基本类型数组是值的完整副本,对引用类型数组只复制引用,两个数组指向同一批对象。要深拷贝得逐元素克隆。System.arraycopy 走本地方法拷贝,比手写循环快一个量级,Arrays.copyOf 底层就是它。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 数组:一维/多维、越界、拷贝与可变参数陷阱
int[] arr = new int[10];
arr[0] = 1; // 越界抛 ArrayIndexOutOfBoundsException
System.out.println(arr.length); // 字段访问,不是方法,输出 10
int[][] grid = {
{
1, 2}, {
3, 4} }; // 数组的数组,第二维长度可不齐
int sum = 0;
for (int[] row : grid) // 外层遍历行
for (int v : row) sum += v; // 内层遍历列,独立长度无碍
// 扩容式复制:长度变大,多出的位置补零值
int[] copy = java.util.Arrays.copyOf(arr, arr.length + 1);
// 可变参数陷阱:基本类型数组整体被当成一个元素
int[] nums = {
1, 2, 3};
System.out.println(java.util.Arrays.asList(nums).size()); // 输出 1,不是 3
// 正确:用装箱数组或逐个元素
System.out.println(java.util.Arrays.asList(1, 2, 3).size()); // 输出 3
// asList 返回的是固定长度的视图,add/remove 会抛异常
java.util.List<int[]> bad = java.util.Arrays.asList(nums);
// bad.add(...); // UnsupportedOperationException
这段代码值得盯三处:第一处,arr.length 是字段不是方法,越界是运行期异常;第二处,多维数组是数组的数组,内层各行独立分配,遍历时用双层 for-each 最省心;第三处,Arrays.asList(基本类型数组) 把整个数组当成一个元素,size 恒为 1,这是把数组当可变参数传入时的经典误用。面试讲到这一层,基本就稳了。
最常见的几个坑
最常见的坑是用 Arrays.asList 包装基本类型数组得到 size 恒为 1,因为泛型擦除后整个数组被当成一个 Object 元素。正确做法是传入装箱后的元素,或先用循环把基本类型转成包装类型数组,再 asList。
其次是把数组直接传给可变参数方法时混入 null,调用方按元素遍历时空指针。可变参数的底层就是数组,传 null 和传"元素为 null 的数组"语义不同,调用前把入参约定清楚能省掉一半排查时间。
还有一个更隐蔽的坑:数组与多维数组的坑别靠人记,应把高频踩坑点做成静态检查或单测断言。比如约定"数组入参禁止为 null""多维数组禁止锯齿长度不一致导致越界",用 ArchUnit 或自定义 lint 在编译期拦住,比靠 review 眼睛盯稳得多。
再一个是以为 arr.clone() 或 Arrays.copyOf 是深拷贝。对引用类型数组它们只复制引用,改其中一个数组里的对象,另一个数组立刻可见。需要深拷贝时逐元素克隆,或在业务上明确"共享引用是预期行为"。
面试中的经典追问
"请简单介绍一下数组与多维数组,它在 Android 开发中起什么作用?"——定义要准,例子要实:数组是定长连续容器,多维数组是数组的数组、第二维可不齐。再用项目场景证明你用过,比如用 int[][] 存网格地图、用 byte[] 做图片像素缓冲。让你给数组的典型误用写一条 lint 规则,这条规则会怎么描述?它考的是临场组织能力:按结论、依据、边界三步走。
"数组的底层原理是什么?能不能详细说一下?"——别停在 API 表层:动机是"定长、连续、随机访问 O(1)",机制是堆上连续分配加边界检查,代价是长度不可变、增删要整体拷贝。System.arraycopy 走本地拷贝比手写循环快一个量级,Arrays.copyOf 底层就是它。换个角度:如果让你给数组做一次性能画像,你会量哪些指标?分配大小、拷贝次数、越界异常频次,这三项是起点。
"在使用数组时遇到过什么问题?"——按现场、排查、根因、预防四段讲;全程围绕一次真实踩坑。比如曾把 Arrays.asList(基本类型数组) 当列表遍历,结果只拿到一个元素导致逻辑漏算,复现后用二分定位到入参转换那一行,修复方式是显式装箱。讲这种"复现—定位—修复"的结构,比空谈概念有说服力得多。
"数组和 ArrayList 相比有什么优劣?"——先列可选方案:数组、ArrayList、LinkedList;再按性能、灵活性、可读性对比,最后给场景结论:固定长度、追求极致性能或要和 JNI/底层 API 打交道用数组;需要动态增删用 ArrayList。把"数组定长、增删要拷贝"这类代价摆到台面上再决定。
工程落地:真实项目里怎么用
数组在项目里的高频落点:一是网络或磁盘 IO 的字节缓冲统一用 byte[],配合 System.arraycopy 做分块拼接,比反复创建中间 List 省内存;二是网格、棋盘、像素这类"二维但规整"的数据用 int[][],遍历用双层 for-each;三是和底层 SDK、NDK 打交道时数组是绕不开的桥,要明确谁拥有这块内存、谁来释放;四是把"数组入参非空、长度达标"做成前置断言,把越界挡在业务逻辑之前。
一个稳妥的工程约定:对外接口的数组入参一律先做空值与长度校验;多维数组若要求规整,在构造处断言各行长度一致;需要频繁增删的容器一律用 ArrayList 而非反复 copyOf;固定长度的配置表用数组并加 final。把这几条固化,数组相关的线上事故能少一大半。
还有一处常被忽略:Arrays.binarySearch 要求数组先按自然序或给定比较器排好序,否则返回的是无意义的负数而非异常;用它做"是否存在"判定前必须先排序,或确认数据已知有序再调用。另外,基本类型数组比包装类型数组省掉一层对象头与引用,在百万级数据上内存差距可达数倍——这种量级的选择在选型时该摆上桌面,而不是默认装箱图省事。
给正在准备面试的你
理解数组之后,需要在真实项目里练一遍:写一个 Arrays.asList(基本类型数组) 看 size 是多少,再改成装箱版本对比;再亲手用 System.arraycopy 拼接两个数组,打印结果确认拷贝正确。这两步做下来,比再读三遍文档都管用。
如果只记两句话,就记这两句:第一,Arrays.asList(基本类型数组) 把整个数组当一个元素,size 恒为 1;第二,Arrays.copyOf/System.arraycopy 是浅拷贝,引用类型数组只复制引用。把这两句讲顺,数组这一关就过了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android 软件开发面试·从入门到精通」连载系列
上一篇:循环-for-while-do-while:遍历与终止的工程课
下一篇预告:String-不可变性:为什么字符串要设计成不可变
有任何问题欢迎在评论区留言交流。