Arrays 工具类与数组操作 | JavaSE

Arrays 工具类与数组操作

一、学习目标

完成本章后,你应该能够:

  1. 解释 java.util.Arrays 工具类存在的意义,并区分“数组本身”和“数组工具类”。
  2. 熟练使用 Arrays.toString() 快速输出一维数组内容。
  3. 使用 Arrays.sort() 对基本数据类型数组进行排序。
  4. 正确使用 Arrays.binarySearch() 在有序数组中执行二分查找,并理解其返回值。
  5. 使用 Arrays.copyOf()Arrays.copyOfRange() 完成数组复制和截取。
  6. 使用 Arrays.fill() 批量填充数组元素。
  7. 使用 Arrays.equals() 判断两个数组内容是否相同。
  8. 能够判断哪些 Arrays API 会修改原数组,哪些 API 会返回新的结果。
  9. 能够 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 知识问答

  1. Arrays 是什么类?为什么可以直接使用类名调用其方法?
  2. 数组与 Arrays 工具类有什么区别?
  3. Arrays.toString() 解决什么问题?
  4. Arrays.sort() 是否修改原数组?
  5. Arrays.binarySearch() 的前置条件是什么?
  6. binarySearch() 找不到数据时返回值有什么特点?
  7. 为什么不能只判断返回值是否等于 -1
  8. Arrays.copyOf() 与直接执行 b = a 有什么本质区别?
  9. copyOf() 新长度大于原长度时,多余位置保存什么?
  10. copyOfRange() 的区间规则是什么?
  11. Arrays.fill() 是否修改原数组?
  12. Arrays.equals() 与数组的 == 有什么区别?
  13. 对象数组执行 copyOf() 为什么属于浅拷贝?
  14. 一维数组与多维数组分别如何方便输出?
  15. 为什么实际业务排序通常不需要自己重新手写排序算法?

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)
        );
    }
}

要求解释:

  1. a 是否被修改?
  2. b 最后两个元素是什么?
  3. a == b 为什么得到对应结果?
  4. Arrays.equals(a, b) 为什么得到对应结果?

7.3 手写代码

任务一:成绩排序

给定:

int[] scores =
        {88, 56, 100, 72, 91, 65};

要求:

  1. 打印原数组。
  2. 使用 Arrays 排序。
  3. 打印排序后数组。
  4. 输出最低分和最高分。

任务二:二分查找

给定:

int[] numbers =
        {17, 3, 99, 25, 8, 66};

要求:

  1. 首先处理数组,使其满足二分查找条件。
  2. 查找 25
  3. 输出索引。
  4. 查找一个不存在的值。
  5. 根据返回值判断是否找到。

任务三:数组复制

给定:

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);
    }
}

要求:

  1. 程序语法是否有问题?
  2. API 使用违反了什么前置条件?
  3. 修改代码。
  4. 说明为什么修改后的搜索结果才可靠。

继续 Debug:

int[] numbers = {1, 2, 3};

int[] copy =
        Arrays.copyOfRange(
                numbers,
                2,
                1
        );

要求:

  1. 分析区间是否合法。
  2. 说明 [fromIndex,toIndex) 中两个索引的关系。
  3. 修复代码。

7.5 综合训练

设计一个“学生成绩分析器”。

已有:

int[] scores =
        {76, 88, 92, 61, 55, 100, 83};

要求:

  1. 保留原数组不变。
  2. 创建成绩数组副本。
  3. 对副本排序。
  4. 输出排序结果。
  5. 查找指定成绩是否存在。
  6. 输出最低分。
  7. 输出最高分。
  8. 比较处理前的原数组和副本是否相同。
  9. 思考什么时候应该修改原数组,什么时候应该先复制。

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

则本章核心能力基本达标。