告别手写数组循环:Apache Commons Lang3 ArrayUtils 实用指南
本文基于
org.apache.commons.lang3.ArrayUtils(Commons Lang3 3.20.0),覆盖判空、查找、增删、反转、装箱拆箱、截取等高频操作,并整理出 5 个最容易踩的坑。文中所有 API 行为均核对过官方 Javadoc。
一、为什么需要 ArrayUtils
Java 数组是定长的、"裸"的数据结构。日常开发里我们反复在写这些样板代码:
java
// 判空要判两次
if (arr != null && arr.length > 0) { ... }
// 查找元素要手写循环
boolean found = false;
for (int i : arr) {
if (i == 2) { found = true; break; }
}
// 删除元素要新建数组 + System.arraycopy
int[] result = new int[arr.length - 1];
System.arraycopy(arr, 0, result, 0, index);
System.arraycopy(arr, index + 1, result, index, arr.length - index - 1);
JDK 自带的 java.util.Arrays 解决了排序、二分查找、copyOf、toString,但没有 contains、没有 remove、连 reverse 都没有 (想反转得先转成 List 再调 Collections.reverse,还得处理装箱拆箱)。
ArrayUtils 正是补上这块空缺的工具类。官方 Javadoc 对它的定位说得很直接:
"Operations on arrays, primitive arrays (like
int[]) and primitive wrapper arrays (likeInteger[]). This class tries to handlenullinput gracefully. An exception will not be thrown for anullarray input."
三个特点:全部静态方法、几乎全部 null 安全、每种基本类型都有独立重载 (int/long/double/boolean/byte/char/short/float,外加泛型 T[])。
二、引入依赖
Maven:
xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
Gradle:
groovy
implementation 'org.apache.commons:commons-lang3:3.20.0'
导入:
java
import org.apache.commons.lang3.ArrayUtils;
⚠️ 两个包名陷阱:
org.apache.commons.lang.ArrayUtils(无3)属于 Commons Lang 2.x,自 2011 年起停止维护,新项目一律用lang3。- Lang3 从 3.7 起要求 Java 7+,3.12+ 支持到 Java 17+,升级前确认项目 JDK 版本。
三、方法逐个拆解
下面所有示例基于同一个数组:
java
int[] arr = {1, 2, 3};
3.1 判空:isEmpty / isNotEmpty
java
ArrayUtils.isEmpty(arr); // false
ArrayUtils.isEmpty(new int[0]); // true
ArrayUtils.isEmpty(null); // true ------ 传 null 不会 NPE
ArrayUtils.isNotEmpty(arr); // true
使用频率最高的一个方法。它把 arr == null || arr.length == 0 收敛成一次调用,参数为 null 时返回 true 而不是抛异常,比手写判断更安全。
配套的 getLength 同样 null 安全:
java
ArrayUtils.getLength(null); // 0
ArrayUtils.getLength(arr); // 3
3.2 查找:contains / indexOf / lastIndexOf
java
ArrayUtils.contains(arr, 2); // true
ArrayUtils.contains(arr, 9); // false
ArrayUtils.contains(null, 2); // false ------ null 安全
ArrayUtils.indexOf(arr, 2); // 1(返回下标)
ArrayUtils.indexOf(arr, 9); // -1,即 ArrayUtils.INDEX_NOT_FOUND
ArrayUtils.lastIndexOf(arr, 2); // 1
indexOf 找不到时返回常量 ArrayUtils.INDEX_NOT_FOUND(值为 -1),与 String.indexOf 语义一致。它支持指定起始位置:
java
int[] dup = {1, 2, 2, 3};
ArrayUtils.indexOf(dup, 2, 2); // 2 ------ 从下标 2 开始往后找
浮点数场景请用带容差的重载,避免精度问题:
java
double[] ds = {1.0, 2.0};
ArrayUtils.contains(ds, 2.0, 0.0001); // true
ArrayUtils.indexOf(ds, 2.0, 0.0001); // 1
如果想一次拿到所有 匹配下标,用 indexesOf(返回 BitSet):
java
BitSet hits = ArrayUtils.indexesOf(dup, 2); // {1, 2}
判断"是否包含其中任意一个"用 containsAny:
java
ArrayUtils.containsAny(dup, 9, 3); // true ------ 注意只提供 int[] 和 Object[] 两种重载
3.3 新增:add / addAll
java
int[] a = ArrayUtils.add(arr, 4);
// a = {1, 2, 3, 4},arr 本身不变
int[] b = ArrayUtils.addAll(arr, new int[]{4, 5});
// b = {1, 2, 3, 4, 5}
int[] c = ArrayUtils.add(null, 4);
// c = {4} ------ 对 null 数组调用是合法的
官方 Javadoc 的行为契约:
ArrayUtils.add(null, 0) = [0]
ArrayUtils.add([1], 0) = [1, 0]
ArrayUtils.add([1, 0], 1) = [1, 0, 1]
关键点 1:add / addAll 都返回新数组,不修改原数组。 因为 Java 数组长度固定,任何"变长"操作底层都是一次 System.arraycopy 拷贝。
关键点 2:泛型版 add(T[], T) 在两个参数都是 null 时抛 IllegalArgumentException ------因为此时无法推断数组的组件类型:
java
ArrayUtils.add(null, null); // ❌ IllegalArgumentException
ArrayUtils.add(null, "a"); // ✅ ["a"],类型推断为 String[]
关键点 3(易踩):三参数的 add(array, index, element) 已被废弃 ,官方要求改用 insert:
java
// ❌ 已 @Deprecated,未来版本可能移除
ArrayUtils.add(arr, 1, 99);
// ✅ 正确写法:insert 的 index 是第一个参数
ArrayUtils.insert(1, arr, 99); // {1, 99, 2, 3}
注意两者对 null 数组的处理恰好相反,这是官方在废弃说明里专门强调的:
java
ArrayUtils.add(null, 0, true); // [true] ------ 旧方法:返回单元素数组
ArrayUtils.insert(0, null, true); // null ------ 新方法:返回 null
另外 3.10 起新增了 addFirst,往头部插元素不必再用 insert(它内部就是 array == null ? add(...) : insert(0, ...),因此对 null 数组返回单元素数组,与直接调 insert 不同):
java
ArrayUtils.addFirst(arr, 0); // {0, 1, 2, 3}
ArrayUtils.addFirst(null, 1); // {1} ← 注意:这里不是 null
拼接多个数组时,新版本提供了 concat,比反复 addAll 清晰得多:
java
ArrayUtils.concat(new int[]{1}, new int[]{2}, new int[]{3}); // {1, 2, 3}
⚠️
concat标注为@since 3.21.0,目前 3.21.0 尚未正式发布。如果你用的是 3.20.0 及更早版本,这个方法还不存在 ,请继续用addAll,或用 Stream 拼接。
addAll 的 null 行为也值得记一下:
ArrayUtils.addAll(array1, null) = array1 的克隆副本
ArrayUtils.addAll(null, array2) = array2 的克隆副本
ArrayUtils.addAll(null, null) = null
3.4 删除:remove / removeElement / removeAll
这是 ArrayUtils 里最容易写错的一组方法,务必区分"按索引"和"按值":
java
ArrayUtils.remove(arr, 1); // 按下标删除 → {1, 3}
ArrayUtils.removeElement(arr, 2); // 按值删除第一个 2 → {1, 3}
上面两行结果一样纯属巧合(下标 1 上的值正好是 2)。换个数组立刻见分晓:
java
int[] arr2 = {10, 20, 30};
ArrayUtils.remove(arr2, 1); // {10, 30} ← 删掉了 20
ArrayUtils.removeElement(arr2, 1); // {10, 20, 30} ← 没有值为 1 的元素,原样返回
批量删除:
java
int[] dup = {1, 2, 2, 3};
ArrayUtils.removeAll(dup, 0, 2); // 按下标删多个 → {2, 3}
ArrayUtils.removeAllOccurrences(dup, 2); // 按值删全部 → {1, 3}
ArrayUtils.removeElements(dup, 2, 2); // 按指定次数删 → {1, 3}
拼写提醒:
removeAllOccurences(少一个r)是旧版的拼写错误方法,已@Deprecated,请使用removeAllOccurrences。
同样地,所有删除方法都返回新数组。两者的异常行为也不同:
remove(array, index):下标越界或数组为null时抛IndexOutOfBoundsException(官方 Javadoc 明确写了这两种情况);removeElement(array, element):找不到值时静默返回clone(array),即原数组的副本,不抛异常。
源码可以直接印证这个差异:
java
public static int[] removeElement(final int[] array, final int element) {
final int index = indexOf(array, element);
return index == INDEX_NOT_FOUND ? clone(array) : remove(array, index);
}
3.5 反转:reverse
java
int[] arr = {1, 2, 3};
ArrayUtils.reverse(arr);
System.out.println(Arrays.toString(arr)); // [3, 2, 1]
注意:reverse 是原地修改(in-place),直接改变传入的数组,返回值是 void。 这是它与前面所有方法最大的行为差异。传 null 进去是安全的空操作。
它还支持只反转指定区间,语义同样是含头不含尾:
java
int[] arr = {1, 2, 3, 4, 5};
ArrayUtils.reverse(arr, 1, 4); // {1, 4, 3, 2, 5} ------ 只反转下标 [1,4)
3.6 装箱与拆箱:toObject / toPrimitive
基本类型数组和包装类数组在 Java 中是完全不同的类型 ,int[] 不能直接赋值给 Integer[]。这两个方法负责互相转换:
java
int[] arr = {1, 2, 3};
Integer[] boxed = ArrayUtils.toObject(arr);
// boxed = {1, 2, 3},int[] → Integer[](自动装箱)
int[] back = ArrayUtils.toPrimitive(boxed);
// back = {1, 2, 3},Integer[] → int[](自动拆箱)
典型场景:把基本类型数组交给只接受 List 或 Object[] 的 API。
java
List<Integer> list = Arrays.asList(ArrayUtils.toObject(arr));
坑点:单参数 toPrimitive 遇到 null 元素会抛 NullPointerException (拆箱 null 本身就不合法)。这正是官方 Javadoc 那句 "an Object array that contains a null element may throw an exception" 所指的情况。
好消息是不需要手写 Stream 过滤 ,官方提供了带默认值的重载 toPrimitive(array, valueForNull),直接把 null 元素替换成你指定的值:
java
Integer[] dirty = {1, null, 3};
ArrayUtils.toPrimitive(dirty); // ❌ NullPointerException
ArrayUtils.toPrimitive(dirty, 0); // ✅ {1, 0, 3} ------ null 被替换为 0
两个方法的数组本身 为 null 时都是安全的:toObject(null) 和 toPrimitive(null) 都返回 null。
ArrayUtils 为全部 8 种基本类型都提供了这组方法,且每种都有 valueForNull 重载。
3.7 截取:subarray
java
int[] arr = {1, 2, 3};
ArrayUtils.subarray(arr, 0, 2); // {1, 2}
语义与 String.substring 一致:含头不含尾 ,即 [startIndexInclusive, endIndexExclusive)。
它最大的优点是越界安全 ,比 Arrays.copyOfRange 宽容得多:负起点按 0 处理,超过数组长度的终点自动截断,起点不小于终点则返回空数组:
java
int[] arr = {1, 2, 3};
ArrayUtils.subarray(arr, 1, 100); // {2, 3} ------ 自动截断到末尾
ArrayUtils.subarray(arr, -5, 2); // {1, 2} ------ 负起点归零
ArrayUtils.subarray(arr, 2, 1); // {} ------ 起点 > 终点,返回空数组
Arrays.copyOfRange(arr, 1, 100); // ❌ 抛 ArrayIndexOutOfBoundsException
subarray(null, 0, 2) 返回 null。正常情况下返回的是新数组(浅拷贝),修改它不影响原数组的元素引用。
一个冷知识:当计算出的区间长度为 0 时,返回的是类内部的共享常量
EMPTY_INT_ARRAY(各类型都有对应常量),而不是新建的空数组。所以不要指望"每次调用都拿到一个全新的空数组",更不要去修改它的元素。
3.8 泛型数组同样适用
上述所有方法都有对应的泛型重载,对象数组用起来完全一致:
java
String[] strs = ArrayUtils.toArray("a", "b"); // {"a", "b"}
String[] s2 = ArrayUtils.add(strs, "c"); // {"a", "b", "c"}
ArrayUtils.contains(strs, "a"); // true
ArrayUtils.reverse(strs); // 原地 → {"b", "a"}
ArrayUtils.subarray(strs, 0, 1); // {"b"}
toArray 的价值在于省去显式声明数组类型(new String[]{"a", "b"}),作为方法实参时尤其方便。它依靠可变参数自动推断元素类型------同样地,不要传 null 作为唯一参数,会导致类型无法推断。
3.9 完整可运行示例
java
import org.apache.commons.lang3.ArrayUtils;
import java.util.Arrays;
public class ArrayUtilsDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(ArrayUtils.isEmpty(arr)); // false
System.out.println(ArrayUtils.contains(arr, 2)); // true
System.out.println(ArrayUtils.indexOf(arr, 2)); // 1
System.out.println(Arrays.toString(ArrayUtils.add(arr, 4))); // [1, 2, 3, 4]
System.out.println(Arrays.toString(ArrayUtils.addAll(arr, new int[]{4, 5}))); // [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(ArrayUtils.insert(1, arr, 99))); // [1, 99, 2, 3]
System.out.println(Arrays.toString(ArrayUtils.remove(arr, 1))); // [1, 3]
System.out.println(Arrays.toString(ArrayUtils.subarray(arr, 0, 2))); // [1, 2]
// 以上调用都没有改变 arr
System.out.println(Arrays.toString(arr)); // [1, 2, 3]
// 只有 reverse 是原地修改
ArrayUtils.reverse(arr);
System.out.println(Arrays.toString(arr)); // [3, 2, 1]
// 装箱 / 拆箱
Integer[] boxed = ArrayUtils.toObject(arr);
System.out.println(Arrays.toString(boxed)); // [3, 2, 1]
System.out.println(Arrays.toString(ArrayUtils.toPrimitive(boxed))); // [3, 2, 1]
// 泛型数组
String[] strs = ArrayUtils.toArray("a", "b");
System.out.println(Arrays.toString(strs)); // [a, b]
}
}
四、五个必须知道的坑
坑 1:remove(index) 与 removeElement(value) 不是一回事
这是最高频的 bug 来源。remove 的第二个参数是下标 ,removeElement 才是值 。想按值删除却写成 remove,会静默删掉一个完全无关的元素------不报错,只是结果不对,非常难排查。
坑 2:绝大多数方法返回新数组,只有 reverse 等少数几个是原地的
add、addAll、insert、remove、removeElement、subarray、clone、concat 全部返回新数组,忘记接收返回值等于什么都没做:
java
ArrayUtils.add(arr, 4); // ❌ 无效,返回值被丢弃
arr = ArrayUtils.add(arr, 4); // ✅ 正确
而原地修改(返回 void)的只有这几个:reverse、shuffle、swap、shift。这个不一致性需要在 code review 时留意。
坑 3:add(array, index, element) 已废弃,且与 insert 的 null 行为相反
老博客和 Stack Overflow 上大量示例还在用三参数 add,它已被 @Deprecated 并可能被移除。改用 insert(index, array, values) 时注意参数顺序变了(index 移到第一位),而且往 null 数组里 insert 会返回 null 而不是单元素数组------迁移老代码时这是个隐蔽的行为变化。
坑 4:null 处理策略并不完全统一
大部分方法对 null 入参 是宽容的(isEmpty(null) → true,contains(null, x) → false,reverse(null) → 空操作),但:
toPrimitive对数组内部的 null 元素 抛 NPE(用带valueForNull的重载解决);add(null, null)抛IllegalArgumentException;addAll(null, null)返回null;insert(0, null, x)返回null。
"入参 null 安全"不等于"元素 null 安全",也不等于"所有方法行为一致"。
坑 5:性能------不要在循环里 add / remove
每次 add、remove 都会创建一个新数组并做一次全量拷贝,时间复杂度 O(n)。在循环中反复调用,整体退化为 O(n²),并产生大量临时对象加重 GC 压力:
java
// ❌ 反例:n 次循环 = n 次数组拷贝
int[] result = new int[0];
for (int i = 0; i < 10000; i++) {
result = ArrayUtils.add(result, i);
}
// ✅ 正确做法:用 List 收集,最后一次性转换
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(i);
}
int[] result = ArrayUtils.toPrimitive(list.toArray(new Integer[0]));
经验法则:数据量小、操作次数少时用 ArrayUtils,代码更清爽;需要频繁增删就直接用 List。
五、其他值得一用的方法
| 方法 | 作用 | 备注 |
|---|---|---|
nullToEmpty(arr) |
null 转为空数组,方便后续无脑遍历 |
有各基本类型重载 |
clone(arr) |
浅拷贝一份新数组 | null 返回 null |
toString(arr) |
打印内容而非内存地址 | null 返回 "" |
get(arr, index) |
越界或 null 时返回 null |
还有带默认值的重载 |
getLength(arr) |
null 安全地取长度 | 返回 0 |
isSameLength(a, b) |
判断两数组长度是否相等 | null 视为长度 0 |
isSorted(arr) |
判断是否已升序排列 | 对象数组支持 Comparator |
shuffle(arr) |
原地随机打乱 | Fisher-Yates,可传 Random |
swap(arr, i, j) |
原地交换两个下标的元素 | 有带 len 的批量版 |
shift(arr, offset) |
原地循环移位 | 类似"轮转数组" |
addFirst(arr, e) |
在头部插入元素 | 3.10+ 新增 |
concat(arrays...) |
拼接多个数组 | 3.21.0+,3.20.0 尚不可用 |
arraycopy(...) |
链式版 System.arraycopy,返回目标数组 |
3.15.0+ 新增 |
indexesOf(arr, v) |
找出全部匹配下标 | 返回 BitSet |
containsAny(arr, vs...) |
是否含任一元素 | 仅 int[] 与 Object[] |
isEmpty(arr) / isNotEmpty(arr) |
判空 | null 安全 |
toMap(array) |
把 {key, value} 对数组转成 Map |
用于二维数组 |
isSameType(a, b) |
判断类型是否相同 | 考虑多维数组 |
EMPTY_INT_ARRAY 等常量 |
预置的空不可变数组 | 避免反复 new int[0] |
顺带一提,isEquals 已废弃,官方建议直接用 JDK 的 Objects.deepEquals。
六、速查表
java
// 以下每一行的注释都基于初始状态 arr = {1, 2, 3},彼此独立、互不影响
int[] arr = {1, 2, 3};
// ===== 判空 =====
ArrayUtils.isEmpty(arr); // false
ArrayUtils.isNotEmpty(arr); // true
ArrayUtils.getLength(arr); // 3(null 安全)
// ===== 查找 =====
ArrayUtils.contains(arr, 2); // true
ArrayUtils.indexOf(arr, 2); // 1(找不到返回 -1)
ArrayUtils.lastIndexOf(arr, 2); // 1
ArrayUtils.indexesOf(arr, 2); // BitSet{1}(全部匹配下标)
// ===== 新增(返回新数组)=====
ArrayUtils.add(arr, 4); // {1, 2, 3, 4}
ArrayUtils.addFirst(arr, 0); // {0, 1, 2, 3}(3.10+)
ArrayUtils.addAll(arr, new int[]{4, 5}); // {1, 2, 3, 4, 5}
ArrayUtils.insert(1, arr, 99); // {1, 99, 2, 3}(index 在第一位)
// ArrayUtils.concat(arr, arr); // 3.21.0+ 才有,见上文说明
// ===== 删除(返回新数组)=====
ArrayUtils.remove(arr, 1); // 按索引 → {1, 3}
ArrayUtils.removeElement(arr, 2); // 按值 → {1, 3}
ArrayUtils.removeAll(arr, 0, 2); // 按多个索引 → {2}
ArrayUtils.removeAllOccurrences(arr, 2); // 删全部匹配 → {1, 3}
// ===== 原地修改(返回 void,直接改 arr)=====
ArrayUtils.reverse(arr); // {3, 2, 1}
ArrayUtils.reverse(arr, 0, 2); // 只反转 [0,2) → {2, 1, 3}
// ===== 类型转换 =====
ArrayUtils.toObject(arr); // int[] → Integer[](装箱)
ArrayUtils.toPrimitive(new Integer[]{1, 2}); // Integer[] → int[](拆箱,元素不可为 null)
ArrayUtils.toPrimitive(new Integer[]{1, null}, 0); // {1, 0} ------ null 用默认值兜底
// ===== 截取(含头不含尾,越界安全)=====
ArrayUtils.subarray(arr, 0, 2); // {1, 2}
// ===== 泛型数组同样适用 =====
String[] strs = ArrayUtils.toArray("a", "b"); // {"a", "b"}
七、小结
ArrayUtils 的定位很清晰:让数组操作回到一行代码的粒度。
三条使用原则:
- 优先用它替代手写的判空、查找、拷贝循环,可读性提升明显,且天然 null 安全。
- 时刻区分"返回新数组"和"原地修改" ,前者必须接收返回值,后者只有
reverse、shuffle、swap、shift。 - 它不是集合的替代品 。涉及频繁增删的场景,
List/Map才是正确答案,ArrayUtils适合处理那些"进来就是数组、出去还是数组"的接口边界。
如果你正在维护还在手写 System.arraycopy 的老代码,把它们替换成 ArrayUtils 通常是一次低风险、高收益的重构------只要留意上面 5 个坑,尤其是 remove / removeElement 的语义差异和 add 三参数版本的废弃。
参考 :Apache Commons Lang3 ArrayUtils 官方 API 文档 | Maven Coordinates