Arrays 工具类
本教程共 100 篇 · 第 27 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
27. Arrays 工具类
本节目标:掌握 java.util.Arrays 提供的排序、二分查找、填充、复制、比较、转字符串和转 List 等常用方法,让数组操作不再手写循环。
数组本身很朴素,只有一个 length 属性和下标访问,连打印内容都做不到。排序要自己写冒泡,复制要自己开循环,比较内容要逐个元素对——这些活每个项目都要重写一遍,纯属浪费。
java.util.Arrays 就是 JDK 为此准备的工具箱,里面全是操作数组的静态方法。用之前记得导包:
import java.util.Arrays;
```bash
它的方法都是 `Arrays.xxx(数组, ...)` 的形式,不需要 `new Arrays()`,实际上它的构造方法是私有的,压根 new 不出来。
## 打印数组:toString
直接打印数组变量,得到的是一串天书:
```java
int[] a = {3, 1, 4, 2};
System.out.println(a); // [I@1b6d3586
[I 表示「int 数组」,@ 后面是哈希码,跟内容毫无关系。因为数组没有重写 toString(),用的还是 Object 的默认实现。
Arrays.toString 帮你把内容拼出来:
import java.util.Arrays;
public class ArraysDemo {
public static void main(String[] args) {
int[] a = {3, 1, 4, 2};
System.out.println(Arrays.toString(a)); // [3, 1, 4, 2]
}
}
```bash
> [!TIP]
> 调试时打印数组,永远用 `Arrays.toString`,别直接打印数组变量。二维及以上用 `Arrays.deepToString`,它会递归展开每一层。
## 排序:sort
`Arrays.sort` 把数组**原地**升序排列,不返回新数组:
```java
int[] a = {3, 1, 4, 2};
Arrays.sort(a);
System.out.println(Arrays.toString(a)); // [1, 2, 3, 4]
「原地」是关键。有人写 int[] b = Arrays.sort(a); 会编译报错,因为它的返回类型是 void。排完之后原数组 a 就已经有序了。
只排一段也行,区间是左闭右开:
int[] b = {5, 4, 3, 2, 1};
Arrays.sort(b, 1, 4); // 只排下标 1、2、3
System.out.println(Arrays.toString(b)); // [5, 2, 3, 4, 1]
```java
想要降序,基本类型数组没有现成参数,得先转成包装类型再传比较器:
```java
Integer[] c = {3, 1, 4, 2};
Arrays.sort(c, Comparator.reverseOrder());
System.out.println(Arrays.toString(c)); // [4, 3, 2, 1]
注意这里必须是 Integer[] 而不是 int[]。比较器只能作用于对象数组,基本类型没有对象可比。
内部实现上,基本类型用双轴快速排序,对象数组用 TimSort(归并的变体)。对象数组要求排序稳定——相等元素的原有先后顺序不能乱,快排做不到这点,所以两边用了不同算法。
二分查找:binarySearch
在已排序的数组里查值,比逐个比对快得多:
int[] a = {1, 2, 3, 4};
int idx = Arrays.binarySearch(a, 3);
System.out.println(idx); // 2,找到,返回下标
```java
没找到时返回的是一个负数,规则是 `-(插入点) - 1`:
```java
int[] a = {1, 2, 3, 4};
System.out.println(Arrays.binarySearch(a, 5)); // -5
5 应该插在下标 4 的位置,所以返回 -(4) - 1 = -5。这个设计的用意是:返回值 < 0 就代表没找到,同时还顺便告诉你该插在哪,一个返回值传递两条信息。
Warning
binarySearch要求数组已经排好序。在无序数组上查,不会报错,但结果完全不可信——可能返回错的下标,也可能明明存在却说找不到。查之前先sort。
填充:fill 与 setAll
把整个数组或某一段填成同一个值:
int[] a = new int[5];
Arrays.fill(a, 7); // 全部填 7
Arrays.fill(a, 1, 3, 9); // 下标 [1,3) 填 9,即 a[1]、a[2]
System.out.println(Arrays.toString(a)); // [7, 9, 9, 7, 7]
```java
区间同样是左闭右开,`[1, 3)` 只改下标 1 和 2,不含 3。JDK 里凡是带 `from`、`to` 的方法几乎都遵守这个约定。
要按规律填值,用 Java 8 加入的 `setAll`,它接收一个「下标 → 值」的函数:
```java
int[] squares = new int[5];
Arrays.setAll(squares, i -> i * i);
System.out.println(Arrays.toString(squares)); // [0, 1, 4, 9, 16]
复制:copyOf 与 copyOfRange
数组长度不可变,要「扩容」只能复制到新数组。手写循环能做,但 copyOf 一行搞定:
int[] a = {1, 2, 3};
int[] bigger = Arrays.copyOf(a, 5); // 变长到 5
System.out.println(Arrays.toString(bigger)); // [1, 2, 3, 0, 0]
int[] smaller = Arrays.copyOf(a, 2); // 截短到 2
System.out.println(Arrays.toString(smaller)); // [1, 2]
```java
变长时多出来的位置填默认值,截短时多余部分直接丢弃。`ArrayList` 内部扩容用的就是这个方法。
只要中间一段,用 `copyOfRange`,依然左闭右开:
```java
int[] mid = Arrays.copyOfRange(a, 1, 3); // 下标 1、2
System.out.println(Arrays.toString(mid)); // [2, 3]
Note对象数组的复制是浅拷贝:新数组里存的还是原来那些对象的引用,改对象内容两边都变。要深拷贝得自己逐个克隆。
比较:equals 与 deepEquals
数组是对象,== 比的是「是不是同一个数组」,跟内容无关:
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println(a == b); // false,两个不同对象
System.out.println(a.equals(b)); // false,没重写,等价于 ==
System.out.println(Arrays.equals(a, b)); // true,逐元素比内容
```java
第二行尤其阴险。`a.equals(b)` 语法上完全合法、不报错,但数组没有重写 `equals`,继承的是 Object 版本,效果和 `==` 一模一样。想比内容必须显式用 `Arrays.equals`。
多维数组用 `Arrays.deepEquals`,`Arrays.equals` 只比一层引用,对二维数组永远返回 false。
Java 9 还补了两个更好用的:
```java
int[] x = {1, 2, 3};
int[] y = {1, 2, 9};
System.out.println(Arrays.compare(x, y)); // 负数,字典序 x < y
System.out.println(Arrays.mismatch(x, y)); // 2,第一个不同的下标
compare 能直接给出大小关系,mismatch 告诉你第一处差异在哪,排查数据不一致时很省事。
转成 List:asList
想用集合的便利方法,可以把数组转成 List:
List<String> list = Arrays.asList("a", "b", "c");
System.out.println(list); // [a, b, c]
```text
但这个 List 有两个必须知道的限制。
第一,它是**定长视图**,不是普通的 ArrayList。长度锁死,增删都会抛异常:
```java
list.add("d"); // UnsupportedOperationException
list.remove(0); // UnsupportedOperationException
list.set(0, "z"); // 这个可以,改元素不改长度
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.AbstractList.add(AbstractList.java:155)
```java
第二,它是**视图**,和原数组共享同一块数据。改 List 会影响数组,改数组也会影响 List:
```java
String[] arr = {"a", "b"};
List<String> view = Arrays.asList(arr);
view.set(0, "z");
System.out.println(arr[0]); // z,原数组跟着变了
要一个真正独立、可增删的 List,包一层:
List<String> real = new ArrayList<>(Arrays.asList(arr));
real.add("c"); // 正常
```bash
Java 9 之后,如果只是想要一个不可变列表,`List.of("a", "b", "c")` 更直接,语义也更清楚——它连 `set` 都不允许,是彻底的只读。
> [!WARNING]
> `Arrays.asList` 遇上基本类型数组会翻车。`Arrays.asList(new int[]{1,2,3})` 得到的是 `List<int[]>`,长度为 1,里面装着整个数组。因为泛型不接受基本类型,编译器只能把 `int[]` 整体当成一个元素。正确写法是 `Arrays.stream(arr).boxed().toList()`。
## 转成流:stream
`Arrays.stream` 把数组变成流,求和求最值一行搞定:
```java
int[] nums = {3, 1, 4, 2};
System.out.println(Arrays.stream(nums).sum()); // 10
System.out.println(Arrays.stream(nums).max().getAsInt()); // 4
System.out.println(Arrays.stream(nums).average().getAsDouble()); // 2.5
max() 返回的是 OptionalInt,因为空数组没有最大值,得用 getAsInt() 取出来。流的完整用法在后面章节展开,这里先知道有这条捷径。
常用方法速查
| 方法 | 作用 | 注意点 |
|---|---|---|
toString(a) | 转可读字符串 | 多维用 deepToString |
sort(a) | 原地升序排序 | 返回 void,不产生新数组 |
sort(a, from, to) | 排某一段 | 区间左闭右开 |
binarySearch(a, key) | 二分查找 | 必须先排序;没找到返回 -(插入点)-1 |
fill(a, v) | 全部填同一个值 | 也支持区间版 |
setAll(a, f) | 按下标规律填值 | Java 8+ |
copyOf(a, n) | 复制并调整长度 | 变长补默认值,截短丢尾部 |
copyOfRange(a, from, to) | 复制一段 | 区间左闭右开 |
equals(a, b) | 比较内容 | 多维用 deepEquals |
compare(a, b) | 字典序比大小 | Java 9+ |
mismatch(a, b) | 首个差异下标 | Java 9+,全同返回 -1 |
asList(...) | 转 List | 定长视图,不能增删 |
stream(a) | 转流 | 配合 sum/max/average |
hashCode(a) | 按内容算哈希 | 多维用 deepHashCode |
常见疑问
问:Arrays.sort 排 100 万个数会不会很慢?
不会。基本类型的双轴快排平均是 O(n log n),百万级数据在普通机器上通常几十毫秒量级。数据量再大且是多核机器,可以换 Arrays.parallelSort,它会拆成多段并行排。数据量小反而不要用并行版,线程调度的开销比排序本身还大。
问:Arrays 和后面要学的 Collections 是什么关系?
定位一样,服务对象不同。Arrays 操作数组,Collections 操作集合(List、Set 这些)。两者方法名有意起得很像,比如都有 sort、fill、binarySearch,学会一个另一个基本能猜出来。
问:为什么 Arrays.toString 不做成数组自己的方法,写成 a.toString()?
数组是 JVM 内建类型,不是普通的类,没法像写 String 那样给它加方法或重写 toString。这是 Java 早期的设计遗留。工具类是绕开这个限制的折中办法,用起来啰嗦一点,但功能不缺。