Apache Commons Lang3 ArrayUtils 实用指南

告别手写数组循环: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 解决了排序、二分查找、copyOftoString,但没有 contains、没有 remove、连 reverse 都没有 (想反转得先转成 List 再调 Collections.reverse,还得处理装箱拆箱)。

ArrayUtils 正是补上这块空缺的工具类。官方 Javadoc 对它的定位说得很直接:

"Operations on arrays, primitive arrays (like int[]) and primitive wrapper arrays (like Integer[]). This class tries to handle null input gracefully. An exception will not be thrown for a null array 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;

⚠️ 两个包名陷阱:

  1. org.apache.commons.lang.ArrayUtils3 )属于 Commons Lang 2.x,自 2011 年起停止维护,新项目一律用 lang3
  2. 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[](自动拆箱)

典型场景:把基本类型数组交给只接受 ListObject[] 的 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 等少数几个是原地的

addaddAllinsertremoveremoveElementsubarraycloneconcat 全部返回新数组,忘记接收返回值等于什么都没做

java 复制代码
ArrayUtils.add(arr, 4);            // ❌ 无效,返回值被丢弃
arr = ArrayUtils.add(arr, 4);      // ✅ 正确

而原地修改(返回 void)的只有这几个:reverseshuffleswapshift。这个不一致性需要在 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)truecontains(null, x)falsereverse(null) → 空操作),但:

  • toPrimitive 对数组内部的 null 元素 抛 NPE(用带 valueForNull 的重载解决);
  • add(null, null)IllegalArgumentException
  • addAll(null, null) 返回 null
  • insert(0, null, x) 返回 null

"入参 null 安全"不等于"元素 null 安全",也不等于"所有方法行为一致"。

坑 5:性能------不要在循环里 add / remove

每次 addremove 都会创建一个新数组并做一次全量拷贝,时间复杂度 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 的定位很清晰:让数组操作回到一行代码的粒度

三条使用原则:

  1. 优先用它替代手写的判空、查找、拷贝循环,可读性提升明显,且天然 null 安全。
  2. 时刻区分"返回新数组"和"原地修改" ,前者必须接收返回值,后者只有 reverseshuffleswapshift
  3. 它不是集合的替代品 。涉及频繁增删的场景,List / Map 才是正确答案,ArrayUtils 适合处理那些"进来就是数组、出去还是数组"的接口边界。

如果你正在维护还在手写 System.arraycopy 的老代码,把它们替换成 ArrayUtils 通常是一次低风险、高收益的重构------只要留意上面 5 个坑,尤其是 remove / removeElement 的语义差异和 add 三参数版本的废弃。


参考Apache Commons Lang3 ArrayUtils 官方 API 文档Maven Coordinates

相关推荐
小宇宙清歡渡1 天前
Apache Commons Lang3 BooleanUtils 实用指南
apache commons·booleanutils
小宇宙清歡渡13 天前
Apache Commons 完全指南:ObjectUtils、ArrayUtils、BooleanUtils 的用法
apache commons·objectutils·arrayutils·booleanutils