Arrays 工具类与数组操作 | JavaSE
Arrays 工具类与数组操作
一、学习目标
完成本章后,你应该能够:
- 解释
java.util.Arrays工具类存在的意义,并区分“数组本身”和“数组工具类”。 - 熟练使用
Arrays.toString()快速输出一维数组内容。 - 使用
Arrays.sort()对基本数据类型数组进行排序。 - 正确使用
Arrays.binarySearch()在有序数组中执行二分查找,并理解其返回值。 - 使用
Arrays.copyOf()、Arrays.copyOfRange()完成数组复制和截取。 - 使用
Arrays.fill()批量填充数组元素。 - 使用
Arrays.equals()判断两个数组内容是否相同。 - 能够判断哪些 Arrays API 会修改原数组,哪些 API 会返回新的结果。
- 能够 Debug 二分查找前未排序、区间越界、直接打印数组地址等常见问题。
二、核心知识
2.1 Arrays 是什么
前面学习数组时,我们已经可以:
- 创建数组
- 根据索引访问元素
- 修改元素
- 使用循环遍历数组
- 自己实现查找、排序、复制等算法
例如:
int[] numbers = {30, 10, 50, 20, 40};
如果希望打印所有数据,我们以前可能写:
for (int number : numbers) {
System.out.println(number);
}
如果希望排序,也可以自己写冒泡排序、选择排序等算法。
但是在实际开发中,一些通用的数组操作没有必要每次重新实现。
Java 为此提供了:
java.util.Arrays
Arrays 是专门用于处理数组的工具类。
可以建立下面的认识:
数组 array
│
├── 真正保存数据
│
└── 本身只有 length 等基础能力
Arrays
│
└── Java 提供的数组工具类
├── 打印
├── 排序
├── 查找
├── 复制
├── 填充
└── 比较
2.2 Arrays 为什么是工具类
使用 Arrays 时通常不会:
new Arrays();
而是直接:
Arrays.toString(array);
Arrays.sort(array);
Arrays.binarySearch(array, key);
也就是说,它提供的大量功能都是:
static
静态方法。
这和前面学习的工具类设计思想是一致的:
功能主要处理外部传入的数据
+
对象本身没有需要长期保存的业务状态
↓
可以设计成工具类
↓
通过 类名.静态方法 调用
因此:
Arrays.sort(numbers);
不是:
numbers 对象自己拥有 sort 方法。
而是:
把 numbers 数组交给 Arrays 工具类处理。
2.3 本章重点 API
| API | 作用 |
| ------------------------------------- | -------------------------- |
| Arrays.toString(array) | 将一维数组转换成可读字符串 |
| Arrays.sort(array) | 对数组排序 |
| Arrays.binarySearch(array, key) | 对有序数组进行二分查找 |
| Arrays.copyOf(array, newLength) | 按新长度复制数组 |
| Arrays.copyOfRange(array, from, to) | 复制指定区间 |
| Arrays.fill(array, value) | 批量填充数组 |
| Arrays.equals(a, b) | 比较两个数组内容 |
| Arrays.deepToString(array) | 输出多维数组内容 |
学习这些 API 时不要只背方法名。
更重要的是判断:
操作是否修改原数组?
操作是否产生新数组?
返回值是什么?
索引区间是什么规则?
调用前有没有前置条件?
三、使用方法
3.1 Arrays.toString():快速查看数组内容
假设:
int[] numbers = {10, 20, 30};
如果直接:
System.out.println(numbers);
通常得到的不是:
[10, 20, 30]
而是类似:
[I@1b6d3586
这是因为数组没有像 String 那样直接提供我们期待的内容输出形式。
可以使用:
import java.util.Arrays;
public class ArraysDemo {
public static void main(String[] args) {
int[] numbers = {10, 20, 30};
System.out.println(Arrays.toString(numbers));
}
}
输出:
[10, 20, 30]
所以调试数组时:
Arrays.toString(array)
非常常用。
3.2 toString() 有没有修改原数组
没有。
例如:
int[] numbers = {3, 1, 2};
String text = Arrays.toString(numbers);
此时:
numbers
↓
[3, 1, 2]
没有变化。
只是额外产生了:
"[3, 1, 2]"
这个 String。
因此:
Arrays.toString()
→ 读取数组
→ 返回字符串
→ 不修改原数组
3.3 Arrays.sort():数组排序
例如:
int[] numbers = {30, 10, 50, 20, 40};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
结果:
[10, 20, 30, 40, 50]
这里必须注意:
Arrays.sort()会直接修改传入的原数组。
执行前:
numbers
↓
[30, 10, 50, 20, 40]
执行:
Arrays.sort(numbers);
执行后:
numbers
↓
[10, 20, 30, 40, 50]
它不像:
String.replace()
那样返回一个新的结果让你重新接收。
对于基本类型数组:
int[]
long[]
double[]
char[]
...
最常见的:
Arrays.sort(array);
会按照对应类型的自然顺序进行排序。
3.4 排序指定区间
Arrays 也支持只排序数组的一部分。
例如:
int[] numbers = {9, 5, 4, 3, 8};
Arrays.sort(numbers, 1, 4);
System.out.println(Arrays.toString(numbers));
排序区间:
[1, 4)
即:
索引 1、2、3
不包含索引 4。
原数组:
索引 0 1 2 3 4
数据 9 5 4 3 8
局部排序后:
9 3 4 5 8
又一次出现了 Java API 中非常常见的:
[fromIndex, toIndex)
也就是:
包前不包后。
3.5 Arrays.binarySearch():二分查找
例如现在有一个已经排序好的数组:
int[] numbers = {10, 20, 30, 40, 50};
查找:
int index = Arrays.binarySearch(numbers, 30);
System.out.println(index);
结果:
2
因为:
10 20 30 40 50
0 1 2 3 4
↑
3.6 binarySearch 最重要的前置条件
使用:
Arrays.binarySearch()
之前,数组必须按照对应规则:
已经排好序。
典型流程:
int[] numbers = {50, 10, 30, 20, 40};
Arrays.sort(numbers);
int index =
Arrays.binarySearch(numbers, 30);
即:
无序数组
↓
Arrays.sort()
↓
有序数组
↓
Arrays.binarySearch()
不能把:
binarySearch
理解成:
无论数组是什么状态都能帮我找到数据。
3.7 binarySearch 找不到时是不是返回 -1
不一定。
这是非常重要的一个易错点。
例如:
int[] numbers = {10, 20, 30, 40, 50};
int index =
Arrays.binarySearch(numbers, 25);
System.out.println(index);
找不到时会返回一个负数。
其规则是:
-(插入点) - 1
假设:
25
应该插到:
20 和 30 之间
插入点索引:
2
因此返回:
-2 - 1
=
-3
所以业务中判断是否找到,最简单的方式是:
if (index >= 0) {
System.out.println("找到了");
} else {
System.out.println("没有找到");
}
不要写:
if (index == -1)
来表示“所有没找到”。
3.8 为什么 binarySearch 不直接统一返回 -1
因为负数返回值除了表示:
没有找到
还携带了:
如果要插入这个数据,应该插在哪里
的信息。
如果返回值为:
result < 0
那么插入点可以反推:
-result - 1
例如:
result = -3
则:
插入点
= -(-3) - 1
= 2
这比简单返回 -1 提供了更多信息。
3.9 Arrays.copyOf():复制数组
假设:
int[] original = {10, 20, 30};
复制:
int[] copy =
Arrays.copyOf(original, original.length);
结果:
original → [10, 20, 30]
copy → [10, 20, 30]
内容一样。
但是:
它们是两个不同的数组对象。
修改:
copy[0] = 999;
之后:
original → [10, 20, 30]
copy → [999, 20, 30]
对于基本类型数组来说,复制的是每个元素的值。
3.10 copyOf() 还可以改变数组长度
例如:
int[] original = {10, 20, 30};
int[] copy =
Arrays.copyOf(original, 5);
System.out.println(
Arrays.toString(copy)
);
结果:
[10, 20, 30, 0, 0]
为什么最后出现:
0, 0
因为:
int
数组新增加的位置使用基本类型默认值:
0
如果:
String[] names = {"A", "B"};
String[] copy =
Arrays.copyOf(names, 4);
新增位置则是:
null
因为引用类型数组默认值是:
null
3.11 copyOf() 可以缩短数组
例如:
int[] original = {10, 20, 30, 40};
int[] copy =
Arrays.copyOf(original, 2);
结果:
[10, 20]
因此:
Arrays.copyOf(original, newLength)
可以理解为:
按照指定的新长度创建数组,并复制能够保留下来的原数据。
3.12 Arrays.copyOfRange():复制指定区间
例如:
int[] numbers =
{10, 20, 30, 40, 50};
int[] part =
Arrays.copyOfRange(numbers, 1, 4);
System.out.println(
Arrays.toString(part)
);
结果:
[20, 30, 40]
因为区间仍然是:
[1, 4)
即:
1
2
3
不包含:
4
这和:
String.substring(beginIndex, endIndex)
具有类似的半开区间设计。
3.13 Arrays.fill():批量填充
假设:
int[] numbers = new int[5];
默认:
[0, 0, 0, 0, 0]
可以:
Arrays.fill(numbers, 100);
结果:
[100, 100, 100, 100, 100]
这里:
fill()会直接修改原数组。
3.14 fill() 填充指定区间
例如:
int[] numbers =
{1, 2, 3, 4, 5};
Arrays.fill(numbers, 1, 4, 0);
System.out.println(
Arrays.toString(numbers)
);
结果:
[1, 0, 0, 0, 5]
仍然是:
[1, 4)
因此修改:
索引 1、2、3
3.15 Arrays.equals():比较数组内容
假设:
int[] a = {10, 20, 30};
int[] b = {10, 20, 30};
如果:
System.out.println(a == b);
通常得到:
false
因为:
a
和
b
保存的是两个不同数组对象的引用。
但是:
System.out.println(
Arrays.equals(a, b)
);
结果:
true
Arrays.equals() 判断的是:
长度是否相同
+
对应位置元素是否相等
所以:
int[] a = {10, 20, 30};
int[] b = {30, 20, 10};
即使包含相同的数据集合:
Arrays.equals(a, b)
仍然是:
false
因为元素顺序不同。
3.16 数组的 == 与 Arrays.equals()
需要建立:
a == b
和:
Arrays.equals(a, b)
之间的区别。
对于数组引用:
==
↓
是不是同一个数组对象
而:
Arrays.equals()
↓
数组内容是否相同
这与之前 String 中:
==
vs
equals()
具有相似的思维方式。
3.17 多维数组与 deepToString()
假设:
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
如果希望方便查看多维结构,可以:
System.out.println(
Arrays.deepToString(matrix)
);
得到类似:
[[1, 2, 3], [4, 5, 6]]
因此:
一维数组
→ Arrays.toString()
嵌套数组 / 多维数组
→ Arrays.deepToString()
3.18 Arrays 常用 API 总结
可以暂时记成:
看
→ toString
排
→ sort
找
→ binarySearch
复制
→ copyOf / copyOfRange
填
→ fill
比
→ equals
形成这样一条 API 选择链:
我要做什么?
↓
只是查看数组?
→ toString
需要排序?
→ sort
在有序数组中快速查找?
→ binarySearch
需要新数组副本?
→ copyOf
只复制一部分?
→ copyOfRange
批量设成同一个值?
→ fill
比较内容?
→ equals
四、原理与进阶
4.1 Arrays.sort 和自己写排序算法是什么关系
学习数据结构与算法时,我们仍然必须理解:
冒泡排序
选择排序
插入排序
快速排序
归并排序
……
因为它们训练的是:
算法思想
复杂度分析
数据移动
比较逻辑
而实际业务开发时:
如果只是需要把数组排序,一般优先使用标准库已经提供的可靠实现。
所以:
学习算法
≠
实际项目每次都自己重新造排序算法
二者目标不同。
4.2 Arrays.sort 的具体实现不是一个固定算法
不要死记:
Arrays.sort 永远等于某一种排序算法
因为:
- 数组元素类型不同
- JDK 实现可能不同
- 对象数组和基本类型数组可能采用不同策略
当前阶段真正应该掌握的是 API 的:
语义
前置条件
输入
输出
副作用
而不是把 JDK 内部实现细节当成永远不变的语言规则。
4.3 二分查找为什么要求数组有序
二分查找能够每次排除大约一半数据的前提是:
我们知道目标值与中间值比较后,应该继续往哪一侧查找。
例如:
10 20 30 40 50 60 70
↑
40
查找:
60
因为:
60 > 40
并且数组有序,所以可以确定:
10 20 30
全部不可能是答案。
如果数组无序:
60 10 50 30 70 20 40
看到中间的:
30
无法根据:
60 > 30
推断 60 一定在右边。
因此:
有序性
是二分查找正确工作的逻辑基础。
4.4 Arrays.copyOf 是浅层复制
对于基本数据类型数组:
int[] source = {10, 20, 30};
int[] copy = Arrays.copyOf(source, source.length);
元素保存的是值,因此非常直观。
但对于对象数组:
Student[] students = ...;
Student[] copy =
Arrays.copyOf(
students,
students.length
);
复制的是:
数组每个位置中保存的对象引用。
因此:
原数组 新数组
│ │
├────┐ ┌───┤
↓ ↓
Student对象
两个数组本身不同,但其中某些位置可能仍然引用同一个 Student 对象。
这叫:
浅拷贝(Shallow Copy)。
真正复制对象内部数据属于:
深拷贝(Deep Copy)。
这是后续工程开发中非常重要的对象模型问题。
4.5 Arrays.compare() 与 mismatch()
JDK 还提供:
Arrays.compare()
Arrays.mismatch()
等方法。
其中:
Arrays.compare(a, b)
可以按照字典序比较两个数组。
而:
Arrays.mismatch(a, b)
可以找到两个数组:
第一个不同元素所在的位置。
例如这类需求:
版本数据对比
协议数据比较
二进制数据检查
测试结果比对
可能会使用它们。
但对于当前基础阶段:
首先把
toString / sort / binarySearch / copyOf / fill / equals掌握扎实。
五、实践应用
5.1 学生成绩排序
int[] scores =
{88, 95, 67, 76, 100, 59};
Arrays.sort(scores);
System.out.println(
Arrays.toString(scores)
);
这可以作为后续:
最低分
最高分
中位数
排名
等统计处理的基础。
5.2 搜索商品价格
假设已经有:
int[] prices =
{999, 1299, 1999, 2999, 4999};
希望判断:
2999 元
是否存在:
int index =
Arrays.binarySearch(prices, 2999);
if (index >= 0) {
System.out.println(
"找到,索引:" + index
);
}
前提仍然是:
prices 已按对应规则排序
5.3 创建数组快照
假设一个方法要处理数组,但不希望直接改变调用者原来的数据。
可以:
int[] copy =
Arrays.copyOf(
source,
source.length
);
Arrays.sort(copy);
这样:
source
原有顺序可以保留。
这是很常见的程序设计意识:
修改数据前先明确:到底应该修改原对象,还是操作副本?
六、常见问题
6.1 Arrays 和 Array 是一回事吗
不是。
数组类型:
int[]
String[]
Student[]
是真正存储数据的数组。
而:
Arrays
是:
java.util.Arrays
工具类。
6.2 为什么 System.out.println(array) 看不到数组内容
因为直接输出数组引用时,不会自动得到我们期待的数组元素列表。
调试一维数组通常使用:
Arrays.toString(array)
6.3 Arrays.sort() 会不会产生新数组
不会。
例如:
Arrays.sort(numbers);
直接改变:
numbers
所引用数组内部的元素顺序。
6.4 为什么 binarySearch 找不到时不一定返回 -1
因为返回负数时还编码了:
插入点
信息。
因此判断:
result >= 0
表示找到。
result < 0
表示未找到。
6.5 无序数组能不能 binarySearch
不能依赖其结果。
必须先保证数组按照与搜索相同的顺序完成排序。
正确流程:
Arrays.sort(array);
int index =
Arrays.binarySearch(array, key);
6.6 Arrays.copyOf 是不是让两个变量指向同一个数组
不是。
例如:
int[] a = {1, 2, 3};
int[] b =
Arrays.copyOf(a, a.length);
有:
a == b
结果:
false
但:
Arrays.equals(a, b)
结果:
true
6.7 copyOf 增长数组后为什么出现 0 或 null
因为新增加的位置使用数组元素类型的默认值。
例如:
int
→ 0
double
→ 0.0
boolean
→ false
引用类型
→ null
6.8 多维数组为什么推荐 deepToString
因为二维数组本质上可以理解为:
一个“元素仍然是数组”的数组。
因此普通:
Arrays.toString(matrix)
只能看到最外层数组元素的字符串表现。
更适合:
Arrays.deepToString(matrix)
6.9 Arrays 和 Collections 是不是一个东西
不是。
Arrays
→ 主要处理数组
Collections
→ 主要为 Collection/List 等集合提供工具方法
后面的集合框架章节会系统学习:
Collections
不要因为两个名字都以:
s
结尾就混为一谈。
七、练习与验收
7.1 知识问答
- Arrays 是什么类?为什么可以直接使用类名调用其方法?
- 数组与 Arrays 工具类有什么区别?
Arrays.toString()解决什么问题?Arrays.sort()是否修改原数组?Arrays.binarySearch()的前置条件是什么?binarySearch()找不到数据时返回值有什么特点?- 为什么不能只判断返回值是否等于
-1? Arrays.copyOf()与直接执行b = a有什么本质区别?copyOf()新长度大于原长度时,多余位置保存什么?copyOfRange()的区间规则是什么?Arrays.fill()是否修改原数组?Arrays.equals()与数组的==有什么区别?- 对象数组执行
copyOf()为什么属于浅拷贝? - 一维数组与多维数组分别如何方便输出?
- 为什么实际业务排序通常不需要自己重新手写排序算法?
7.2 代码阅读
不运行代码,写出每一步输出:
import java.util.Arrays;
public class ArraysRead01 {
public static void main(String[] args) {
int[] numbers =
{40, 10, 30, 20};
System.out.println(
Arrays.toString(numbers)
);
Arrays.sort(numbers);
System.out.println(
Arrays.toString(numbers)
);
System.out.println(
Arrays.binarySearch(
numbers,
30
)
);
}
}
继续阅读:
import java.util.Arrays;
public class ArraysRead02 {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b =
Arrays.copyOf(a, 5);
b[0] = 100;
System.out.println(
Arrays.toString(a)
);
System.out.println(
Arrays.toString(b)
);
System.out.println(a == b);
System.out.println(
Arrays.equals(a, b)
);
}
}
要求解释:
a是否被修改?b最后两个元素是什么?a == b为什么得到对应结果?Arrays.equals(a, b)为什么得到对应结果?
7.3 手写代码
任务一:成绩排序
给定:
int[] scores =
{88, 56, 100, 72, 91, 65};
要求:
- 打印原数组。
- 使用 Arrays 排序。
- 打印排序后数组。
- 输出最低分和最高分。
任务二:二分查找
给定:
int[] numbers =
{17, 3, 99, 25, 8, 66};
要求:
- 首先处理数组,使其满足二分查找条件。
- 查找
25。 - 输出索引。
- 查找一个不存在的值。
- 根据返回值判断是否找到。
任务三:数组复制
给定:
int[] source =
{10, 20, 30, 40, 50};
分别完成:
- 完整复制
- 只复制前三个元素
- 复制索引
[1,4)的数据 - 扩容为长度 8
观察并解释每一个结果。
任务四:数组初始化
创建长度为 20 的:
int[]
要求使用 Arrays API 将全部元素初始化为:
-1
7.4 Debug
下面代码存在逻辑问题:
import java.util.Arrays;
public class DebugDemo {
public static void main(String[] args) {
int[] numbers =
{50, 10, 40, 20, 30};
int index =
Arrays.binarySearch(
numbers,
40
);
System.out.println(index);
}
}
要求:
- 程序语法是否有问题?
- API 使用违反了什么前置条件?
- 修改代码。
- 说明为什么修改后的搜索结果才可靠。
继续 Debug:
int[] numbers = {1, 2, 3};
int[] copy =
Arrays.copyOfRange(
numbers,
2,
1
);
要求:
- 分析区间是否合法。
- 说明
[fromIndex,toIndex)中两个索引的关系。 - 修复代码。
7.5 综合训练
设计一个“学生成绩分析器”。
已有:
int[] scores =
{76, 88, 92, 61, 55, 100, 83};
要求:
- 保留原数组不变。
- 创建成绩数组副本。
- 对副本排序。
- 输出排序结果。
- 查找指定成绩是否存在。
- 输出最低分。
- 输出最高分。
- 比较处理前的原数组和副本是否相同。
- 思考什么时候应该修改原数组,什么时候应该先复制。
7.6 本章验收
闭卷写出并解释:
Arrays.toString()
Arrays.sort()
Arrays.binarySearch()
Arrays.copyOf()
Arrays.copyOfRange()
Arrays.fill()
Arrays.equals()
Arrays.deepToString()
并能够回答:
哪些会修改原数组?
哪些会产生新结果?
binarySearch 为什么必须先排序?
copyOf 与 b = a 有什么区别?
数组 == 与 Arrays.equals 有什么区别?
如果能够独立完成:
排序
+
二分查找
+
复制
+
填充
+
比较
+
Debug
则本章核心能力基本达标。