
三个 Java 数组编程题放在一起其实恰好覆盖了初学者到面试前最容易忽略的三个基本功遍历统计、方法返回值和引用传递。数组求和、奇偶分类、最大值最小值查找、原地逆序听起来都是入门级别的题目但我在实际带新人和模拟面试时发现能把这几道题写得干净、考虑周全的人并不算多。不是不会写而是习惯性地用能跑的标准要求自己结果一追问边界条件和设计理由就露馅。这篇文章就从这三个题目出发把每道题背后的设计意图、写法和踩坑点一次说清楚。1. 这三个数组题为什么值得手写一遍1.1 面试常客但考察点并不浅Java 数组求和与奇偶分类统计查找最大值最小值原地逆序数组这三道题经常出现在 Java 基础面试题和大学作业里但它们的考察层级完全不同。第一道题考的是遍历和取模运算属于语法层面第二道题考的是方法设计与返回值组织属于代码组织层面第三道题考的是对数组引用传递和空间复杂度的理解属于原理层面。把三道题串起来恰好构成一条从会写循环到理解方法本质的能力升级线。很多人在 LeetCode 或者牛客上刷题时喜欢直接用现成的工具类比如Arrays.stream(arr).sum()一行搞定求和但面试官追问一句底层怎么实现的如果数组是空数组会怎样往往就卡住。手写这三道题的真正价值不是告诉你答案而是逼你把每一个索引、每一次赋值、每一个返回值都落到实处。1.2 从需求到方法拆解编程思维的起点这三道题还有一个共性它们都要求你从需求中提炼方法。数组求和加奇偶统计本质是一个循环内完成多项统计的设计最大值最小值查找本质是方法如何返回多个结果的设计原地逆序本质是如何在不申请新数组的前提下完成元素重排的设计。这些设计思维会在后续写业务代码时反复用到比如分页统计、数据清洗、内存敏感型工具方法的封装。我建议每个 Java 学习者都亲手敲一遍这三道题而不是直接复制网上的答案。敲的过程中你会自然产生很多疑问为什么求和变量要初始化为 0为什么最大值要初始化为数组第一个元素而不是 0为什么逆序循环只走一半这些疑问才是真正的学习收获。2. 求和与奇偶分类一个循环里的统计学问2.1 需求拆解与基础实现先看题目需求给定一个 int 数组计算所有元素的总和同时统计偶数和奇数的个数。拆解一下这个需求包含两个子任务累加求和、分类计数。两个任务可以分开写两个循环也可以合并成一个循环。合并的好处是只遍历一次数组时间复杂度保持在 O(n)对于大数组来说性能更优。基础写法如下public class ArrayStats { public static void main(String[] args) { int[] arr {3, 8, 15, 22, 7, 10, 31, 4}; int sum 0; int evenCount 0; int oddCount 0; for (int i 0; i arr.length; i) { sum arr[i]; if (arr[i] % 2 0) { evenCount; } else { oddCount; } } System.out.println(数组元素之和: sum); System.out.println(偶数个数: evenCount); System.out.println(奇数个数: oddCount); } }运行结果数组之和是 100偶数个数是 48、22、10、4奇数个数是 43、15、7、31。代码本身很简单但有几个细节值得展开。2.2 奇偶判断的坑负数与取模很多人以为奇偶判断就是arr[i] % 2 0判断偶数arr[i] % 2 1判断奇数。如果数组里出现负数第二个判断就出问题了。Java 中取模运算的结果符号与被除数一致-5 % 2的结果是-1而不是1。所以-5 % 2 1是 false一个奇数被错误地归入非奇数分支。正确做法是判断不等于 0arr[i] % 2 ! 0表示奇数。或者用位运算(arr[i] 1) 0表示偶数(arr[i] 1) 1表示奇数。位运算判断奇偶在底层更高效因为 1只检查最低二进制位但可读性对初学者稍差。我个人的习惯是业务代码里用% 2 ! 0判断奇数既安全又直观只有在追求极致性能的算法场景才用位运算。注意如果数组元素可能为负数不要用arr[i] % 2 1判断奇数要用! 0。这是新手最容易踩的坑面试时也经常被单独拎出来问。2.3 进阶用增强 for 循环和流式写法懂了基础循环后可以看看更简洁的写法。如果循环里不需要索引参与计算增强 for 循环是更好的选择代码更短、也更不容易出现索引越界for (int num : arr) { sum num; if (num % 2 0) { evenCount; } else { oddCount; } }Java 8 之后的 Stream 也可以做但注意流式写法会创建额外的中间对象对于小数组完全没问题对超大数组反而增加开销int sum Arrays.stream(arr).sum(); long evenCount Arrays.stream(arr).filter(n - n % 2 0).count(); long oddCount arr.length - evenCount;这种写法适合追求代码简洁的场景比如写工具方法或者处理集合数据。但如果是面试我更推荐手写循环因为面试官想看到你对底层遍历逻辑的控制力而不是对 API 的熟练度。而且 Stream 的一次sum()加一次filter().count()其实是两次完整的数组遍历性能上不如手写单循环。3. 最大值最小值查找方法返回值的正确打开方式3.1 为什么从数组第一个元素初始化第二个题目的核心是方法实践。题目要求定义方法接收一个 int 数组查找并返回最大值和最小值。先说我见过的常见错误有人把最大值初始化为Integer.MIN_VALUE最小值初始化为Integer.MAX_VALUE这当然也能跑逻辑上完全没问题。但更自然的做法是把初始值设为数组的第一个元素arr[0]。为什么因为数组里真正的最大值和最小值一定存在于数组本身直接用arr[0]作为初始候选值可以减少一次无意义的比较而且天然处理了数组里全是负数的场景。如果初始化成 0一个全负数的数组会让最大值结果错误地变成 0初始化成Integer.MIN_VALUE虽然不会错但显得不够优雅。基础实现public class MaxMinFinder { public static void main(String[] args) { int[] arr {12, 45, 3, 78, 34, 56}; int max findMax(arr); int min findMin(arr); System.out.println(最大值: max); System.out.println(最小值: min); } public static int findMax(int[] arr) { // 防御性检查空数组或 null 直接抛出异常 if (arr null || arr.length 0) { throw new IllegalArgumentException(数组不能为空); } int max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } } return max; } public static int findMin(int[] arr) { if (arr null || arr.length 0) { throw new IllegalArgumentException(数组不能为空); } int min arr[0]; for (int i 1; i arr.length; i) { if (arr[i] min) { min arr[i]; } } return min; } }注意循环从i 1开始因为arr[0]已经作为初始值了不需要和自己比较。有些人习惯从i 0开始那也不会出错只是多做一次无意义的自我比较。这种细节在性能敏感的大数组遍历中虽然影响微乎其微但养成好习惯总没错。3.2 同时返回两个值的几种方案题目要求同时返回最大值和最小值。Java 方法只能有一个返回值怎么办有几种常见方案。方案一定义两个方法分别返回上面的代码就是这种思路。优点是职责单一、代码清晰缺点是遍历了两次数组时间复杂度变成 O(2n)。对于大多数场景这完全不是问题但严格来说存在优化空间。方案二在一个方法里同时找到最大值和最小值然后放到一个新的长度为 2 的数组里返回。一次遍历搞定空间开销是一个 2 元素数组public static int[] findMaxAndMin(int[] arr) { if (arr null || arr.length 0) { throw new IllegalArgumentException(数组不能为空); } int max arr[0]; int min arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } if (arr[i] min) { min arr[i]; } } return new int[]{max, min}; }调用方这样用int[] result findMaxAndMin(arr); System.out.println(最大值: result[0] , 最小值: result[1]);方案三如果觉得返回数组不够语义化可以定义一个简单的内部类Result包含max和min两个字段。这种写法在业务代码里更常见因为可读性好public static class MaxMinResult { public int max; public int min; } public static MaxMinResult findMaxAndMin(int[] arr) { MaxMinResult result new MaxMinResult(); result.max arr[0]; result.min arr[0]; // ... 遍历更新 return result; }从设计角度看方案二和方案三各有千秋。方案二零额外类定义适合简单工具方法方案三语义清晰适合作为 API 对外暴露。我个人倾向于方案二因为这道题的定位是方法实践重点在方法定义和返回值的使用把两个值装进数组是最直接、依赖最少的方式。如果你用的是 Java 17 及以上还可以用record定义不可变结果类型更加优雅。3.3 方法重载与边界保护既然是方法实践的题目顺便讲讲方法重载。你可以定义重载版本支持int[]、double[]甚至long[]数组public static double findMax(double[] arr) { if (arr null || arr.length 0) { throw new IllegalArgumentException(数组不能为空); } double max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } } return max; }重载的好处是调用方不用关心底层类型差异API 更友好。但注意重载方法之间要有足够的差异化避免参数类型模糊导致调用歧义。比如findMax(int[])和findMax(Integer[])同时存在调用findMax(null)就会编译报错因为编译器无法确定该匹配哪个。边界保护也是实践的一部分。如果传入null或者空数组arr[0]会抛出NullPointerException或ArrayIndexOutOfBoundsException这两种异常对调用方来说不够友好。显式抛出IllegalArgumentException并附上清晰错误信息是良好的编码习惯。这道题目虽然是练习但按生产代码的标准来写才能体现方法实践的真正意义。4. 原地逆序理解引用传递的绝佳案例4.1 双指针交换的原理第三个题目要求实现原地逆序数组并输出。原地两个字是核心意思是不能申请一个新的等长数组然后把原数组倒着填进去必须直接修改传入的数组。原地逆序的标准解法是双指针也叫对撞指针。左指针指向数组开头右指针指向数组末尾交换两个位置的元素然后左指针右移、右指针左移直到两个指针相遇或者错开。public class ArrayReverser { public static void main(String[] args) { int[] arr {1, 2, 3, 4, 5, 6, 7}; System.out.print(原始数组: ); printArray(arr); reverseInPlace(arr); System.out.print(逆序后: ); printArray(arr); } public static void reverseInPlace(int[] arr) { if (arr null || arr.length 0) { return; } int left 0; int right arr.length - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } } public static void printArray(int[] arr) { for (int i 0; i arr.length; i) { System.out.print(arr[i] ); } System.out.println(); } }输出结果为原始数组1 2 3 4 5 6 7逆序后7 6 5 4 3 2 1。交换三个步骤用生活类比来说就像两杯水互换必须借助第三个空杯子先倒一杯进去再倒另一杯最后把第三杯倒回去。temp就是那个空杯子。循环终止条件left right很关键。数组长度为偶数时左右指针最终会错开比如长度为 6指针会经历 (0,5)、(1,4)、(2,3)、(3,2) 时退出数组长度为奇数时左右指针最终会在中间元素相遇比如长度为 7走到 (3,3) 时退出此时中间元素不需要交换。如果用left right作为条件奇数长度数组的中间元素会和自身交换一次虽然结果正确但属于无效操作。4.2 为什么必须在原数组上操作这个问题值得展开。如果允许新数组逆序一行代码可以搞定public static int[] reverseWithNewArray(int[] arr) { int[] result new int[arr.length]; for (int i 0; i arr.length; i) { result[i] arr[arr.length - 1 - i]; } return result; }这种方式时间复杂度和原地方案一样是 O(n)但空间复杂度是 O(n)因为额外申请了一个等长数组。原地方案空间复杂度是 O(1)只使用了一个临时变量temp。在嵌入式开发、移动端或大数据处理场景中数组可能非常大额外申请一份完整拷贝的内存开销不可忽视。这就是原地这个要求背后的工程意义。这也是理解 Java 参数传递的绝佳案例。很多初学者以为Java 方法是值传递所以方法内修改数组不会影响外部变量这其实是对值传递的误解。Java 中数组变量存储的是对象的引用可以理解为指针方法参数接收到的是这个引用的拷贝但拷贝指向同一个数组对象。因此通过arr[left] ...这样直接操作数组元素实际上修改的是外部也能看到的同一块内存。只要你不重新new一个数组赋值给参数变量外部数组就会被修改。注意如果在方法内部执行arr new int[arr.length]或者其他重新赋值的操作那只改变参数引用本身的指向外部数组不受影响。这是原地操作必须理解的关键点。4.3 完整实现与输出验证上面给出的reverseInPlace方法已经可以直接运行。这里再补一个更简洁的交换写法用位运算代替temp变量适合炫技但可读性差一些了解即可public static void reverseInPlace(int[] arr) { int left 0; int right arr.length - 1; while (left right) { arr[left] arr[left] ^ arr[right]; arr[right] arr[left] ^ arr[right]; arr[left] arr[left] ^ arr[right]; left; right--; } }位运算交换不需要临时变量但有一个致命限制如果两个元素值相同异或后结果会是 0交换出错。不过对于逆序场景交换的两个元素通常不同而且即使相同比如数组有两个 5三次异或后你会发现两个位置还是 5结果碰巧正确。但这个方法在可读性和健壮性上都不如temp方案我强烈建议只在面试聊底层原理时提到它实际代码一律用临时变量。输出验证部分我建议封装一个printArray方法而不是在main里写死循环。这样测试不同数组时只需复用方法也符合方法实践的题目要求。5. 常见问题与排查技巧实录5.1 问题速查表这三道题我在教学和面试点评中收集了不少典型错误整理成一张表格方便你对照自查症状原因解决方法求和的初始值为 0但结果异常局部变量未初始化直接使用或累加变量误放在循环内部累加变量定义在循环外并初始化为 0负数的奇数统计不准确用% 2 1判断奇数负数取模结果为 -1改用arr[i] % 2 ! 0空数组调用findMax报ArrayIndexOutOfBoundsException直接访问arr[0]未做防御方法开头检查arr.length 0抛出带信息的异常全负数数组的最大值结果是 0最大值初始化为 0 而非arr[0]初始值取arr[0]循环从 1 开始逆序后数组没有变化方法内创建了新数组赋值给参数没有直接操作元素直接通过索引修改元素确认没有arr new int[]逆序结果正确但多执行了一半循环循环条件写成left length / 2或类似统一用while (left right)或for (int i 0; i arr.length / 2; i)交换元素用a b; b a;没有临时变量保存原值先用temp保存a的值再依次赋值其中最后一条是最经典的。有人写交换时图省事写成arr[left] arr[right]; arr[right] arr[left];结果两个位置变成了同一个值。你完全可以记住交换必须三行第三杯是 temp这个口诀永远不会出错。5.2 实战心得最后分享几个我在实际教学和写代码过程中的体会。第一个体会是这三道题不要只写一次。第一遍用最基础的 for 循环写完第二遍尝试增强 for 循环第三遍尝试用方法封装。每换一种写法你对数组和方法的理解都会加深一层。我自己带新人的时候要求他们至少写三遍每一遍都要比上一遍更简洁或者更健壮。第二个体会是面试时不要急着炫技。面试官问这道题核心是想看你的代码风格和边界意识。你写一个for (int num : arr)的单循环统计比写一个冗长的 Stream 链更稳妥。等面试官主动问你还能怎么优化你再提出位运算、双指针或者 Stream 方案会显得你基本功扎实且知识面广。第三个体会是把这三道题当作你学习 Java 基础的一个里程碑组合。数组遍历是你接触数据结构的起点方法定义是你组织代码的起点引用传递是你理解 Java 内存模型的起点。三个起点正好是这三道题分别对应的能力点。花一两个小时认真写完它们比漫无目的地刷十道难题更有收获。如果你能把每一步的为什么都讲清楚那说明 Java 的基础关已经过了大半。