反转 Java 数组最常用的思路是首尾对应交换,但真正动手写时,循环条件经常被写成 i 小于等于 len 除以 2,或者在双指针循环里继续让左右指针交错。这类问题不会报异常,却会让数组被交换两次,结果又变回原样。很多单元测试只覆盖了三个元素以下的情况,偶数长度或空数组往往被漏掉。本文围绕原地反转的交换规则,把边界计算、重复交换成因,以及基本类型数组与对象数组的不同处理方式讲清楚。

先从一个正确实现开始。对于长度为 len 的数组,只需要交换前 len 除以 2 个元素与对应的后半部分元素。比如长度为 6 的数组,交换索引 0 和 5、1 和 4、2 和 3;长度为 7 的数组,交换 0 和 6、1 和 5、2 和 4,索引 3 天然居中,不需要移动。
索引折半写法为什么容易写错
直接看一个最常见但存在问题的实现。有人会把循环条件写成 i 小于等于 len 除以 2,并且交换 arr[i] 与 arr[len - 1 - i]。当数组长度为奇数时,最中间的元素会和自己交换一次,这虽然没有数据错误,但属于多余操作。真正严重的是偶数长度,例如长度 4 的数组,len 除以 2 等于 2。第一次 i 等于 0 交换索引 0 和 3,第二次 i 等于 1 交换索引 1 和 2,此时数组已经完成反转。如果条件允许 i 等于 2 继续执行,就会交换索引 2 和 1,把刚刚换好的两个元素又换回去,相当于多执行了一轮对称位置交换,最终数组恢复原顺序。
正确的折半写法只需要让 i 严格小于 len 除以 2。下面的代码可以直接用于 int 数组。
public static void reverse(int[] arr) {
int len = arr.length;
for (int i = 0; i < len / 2; i++) {
int temp = arr[i];
arr[i] = arr[len - 1 - i];
arr[len - 1 - i] = temp;
}
}
这个实现的时间复杂度是 O(n),空间复杂度是 O(1)。它把交换次数固定为数组长度的一半,奇数长度时中间元素不参与交换。很多开发者看到 len 除以 2 会下意识觉得要加上等号来覆盖中间位置,其实中间位置一旦被包含,恰恰是重复交换的来源。
双指针写法更直观,也不容易越界
如果不想去记折半上界,更推荐双指针。一个指针从左向右移动,另一个从右向左移动,只要左指针还小于右指针,就交换两个位置的元素。判断条件非常直观:当两个指针相遇或错过时,数组就已经反转完成,不需要再计算中间索引。
public static void reverse(int[] arr) {
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 相等时只是让最中间的元素与自身交换,它不会破坏数组,但也不是干净的做法。真正需要警惕的是把两个指针的终止条件与移动顺序写反,例如先移动指针再判断、或者写一个固定次数的循环从 0 到数组长度减 1。固定次数循环会让索引 i 和 len - 1 - i 完整走完整个区间,当 i 进入后半段后,交换对象就是前半段已经处理过的位置,结果必然还原。双指针通过 left 和 right 的移动把交换范围逐步向中间收缩,每次循环都缩小未处理区间,因此只要条件不写成 left 小于等于 right 并在相等时还继续移动,通常不会出现折半写法那种多交换一轮的问题。
双指针写法还有一个好处:它能很自然地处理数组长度为 0 或 1 的情况。空数组的 right 初始为 -1,while 条件一开始就不成立;单元素数组的 left 和 right 都为 0,条件也不成立。折半写法对于这些边界也能正确返回,但双指针不需要推导 len 除以 2 的取整行为,阅读代码时更容易确认正确性。
基本类型数组和对象数组要分开处理
String 数组可以直接复用上面的双指针逻辑,因为交换的临时变量类型从 int 改成 String 即可。但不少开发者会想到 JDK 自带的 Collections.reverse 方法。这个方法接收 List 参数,并不是数组。于是有人用 Arrays.asList 把数组转成 List 再反转。对于 String[] 来说,这样做结果会反映到原数组,因为 Arrays.asList 返回的列表底层就是传入的数组,反转列表等价于修改数组中的元素顺序。
String[] arr = {"a", "b", "c", "d"};
List<String> list = Arrays.asList(arr);
Collections.reverse(list);
// 此时 arr 变为 {"d", "c", "b", "a"}
但是这个方法对基本类型数组完全无效,甚至会产生令人困惑的编译结果。int[] 本身是一个引用类型对象,Arrays.asList 接收可变参数 T...,当传入 int[] 时,编译器会把它当作一个整体元素,得到的是 List<int[]> 而不是 List<Integer>。也就是说列表里只有一个元素,这个元素是原来的整个 int 数组,反转自然没有任何意义。
如果一定要用 Collections.reverse 处理 int 数组,需要先把 int[] 装箱为 Integer[],再做反转。示例代码如下。
int[] arr = {1, 2, 3, 4, 5};
Integer[] boxed = Arrays.stream(arr).boxed().toArray(Integer[]::new);
Collections.reverse(Arrays.asList(boxed));
// boxed 的元素已经是 5,4,3,2,1,可以再转回 int[] 或直接使用
不过这种写法会引入装箱和拆箱开销,还额外分配一个 Integer 数组。如果只是反转一个普通 int 数组,性能远不如手动原地交换。泛型数组反转可以用下面这种方式,让同一套逻辑服务于 String[]、Integer[] 等引用类型数组。
public static <T> void reverse(T[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
T temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
这个泛型方法无法接收 int[],因为 Java 泛型不支持基本类型作为类型参数。基本类型数组必须单独写方法,这是 Java 类型系统的限制,不是代码缺陷。实际项目中,如果确定要频繁反转基本类型数组,建议直接写一个 int[] 版本和一个 long[] 版本,不要绕道泛型或流 API。
常见错误检查清单
把前面提到的坑集中起来,可以帮助在代码评审时快速判断反转逻辑是否可靠。第一个错误是循环上界使用 arr.length 或 i 小于等于 arr.length 减 1。这样会让每个位置都被交换两次,最终数组顺序不变。第二个错误是交换时没有临时变量保存值,例如先写 arr[i] = arr[last],再写 arr[last] = arr[i],此时 arr[i] 已经是原 arr[last] 的值,后续赋值会丢失数据。第三个错误是空数组处理不当,比如直接访问 arr[0] 或者 arr[arr.length - 1],在长度为 0 时抛出 ArrayIndexOutOfBoundsException。
- 循环上界写成 arr.length,导致两轮交换后数组还原。
- 临时变量缺失,导致首尾值互相覆盖。
- 对 int[] 使用 Arrays.asList,得到 List<int[]> 而不是元素列表。
- 双指针条件写成 left 小于等于 right,会让中间元素执行一次无意义的自身交换。
测试反转方法时,建议至少覆盖奇数长度、偶数长度、空数组、单元素数组和两个元素数组这五种情况。奇数长度测试中间元素是否保持不变,偶数长度测试是否能正确反转且没有重复交换。两个元素的数组最容易暴露临时变量缺失和条件写错的问题。
assertArrayEquals(new int[]{5, 4, 3, 2, 1}, reverse(new int[]{1, 2, 3, 4, 5}));
assertArrayEquals(new int[]{4, 3, 2, 1}, reverse(new int[]{1, 2, 3, 4}));
assertArrayEquals(new int[]{}, reverse(new int[]{}));
assertArrayEquals(new int[]{1}, reverse(new int[]{1}));
assertArrayEquals(new int[]{2, 1}, reverse(new int[]{1, 2}));
双指针版本的 reverse 方法可以直接跑这组断言。如果项目中还需要反转 List,可以直接使用 Collections.reverse,它会修改传入的列表,底层通过交换元素实现,不需要自己再写循环。但对于数组尤其是基本类型数组,手动双指针仍然是最简单、性能最好的方案。