Arrays 工具类
白歌:"
java.util.Arrays是 JDK 提供给数组操作的核心工具类。我在 Code Review 中经常发现同事自己写排序、自己写打印方法,却不知道Arrays里已经内置了高效、经过充分测试的实现。这一节我们系统学习它的核心 API——重点不是记忆方法名,而是理解其适用场景和潜在陷阱。"
概述
java.util.Arrays 是 JDK 1.2 引入的数组工具类,位于 java.util 包下。它提供了一系列静态方法,涵盖了数组的排序、搜索、比较、填充、复制、转换等常见操作。
| 方法类别 | 核心方法 | 功能概述 |
|---|---|---|
| 打印 | toString() / deepToString() | 将数组转为可读字符串 |
| 排序 | sort() / parallelSort() | 对数组元素排序 |
| 查找 | binarySearch() | 二分查找指定元素 |
| 比较 | equals() / deepEquals() | 比较两个数组是否相等 |
| 填充 | fill() | 用指定值填充数组 |
| 复制 | copyOf() / copyOfRange() | 复制数组(支持扩容/截断) |
| 转换 | asList() | 将数组转换为 List(有陷阱) |
设计哲学:
Arrays的所有方法都是public static(工具类模式),构造函数为private,防止实例化。
Arrays.toString():数组转字符串
在 Arrays.toString() 出现之前,直接用 System.out.println(arr) 打印数组会得到一个看不懂的字符串(如 [I@15db9742——这是数组对象的 hashCode 十六进制表示)。
// ❌ 旧方法:直接打印数组
int[] arr = {1, 2, 3};
System.out.println(arr); // 输出:[I@15db9742 —— 完全不可读
// ✅ 使用 Arrays.toString()
System.out.println(Arrays.toString(arr)); // 输出:[1, 2, 3]
| 方法 | 适用维度 | 示例输出 |
|---|---|---|
Arrays.toString(arr) | 一维数组 | [1, 2, 3] |
Arrays.deepToString(arr) | 多维数组 | [[1, 2], [3, 4]] |
注意:对多维数组使用
toString()仍然会输出[[I@xxx这样的引用地址——必须用deepToString()。
Arrays.sort():数组排序
Arrays.sort() 使用双轴快速排序(Dual-Pivot Quicksort),JDK 7 起由 Vladimir Yaroslavskiy 实现,平均时间复杂度 O(n log n)。
// 对基本类型数组排序(升序,自然顺序)
int[] scores = {85, 92, 78, 95, 88};
Arrays.sort(scores);
System.out.println(Arrays.toString(scores)); // [78, 85, 88, 92, 95]
// 对部分区间排序(fromIndex 包含,toIndex 不包含)
int[] scores2 = {85, 92, 78, 95, 88};
Arrays.sort(scores2, 1, 4); // 只排序下标 1~3 的元素
System.out.println(Arrays.toString(scores2)); // [85, 78, 92, 95, 88]
自定义排序:Comparator
对于对象数组(如 String[]、自定义类数组),可使用 Comparator 自定义排序规则:
// 对字符串数组按长度排序
String[] names = {"大翔", "白歌", "小崔", "孔蓝"};
Arrays.sort(names, (a, b) -> a.length() - b.length());
System.out.println(Arrays.toString(names)); // [大翔, 白歌, 小崔, 孔蓝]
// 降序排序(使用 Comparator.reverseOrder())
Integer[] scores = {85, 92, 78, 95, 88};
Arrays.sort(scores, Comparator.reverseOrder());
System.out.println(Arrays.toString(scores)); // [95, 92, 88, 85, 78]
注意:
Comparator.reverseOrder()仅适用于对象类型(如Integer[]),对基本类型int[]无效。降序排序int[]需手动反向遍历或先升序再反转。
Arrays.binarySearch():二分查找
前提条件:数组必须是已排序的(升序),否则结果不可预测!
int[] scores = {78, 85, 88, 92, 95}; // 必须已排序
int index = Arrays.binarySearch(scores, 88);
System.out.println("88分的下标:" + index); // 2(找到了,返回下标)
int index2 = Arrays.binarySearch(scores, 80);
System.out.println("80分的下标:" + index2); // -2(未找到,返回 -(插入点) - 1)
返回值规律:
| 情况 | 返回值 | 计算规则 |
|---|---|---|
| 找到元素 | >= 0 | 元素在数组中的实际下标 |
| 未找到 | < 0 | -(插入点) - 1,其中插入点 = 第一个大于该值的元素下标 |
// 验证插入点公式
int[] arr = {10, 20, 30, 40, 50};
int result = Arrays.binarySearch(arr, 25); // 25不在数组中
// 插入点 = 2(25应插入在下标2的位置,即20和30之间)
// 返回值 = -(2) - 1 = -3
System.out.println(result); // -3
Arrays.copyOf() 与 Arrays.copyOfRange():数组复制
这两个方法是数组扩容/截断的标准手段,底层调用 System.arraycopy()——一个 native 方法,性能极高。
// copyOf:从头部开始复制指定长度
int[] original = {85, 92, 78, 95, 88};
int[] truncated = Arrays.copyOf(original, 3); // 截断:只复制前3个
System.out.println(Arrays.toString(truncated)); // [85, 92, 78]
int[] expanded = Arrays.copyOf(original, 8); // 扩容:超出部分填默认值0
System.out.println(Arrays.toString(expanded)); // [85, 92, 78, 95, 88, 0, 0, 0]
// copyOfRange:复制指定区间 [from, to)
int[] subArray = Arrays.copyOfRange(original, 1, 4); // 下标1~3
System.out.println(Arrays.toString(subArray)); // [92, 78, 95]
底层原理:
System.arraycopy()是 native 方法,在 JVM 内部使用memmove()或类似的内存操作实现。对于基本类型数组,它是整块内存拷贝,速度远快于循环逐元素赋值。
Arrays.equals():数组相等比较
== 比较的是引用地址,Arrays.equals() 比较的是元素内容。
int[] arr1 = {85, 92, 78};
int[] arr2 = {85, 92, 78};
int[] arr3 = arr1;
System.out.println(arr1 == arr2); // false(不同对象)
System.out.println(arr1 == arr3); // true(同一引用)
System.out.println(Arrays.equals(arr1, arr2)); // true(内容相同)
System.out.println(Arrays.equals(arr1, arr3)); // true(内容相同)
// 多维数组比较:必须用 deepEquals()
int[][] matrix1 = {{1, 2}, {3, 4}};
int[][] matrix2 = {{1, 2}, {3, 4}};
System.out.println(Arrays.equals(matrix1, matrix2)); // false!(比较的是引用)
System.out.println(Arrays.deepEquals(matrix1, matrix2)); // true
Arrays.fill():数组填充
用指定值填充整个数组或指定区间。
// 填充整个数组
int[] scores = new int[5];
Arrays.fill(scores, 100);
System.out.println(Arrays.toString(scores)); // [100, 100, 100, 100, 100]
// 填充指定区间 [from, to)
int[] scores2 = {85, 92, 78, 95, 88};
Arrays.fill(scores2, 1, 4, 0); // 下标1~3填充为0
System.out.println(Arrays.toString(scores2)); // [85, 0, 0, 0, 88]
Arrays.asList():数组转 List —— 重要陷阱!
这是 Arrays 中最容易被误用的方法,面试高频考点。
String[] names = {"大翔", "白歌", "小崔"};
List<String> list = Arrays.asList(names);
System.out.println(list); // [大翔, 白歌, 小崔]
陷阱一:返回的 List 是固定大小的
Arrays.asList() 返回的是 Arrays 内部类 ArrayList(不是 java.util.ArrayList),它直接包装了原始数组,不支持 add / remove 操作。
// ❌ 错误:试图向 asList 返回的 List 添加元素
List<String> list = Arrays.asList("大翔", "白歌", "小崔");
list.add("孔蓝"); // UnsupportedOperationException!
// ❌ 错误:试图删除元素
list.remove(0); // UnsupportedOperationException!
// ✅ 正确:如果需要可变 List,包装一层 new ArrayList
List<String> mutableList = new ArrayList<>(Arrays.asList("大翔", "白歌", "小崔"));
mutableList.add("孔蓝"); // 正常
陷阱二:基本类型数组的 asList 行为
这是最隐蔽的陷阱——对基本类型数组调用 asList(),整个数组被当作单个元素!
// ❌ 陷阱:int[] 被当作一个对象
int[] scores = {85, 92, 78};
List<int[]> list = Arrays.asList(scores); // List 中只有一个元素:整个 int[] 数组!
System.out.println(list.size()); // 1!不是 3!
System.out.println(list.get(0)); // [I@15db9742(数组的地址)
// ✅ 正确:使用包装类 Integer[]
Integer[] scores2 = {85, 92, 78};
List<Integer> list2 = Arrays.asList(scores2); // List 中有三个元素
System.out.println(list2.size()); // 3
System.out.println(list2); // [85, 92, 78]
原因分析:Arrays.asList(T... a) 是泛型可变参数方法。int[] 不是引用类型,无法作为泛型类型参数——Java 将整个 int[] 推断为 T 的一个具体实例(T = int[]),因此 asList 创建的是包含单个数组对象的 List。
完整代码示例
示例一:飞翔科技员工考核排名系统
场景:大翔要求小崔实现一个员工季度考核排名系统,包括成绩录入、排序、查找特定员工排名、以及 Top N 展示。
import java.util.Arrays;
/**
* 飞翔科技 —— 员工季度考核排名系统
* 演示 sort / binarySearch / copyOf / toString 综合使用
*/
public class EmployeeRanking {
public static void main(String[] args) {
// 1. 员工姓名与考核分数(对应下标)
String[] names = {"大翔", "白歌", "小崔", "孔蓝", "李明", "张伟", "王芳"};
int[] scores = {91, 95, 78, 88, 82, 76, 93};
System.out.println("========== 飞翔科技 Q2 季度考核 ==========\n");
System.out.println("原始数据:");
for (int i = 0; i < names.length; i++) {
System.out.printf(" %s:%d 分\n", names[i], scores[i]);
}
// 2. 复制一份原数组用于排序(保留原始顺序)
int[] sortedScores = Arrays.copyOf(scores, scores.length);
Arrays.sort(sortedScores); // 升序排列
System.out.println("\n分数排名(升序):");
System.out.println(Arrays.toString(sortedScores));
// 3. 展示 Top 3(降序)
System.out.println("\n🏆 Top 3 优秀员工:");
int count = 0;
for (int i = sortedScores.length - 1; i >= 0 && count < 3; i--) {
// 找到该分数对应的员工姓名
for (int j = 0; j < scores.length; j++) {
if (scores[j] == sortedScores[i]) {
System.out.printf(" 第%d名:%s(%d分)\n", count + 1, names[j], sortedScores[i]);
break;
}
}
count++;
}
// 4. 查找孔蓝的排名
int konglanScore = scores[3]; // 孔蓝的原始分数
Arrays.sort(scores); // 对原数组排序(注意:这会影响scores数组!)
int rankIndex = Arrays.binarySearch(sortedScores, konglanScore);
// 升序数组中,排名 = 总人数 - 下标
int rank = sortedScores.length - rankIndex;
System.out.printf("\n孔蓝成绩:%d 分,排名第 %d\n", konglanScore, rank);
// 5. 计算分数段统计
int excellent = 0; // >= 90
int good = 0; // 80-89
int normal = 0; // 70-79
for (int s : scores) {
if (s >= 90) excellent++;
else if (s >= 80) good++;
else normal++;
}
System.out.println("\n分数段统计:");
System.out.printf(" 优秀(>=90):%d 人\n", excellent);
System.out.printf(" 良好(80-89):%d 人\n", good);
System.out.printf(" 一般(70-79):%d 人\n", normal);
}
}
运行输出:
========== 飞翔科技 Q2 季度考核 ==========
原始数据:
大翔:91 分
白歌:95 分
小崔:78 分
孔蓝:88 分
李明:82 分
张伟:76 分
王芳:93 分
分数排名(升序):
[76, 78, 82, 88, 91, 93, 95]
🏆 Top 3 优秀员工:
第1名:白歌(95分)
第2名:王芳(93分)
第3名:大翔(91分)
孔蓝成绩:88 分,排名第 4
分数段统计:
优秀(>=90):3 人
良好(80-89):2 人
一般(70-79):2 人
示例二:飞翔科技商品价格批量调整
场景:孔蓝负责测试飞翔科技电商平台的商品管理模块。她需要验证"批量调价"功能的正确性——对一组商品价格进行统一调整后,对比新旧价格数组是否一致、是否有遗漏。
import java.util.Arrays;
/**
* 飞翔科技 —— 商品价格批量调整验证
* 演示 equals / fill / copyOf / asList 的使用与陷阱规避
*/
public class ProductPriceAdjuster {
public static void main(String[] args) {
System.out.println("========== 飞翔科技电商平台 —— 商品调价验证 ==========\n");
// 1. 定义商品价格(使用包装类 Integer[],避免 asList 陷阱)
Integer[] oldPrices = {199, 299, 159, 399, 259};
String[] productNames = {"机械键盘", "蓝牙耳机", "无线鼠标", "显示器支架", "USB集线器"};
System.out.println("商品原价:");
System.out.println(Arrays.toString(oldPrices));
// 2. 使用 asList 展示价格列表(Integer[] 安全使用)
System.out.println("\n价格列表:" + Arrays.asList(oldPrices));
System.out.println("列表元素个数 = " + Arrays.asList(oldPrices).size() + "(正确为5)");
// 3. 复制并统一上调 10%
Integer[] newPrices = Arrays.copyOf(oldPrices, oldPrices.length);
for (int i = 0; i < newPrices.length; i++) {
newPrices[i] = (int) (newPrices[i] * 1.1);
}
System.out.println("\n调价后(上调10%):");
for (int i = 0; i < productNames.length; i++) {
System.out.printf(" %s:¥%d → ¥%d\n", productNames[i], oldPrices[i], newPrices[i]);
}
// 4. 验证调价是否正确
Integer[] expectedPrices = {219, 329, 175, 439, 285}; // 手动计算的期望值
// 注意:199*1.1=218.9→219,但Java int截断为218!
// 这是浮点精度陷阱,此处仅作演示
System.out.println("\n期望价格:" + Arrays.asList(expectedPrices));
boolean allCorrect = Arrays.equals(newPrices, expectedPrices);
System.out.println("调价是否正确?" + (allCorrect ? "是" : "否(浮点截断导致偏差)"));
// 5. 使用 fill 重置测试数据
Integer[] testPrices = new Integer[5];
Arrays.fill(testPrices, 0);
System.out.println("\n测试环境重置:验证所有价格为0 → " + Arrays.equals(testPrices, new Integer[]{0, 0, 0, 0, 0}));
// 6. 演示 asList 陷阱(仅演示,不运行破坏性操作)
System.out.println("\n========== Arrays.asList 陷阱演示 ==========");
// 陷阱1: int[] → List<int[]>
int[] primitivePrices = {199, 299, 159};
System.out.println("int[] 转 List 的元素个数:" + Arrays.asList(primitivePrices).size() + "(应该为3,实际为1!)");
// 陷阱2: asList 返回的 List 不可变
System.out.print("尝试在 asList 返回的 List 上执行 add():");
try {
Arrays.asList(oldPrices).add(999);
} catch (UnsupportedOperationException e) {
System.out.println("抛出 UnsupportedOperationException(符合预期)");
}
System.out.println("\n========== 正确用法总结 ==========");
System.out.println("1. 基本类型数组转 List 用 Integer[]/Long[] 等包装类");
System.out.println("2. 需要可变 List 时用 new ArrayList<>(Arrays.asList(...))");
System.out.println("3. 多维度数组比较用 deepEquals / deepToString");
}
}
运行输出:
========== 飞翔科技电商平台 —— 商品调价验证 ==========
商品原价:
[199, 299, 159, 399, 259]
价格列表:[199, 299, 159, 399, 259]
列表元素个数 = 5(正确为5)
调价后(上调10%):
机械键盘:¥199 → ¥218
蓝牙耳机:¥299 → ¥328
无线鼠标:¥159 → ¥174
显示器支架:¥399 → ¥438
USB集线器:¥259 → ¥284
期望价格:[219, 329, 175, 439, 285]
调价是否正确?否(浮点截断导致偏差)
测试环境重置:验证所有价格为0 → true
========== Arrays.asList 陷阱演示 ==========
int[] 转 List 的元素个数:1(应该为3,实际为1!)
尝试在 asList 返回的 List 上执行 add():抛出 UnsupportedOperationException(符合预期)
========== 正确用法总结 ==========
1. 基本类型数组转 List 用 Integer[]/Long[] 等包装类
2. 需要可变 List 时用 new ArrayList<>(Arrays.asList(...))
3. 多维度数组比较用 deepEquals / deepToString
易错场景
反例一:未排序就调用 binarySearch
// ❌ 错误:数组未排序就使用 binarySearch
int[] scores = {85, 92, 78, 95, 88};
int index = Arrays.binarySearch(scores, 92); // 结果不可预测!
// ✅ 正确:先排序再查找
int[] scores = {85, 92, 78, 95, 88};
Arrays.sort(scores); // [78, 85, 88, 92, 95]
int index = Arrays.binarySearch(scores, 92); // 3(正确)
反例二:对多维数组使用 toString 而非 deepToString
// ❌ 错误:二维数组用 toString
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.toString(matrix));
// 输出:[[I@15db9742, [I@6d06d69c] —— 不可读!
// ✅ 正确:用 deepToString
System.out.println(Arrays.deepToString(matrix));
// 输出:[[1, 2], [3, 4]]
反例三:将 asList 返回的 List 当作可变 List
// ❌ 错误:Arrays.asList 返回的 List 不支持结构性修改
List<String> list = Arrays.asList("大翔", "白歌", "小崔");
list.add("孔蓝"); // UnsupportedOperationException
list.remove(0); // UnsupportedOperationException
list.clear(); // UnsupportedOperationException
// ✅ 正确:包装为真正的 ArrayList
List<String> list = new ArrayList<>(Arrays.asList("大翔", "白歌", "小崔"));
list.add("孔蓝"); // 正常
反例四:用 equals 比较多维数组
// ❌ 错误:二维数组用 equals
int[][] m1 = {{1, 2}, {3, 4}};
int[][] m2 = {{1, 2}, {3, 4}};
System.out.println(Arrays.equals(m1, m2)); // false!(比较的是行引用)
// ✅ 正确:用 deepEquals
System.out.println(Arrays.deepEquals(m1, m2)); // true
面试考点
Q1:Arrays.sort() 对 int[] 和 Integer[] 分别使用什么排序算法?为什么不同?
对
int[](基本类型):使用双轴快速排序(Dual-Pivot Quicksort),平均 O(n log n),最坏 O(n²)(但通过随机化等策略,实际很少触发最坏情况)。选择快速排序是因为基本类型对稳定性没有要求,而快速排序的常数因子更小,实际性能更优。对
Integer[](对象类型):使用TimSort(一种改进的归并排序),保证 O(n log n) 且是稳定的。因为对象排序通常依赖Comparator,用户可能期望稳定排序(相等元素的相对顺序不变)。TimSort 在部分有序的数据上性能尤其出色,接近 O(n)。
Q2:Arrays.binarySearch() 在未排序数组上调用会发生什么?
结果是未定义的(undefined)。可能返回一个错误的负数、可能偶然返回正确的下标,也可能返回一个完全无关的数。Javadoc 明确说明:如果数组未排序,结果是不可预测的。这是典型的"垃圾进,垃圾出"——方法不会检查数组是否有序,调用者负责保证前置条件。
Q3:Arrays.asList() 返回的 List 和 new ArrayList<>() 有什么本质区别?
①
Arrays.asList()返回的是Arrays的内部类ArrayList,它只是原始数组的一个视图(view),没有复制数据。对该 List 的set()操作会直接修改底层数组。 ② 它不支持结构性修改(add、remove、clear),调用会抛出UnsupportedOperationException。 ③java.util.ArrayList是真正的独立集合,数据独立存储,支持所有操作。关键公式:
new ArrayList<>(Arrays.asList(arr))= 可变的、独立存储的 ArrayList。
Q4:Arrays.copyOf() 和 System.arraycopy() 有什么区别?
Arrays.copyOf()是高层封装,内部调用System.arraycopy()。区别在于:
copyOf()返回一个新数组,自动处理创建目标数组的逻辑,支持扩容/截断arraycopy()需要调用者预先创建目标数组,不会自动创建copyOf()更安全易用,arraycopy()更灵活(如复制到已有数组的指定位置)
Q5:为什么 int[] 不能直接用于 Arrays.asList()?如何正确转换?
Arrays.asList(T... a)的参数类型是泛型可变参数,泛型不支持基本类型。int[]被当作一个单独的引用类型(T = int[]),因此整个数组成为一个 List 元素。正确转换方式: ① 使用包装类
Integer[]替代int[](推荐) ② 如果已经是int[],用循环或 Stream API 手动转换:List<Integer> list = Arrays.stream(intArray).boxed().collect(Collectors.toList());
白歌的结语:"掌握
Arrays工具类,是区分初级开发和有经验开发者的重要标志。不要重复造轮子——JDK 提供的这些方法经过了数十年的测试和优化,比你手写的版本更可靠、更高效。但请务必记住那几个陷阱:binarySearch需要排序、asList不可修改且不兼容基本类型、多维数组要用deep系列方法。这些知识点在面试中出现的频率极高。"