☰
Java数组经典练习题:求和、最值查找与原地逆序全解析
2026/9/30 4:32:59 网站建设 项目流程

说实话,Java 数组这块儿的内容,我见过太多人"题能写对、问就卡壳"。求和、找最大值最小值、原地逆序,这三道题单独拎出来,随便一个学过 for 循环的人都写得出来,可一旦被追问"为什么初始值要取 arr[0]""为什么逆序只需要遍历一半""方法到底该返回什么类型",很多人的思路就开始打结。这篇文章想做的,就是把这几个经典编程题从需求拆解、代码实现到方法封装完整走一遍,把关键细节、边界处理和那些不写在教科书里的坑都摊开讲。内容主要围绕三个场景:数组求和与奇偶分类统计、数组最大值与最小值查找与返回、原地逆序数组并输出。适合正在学 Java 基础的朋友、准备初级面试的准程序员,也适合需要给团队新人做代码培训的老手当教案参考。

1. 数组求和与奇偶分类统计:拿到题目先拆需求

1.1 一个表面简单的需求,背后藏着三个能力点

"数组求和与奇偶分类统计",这句描述放到真实开发环境里,其实就是一行业务需求的浓缩。直接上手写循环的人不少,但我会建议先在纸上把需求拆开:第一,要遍历数组;第二,要按奇偶性分类;第三,要把统计结果组织好再输出。这三点分别对应 Java 里循环结构、取模运算符、变量和输出的使用,看起来基础,却是后面所有复杂逻辑的地基。

拆需求的时候还要问清楚一个问题:到底是只统计奇数和偶数的个数与总和,还是要把奇数、偶数这两类元素本身收集起来?这两种需求对应的数据结构完全不同。前者靠几个 int 变量累加就行,后者得用 ArrayList 之类的动态容器。我见过不少初学者上来就写 ArrayList,其实根本不需要;也见过有人只算了总和,把"分类统计"里的"分类"两个字漏掉了。把需求量化成"输入是什么、处理分几步、输出要哪几项",再开始写代码,返工率会低很多。

1.2 第一版实现:循环、取模与累加的关键细节

基础版代码长这样:

public class ArrayStat { public static void main(String[] args) { int[] nums = {12, 5, 7, 8, 3, 10, 21}; int sum = 0; int oddCount = 0; int evenCount = 0; int oddSum = 0; int evenSum = 0; for (int num : nums) { sum += num; if (num % 2 == 0) { evenCount++; evenSum += num; } else { oddCount++; oddSum += num; } } System.out.println("数组总和:" + sum); System.out.println("奇数个数:" + oddCount + ",奇数和:" + oddSum); System.out.println("偶数个数:" + evenCount + ",偶数和:" + evenSum); } }

这里最核心的判断就是num % 2 == 0。取模运算符%返回整数除法剩下的余数,任何整数对 2 取模,结果只可能是 0 或 1(负数的情况下一节单独讲)。余数为 0 说明能被 2 整除,是偶数;否则走 else 分支,归为奇数。用增强 for 遍历数组,不需要关心下标,代码最简洁,适合这种只读不改的场景。如果想练习基本功,也可以用普通 for 配合nums[i]写一遍,两种方式都要能随手敲出来,面试时经常被要求切换写法。

这版代码我特意用了四个统计变量而不是一个数组,因为需求里各个数字是独立的输出项,分开放语义最清楚。等你能熟练区分"什么时候用变量、什么时候用数组、什么时候用集合",对数据结构的理解就上一个台阶了。

1.3 负数取模:新手最容易忽略的隐藏地雷

如果你刚接触 Java 不久,有个细节特别容易埋雷:Java 对负数取模,结果的符号跟随被除数。比如-3 % 2结果是 -1,而不是很多人直觉中的 1。按上面代码num % 2 == 0来判断,-3 % 2等于 -1,不等于 0,会走进 else 分支被当作奇数。从数学定义看 -3 确实是奇数,结果歪打正着是对的。可如果有人在判断里写num % 2 == 1,负数奇数就全部漏掉了,统计结果直接出错。

稳妥的做法有两种。一种是只判断偶数分支,奇数走 else,就像上面代码那样;另一种是用位运算(num & 1) == 0判断偶数。位运算的原理是拿数字和 1 做按位与,奇数的最低位一定是 1,偶数的最低位一定是 0,所以结果只可能是 0 或 1,彻底避开正负号问题。位运算效率略高一点点,但可读性对初学者没有取模直观。我的习惯是推荐第一种写法,然后在注释里说明负数的情况。别小看这个细节,面试官特别喜欢把它伪装成"数组里有负数怎么统计奇偶"来挖坑。

1.4 只算个数不够用:用 ArrayList 收集元素

有些场景不只要数字,还要把奇数和偶数分别收集起来备用。这时候只靠几个 int 变量就不够了,需要动态容器。推荐用ArrayList<Integer>:

import java.util.ArrayList; import java.util.List; List<Integer> evens = new ArrayList<>(); List<Integer> odds = new ArrayList<>(); for (int num : nums) { if ((num & 1) == 0) { evens.add(num); } else { odds.add(num); } }

ArrayList 和数组的核心区别在于长度是否固定。数组一旦 new 出来,长度就锁死了;ArrayList 内部维护了一个可扩容的数组,调用 add 的时候容量不够会自动扩容,适合"元素个数事先不知道"的场景。面试里如果被问"为什么不用数组装结果",标准答案就是:数组长度必须预先确定,而奇数和偶数的数量未知,用集合更合理。这道题如果你能用evens.add(num)这种写法一眼看出"这是在动态收数据",基础就已经合格了。JDK 8 之后也可以用 Stream 的collect(Collectors.partitioningBy(...))做分组,但那是锦上添花,基础写法必须烂熟于心。

2. 最大值与最小值查找:方法返回值设计是门学问

2.1 为什么非要用"方法"把查找逻辑封装起来

第二题题面是"数组最大值与最小值查找与返回",重点其实落在"方法实践"四个字上。很多新手直接在 main 里写两段循环就交差,面试官一眼就能看出没理解封装。把查找逻辑抽成独立方法,至少四层好处:main 函数变瘦、逻辑可以复用、方便单独测试、接口清晰易维护。这也是实际开发天天在做的事——一个方法只做一件事,命名、入参、返回值都明明白白,别人读代码的时候不用一头扎进实现细节里。

可能有读者觉得,就几行循环还封装什么?我可以举一个现实场景:一个报表系统里,十个地方都要找最大值,如果每次都在业务代码里写循环,一旦要改判断逻辑,十个地方都要跟着改,漏改一个就出线上事故。抽成findMax(int[] arr)之后,所有调用方只依赖方法签名,内部怎么改都不影响它们。这就是封装的价值,也是这类"基础题"背后真正想训练的东西。

2.2 为什么初始值必须是 arr[0],以及循环从哪里开始

先看最简单的最大值查找方法:

public static int findMax(int[] arr) { if (arr == null || arr.length == 0) { throw new IllegalArgumentException("数组为空或为null"); } int max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }

我见过太多新人写成int max = 0,这在数组元素全为负数的时候直接翻车——最大值明明是 -1,程序却输出 0。把初始值设为arr[0],就是先假设"第一个元素就是当前最大的",然后从下标 1 开始逐个比较,谁更大谁上位。为什么循环从 1 开始?因为下标 0 已经被拿来当初始值了,没必要再和自己比一次。这里的时间复杂度是 O(n),数组无序的情况下,没有任何办法跳过比较,这已经是查找问题的理论下限。最小值方法对称着写就行,把>换成<。

我建议初学者亲手把这段代码跑三组数据验证:正数数组、全负数数组、单元素数组。单元素数组时循环一次都不会执行,直接返回唯一元素,逻辑也是对的。这几组用例跑下来,对边界条件的理解会比看十遍书都深。

2.3 一次遍历同时拿最大值和最小值:返回值怎么设计

如果分别写 findMax 和 findMin,两个方法各遍历一次,总比较次数是 2n,常数翻倍。更常见的做法是一个方法同时返回两个结果。但 Java 方法只能返回一个引用,怎么办?思路很直接:把两个结果塞进一个长度为 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(new int[]{3, 7, 2, 9, 1}); System.out.println("最大值:" + result[0] + ",最小值:" + result[1]);

有人会问,返回 int[] 会不会语义不清晰?确实,调用方得靠约定记住下标 0 是最大值、下标 1 是最小值,时间一长容易忘。进阶做法是定义一个结果类,Java 16 之后可以直接用 record:

public record MaxMin(int max, int min) { }

调用时MaxMin result = findMaxAndMin(nums); result.max()就非常直观了。我在实际教学中会先教返回数组的方式,因为简单直接,不需要额外定义类型;等理解了方法签名设计,再引入 record 或普通类,代码的自文档化程度立刻上一个层次。两种写法都要会,因为面试现场手写 record 可能受 JDK 版本限制,而返回数组是通用答案。

2.4 数组为 null 和长度为 0 是两个完全不同的情况

很多人会把arr == null和arr.length == 0混为一谈。前者是"引用指向空",你连数组对象都没有;后者是"有一个合法的空壳数组,里面一个元素没有"。访问arr.length时如果 arr 是 null,会直接抛 NullPointerException;对空数组取arr[0]则会抛 ArrayIndexOutOfBoundsException。两种异常都不该由调用方去猜,方法开头统一拦截,抛出信息明确的 IllegalArgumentException,这才是防御性编程。

我见过真实项目里因为省掉这段检查,导致线上日志全是 "at xxx.findMax(ArrayUtils.java:3)" 这样的报错,排查半天才发现是上游传了 null。方法开头写三行防御代码,看起来是"不信任调用方",实际是帮调用方提前暴露问题,省掉的是整个团队后期排查的时间。这个习惯从数组工具类开始养成,后面写任何公共方法都受用。

3. 原地逆序:不申请新数组也能完成反转

3.1 "原地"这个词到底在限制什么

第三题是"原地逆序数组并输出"。注意题面限定了"原地",也就是 in-place,意思是你只能在原数组上操作,不允许 new 一个等长的新数组来承接反转结果。为什么要这样限制?因为在真实系统里,数组可能非常庞大,比如几十万个元素甚至更大,你再复制一份,内存占用直接翻倍;如果能原地调整,空间复杂度从 O(n) 降到 O(1),对内存的节省是实打实的。

这个思想在排序算法里特别常见。快排、堆排之所以能处理海量数据,很大程度就是因为它们尽量在原地交换元素,而不是频繁申请新空间。反转数组只是最小的一个例子,理解了"原地操作"的价值,后面学算法的时候会更容易理解为什么有些操作要设计成交换而不是复制。

3.2 双指针交换:为什么循环只要走一半

原地逆序最经典的方案是双指针:一个指向数组开头,一个指向末尾,交换两个位置的值,然后左指针右移、右指针左移,直到两个指针相遇或交错。代码很短:

public static void reverseInPlace(int[] arr) { if (arr == null || arr.length <= 1) { 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--; } }

为什么循环条件是left < right而不是left <= right?因为当 left 等于 right 时,两个指针指向同一个元素,自己和自己交换毫无意义,纯属浪费一次操作。而 left 大于 right 说明已经交错,再交换就重复了。以长度为 5 的数组{1,2,3,4,5}为例走一遍:第一轮交换 1 和 5,数组变{5,2,3,4,1};第二轮交换 2 和 4,变{5,4,3,2,1};第三轮 left 和 right 都停在 3 上,条件不满足,循环结束。可以看到交换次数正好是 length / 2 的整数部分,时间复杂度 O(n),空间复杂度 O(1),这就是双指针法最优的地方。

新手容易犯的一个错是把交换的两个下标写反,比如arr[left] = arr[right]; arr[right] = arr[left];,这样第二个赋值其实是在用已经被覆盖的值,结果全是同一个数。所以交换三步走——存 temp、覆盖左边、用 temp 覆盖右边——顺序一步都不能乱。我在带新人的时候经常让他们在白板上用三个小纸片模拟这段交换,演示一遍比讲十遍都管用。

3.3 交换变量为什么老老实实用临时变量

交换两个变量的值,教材里永远用临时变量 temp 中转。有人会问:能不能用a = a + b; b = a - b; a = a - b这种算术技巧省掉临时变量?对于 int 类型确实可以,但存在整数溢出风险,而且代码可读性差;更不要提数组里存放的是引用类型时,算术交换根本没法用。临时变量在栈上占一个槽位,开销极小,换来的是清晰和安全,这笔账怎么算都划算。

我特意把这个细节拎出来,是因为实际面试里真有人为了"炫技"写算术交换,反而被面试官追问出溢出风险,得不偿失。工程代码的第一要求是可读可维护,不是看起来聪明。老老实实用 temp,是工程素养的体现,不是能力不足。

3.4 方法没有返回值,为什么调用方数组却被改了

题目里说"并输出",很多人就顺手在方法里打印数组。从封装角度讲,我建议把"逆序"和"输出"分开:方法只负责修改数组,输出由调用方决定。打印的时候用Arrays.toString(arr):

int[] nums = {1, 2, 3, 4, 5}; reverseInPlace(nums); System.out.println(Arrays.toString(nums)); // [5, 4, 3, 2, 1]

这里有个容易忽略的认知点:数组是引用类型,方法内部修改数组元素,会直接反映到调用方的数组上。Java 方法参数传递是值传递,但这个"值"对引用类型来说是引用的拷贝,它指向的还是同一个堆对象。所以 reverseInPlace 方法不需要返回值,因为原数组已经被改掉了。如果哪天真需要保留原数组,记得在调用前用System.arraycopy、Arrays.copyOf或clone备份一份。很多初学者在这里绕弯子,把这个点想通了,后面学集合、学对象传参都会顺畅很多。

4. 方法封装实践:把三道题串成一个小工具类

4.1 一个数组工具类的骨架该长什么样

文首提到的三个题目,本质上都是在训练方法的组织能力。既然每道题都涉及数组操作,完全可以合并成一个工具类,统一命名、统一风格:

import java.util.Arrays; public final class ArrayUtils { private ArrayUtils() { throw new AssertionError("工具类不允许实例化"); } public static int sum(int[] arr) { if (arr == null) { throw new IllegalArgumentException("数组不能为null"); } int total = 0; for (int num : arr) { total += num; } return total; } public static int[] findMaxAndMin(int[] arr) { // 实现同前文 } public static void reverseInPlace(int[] arr) { // 实现同前文 } }

工具类的类名用 Utils 结尾是 Java 社区的通行约定,JDK 自带的 Collections 类就是这个命名风格。构造方法私有化,防止别人 new 出无意义的实例;所有方法用 static 修饰,直接通过类名调用。这些细节看起来微不足道,但正是"工程化写法"和"课堂练习写法"的分水岭。等你学会编写、阅读这样的工具类,再看 Spring 里的 StringUtils、CollectionUtils,就能一眼明白它们的设计意图。

4.2 方法签名设计:四个问题一次想清楚

设计一个方法,本质是回答四个问题:这个方法叫什么、接收什么参数、返回什么结果、什么情况下抛异常。命名要动词开头,find、sum、reverse 都符合习惯;参数要尽量少,能只传一个数组就不要塞额外标志位;返回值要能承载完整语义,单个结果直接返回,多个结果用数组或结果对象;异常要主动处理,而不是让调用方去猜。

以 findMaxAndMin 为例,如果当初设计成findMax(int[] arr)和findMin(int[] arr)两个方法,调用方就得写两遍防御逻辑,遍历两遍数组;合并成一个方法后,接口数量和遍历次数都减半。如果有人图省事,设计成返回 void 然后在方法里打印结果,调用方拿到不任何数据,这个方法就等于废了。我在评审新人代码时,习惯先看方法签名再看方法体——签名乱了,方法体写得再漂亮也白搭。

4.3 方法级测试:不写 JUnit 也能验证正确性

很多人写方法不测试,或者说不知道怎么测。最简单的方式是在 main 里写几组调用,覆盖正常情况和边界情况:

public static void main(String[] args) { int[] normal = {3, 7, 2, 9, 1}; System.out.println(Arrays.toString(findMaxAndMin(normal))); // [9, 1] int[] single = {5}; System.out.println(Arrays.toString(findMaxAndMin(single))); // [5, 5] int[] negative = {-3, -7, -2}; System.out.println(Arrays.toString(findMaxAndMin(negative))); // [-2, -7] int[] empty = {}; try { findMaxAndMin(empty); } catch (IllegalArgumentException e) { System.out.println("空数组已拦截:" + e.getMessage()); } }

测试用例设计的原则,用三个词概括:正常、边界、异常。正常情况验证功能正确;边界情况验证单元素数组、全负数数组、最大最小值在两端;异常情况验证空数组和 null。这几组用例跑通了,方法基本就能放心用了。以后学到 JUnit,只是把这套手工验证自动化而已,思想完全一致。我面试候选人的时候,会先看对方会不会写这些验证用的 main,如果连测试意识都没有,代码质量基本可以打个问号。

5. 高频踩坑与排查技巧实录

5.1 常见报错和错误结果速查表

现象根本原因解决办法
数组全负数时最大值输出 0初始值设成了 0 而不是 arr[0]初始值取数组首元素
带负数的数组奇偶统计不对判断写成 num % 2 == 1,负数取模得 -1判断偶数分支或使用位运算
reverse 后打印结果没变打印时用了别的副本或没注意原数组被改确认传入的是同一个数组引用
访问 arr.length 抛空指针没检查 arr == null方法开头统一做防御判断
逆序结果元素重复交换两步赋值顺序写错严格按 temp 三步交换
普通 for 越界循环写成了 i <= arr.length习惯用增强 for 或 i < arr.length

这张表里的每一行,都是我在带新人和自己练习时真实遇到过的。尤其第一行,初学阶段中招概率极高,因为大家默认"最大值的初始值应该是 0",可数组元素未必都是正数。还有最后一行,下标越界是 Java 新人最常见的运行时异常,报错信息 ArrayIndexOutOfBoundsException 一出现,先检查循环边界,很多时候问题一秒就定位了。

5.2 面试官其实在考什么:基础题背后的四重考察

这类题目之所以常出现在面试中,是因为它们能在五分钟内暴露一个人的基础是否扎实。面试官看你写数组求和,实际在看有没有理解增强 for 和普通 for 的适用场景;看你写最大值查找,在看有没有考虑边界条件和防御性编程;看你写原地逆序,在看有没有空间复杂度意识、有没有掌握双指针思路。甚至连你输出的格式、变量命名的风格、注释写不写,都能反映出平时的编码习惯。

我面试过不少简历写得漂亮的候选人,一到白板写代码就露馅。不是不会,而是平时没有把基础题当工程题来练。我自己带人的时候,会刻意要求每写完一个方法后立刻问三个问题:边界都处理了吗?别人能看懂吗?有没有更省空间的写法?这三个问题想清楚了,代码质量和面试表现都会同步提升。准备面试的朋友,与其背一堆"八股文",不如先把这几类数组题闭卷写熟练。

5.3 几条沉淀下来的实操心得

第一,数组遍历优先用增强 for,只有在需要下标操作时才用普通 for,比如逆序交换这种场景。这不是强迫症,而是增强 for 杜绝了绝大多数下标越界问题,代码意图也更清晰。

第二,凡是涉及数组的工具方法,开头三行永远先做防御检查:null 判断、空数组判断、长度合理性判断。这三行写起来烦,但能把后期调试成本压到最低。真实项目里,数组工具往往是公共方法,调用方来自各个业务模块,你永远不知道谁会传一个 null 进来。

第三,遇到"返回多个结果"的需求,优先提供返回数组或结果对象的方法,不要用全局变量偷偷传递。全局变量会让方法之间产生隐藏的耦合,排查问题的时候特别痛苦。我年轻时候干过这种事,一个静态变量被两个线程同时改,线上数据错得莫名其妙,后来排查了两天才定位到根因,从那以后就再也不用这种方式了。

第四,多动手敲一遍比看十遍都管用。这三个题目看起来简单,但亲手写一遍、再改写成不同写法——普通 for、增强 for、Stream、递归——再给自己讲一遍为什么,收获完全不一样。把这三道题吃透,后面学排序、学集合、学泛型,都会有一个扎实的底子。

最后说点题外的。这三个题我在不同场合出现过很多次:给新人培训的时候用它,面试候选人的时候用它,自己复盘基础的时候也用它。每次写都觉得"不过如此",可每次细看总能发现一点新东西——比如负数取模的符号规则,比如 record 类型简化返回值对象,又比如 JDK 9 之后 Arrays 工具类里新增的那些方法。基础题不是做一遍就完事的,隔段时间回来重写一次,往往能照见自己的进步。如果你也在学 Java,建议把这三道题存进自己的练习清单,每隔几周闭卷重写一遍,看看能不能一次写得又对又干净。能,就说明你真的扎下根了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询