数组
Java 初级 · 基础语法
数组是定长、同类型元素的连续容器,是 Java 最基础的数据结构。它看似简单,但「数组在 JVM 里是对象」「多维数组是数组的数组」「length 是字段而非方法」这些细节,是后面学集合、理解内存布局的关键。本节把数组的内存本质和常用算法讲透。
一、数组在 JVM 里到底是什么
一个反直觉的事实:Java 的数组是对象。它和 new Object() 一样在堆里分配,只是它是一种「特殊对象」——JVM 为每种数组类型生成对应的类(如 int[] 对应 [I,String[] 对应 [Ljava/lang/String;),有专门的字节码指令操作:
newarray:创建基本类型数组(int[]、byte[]等)。anewarray:创建引用类型数组(String[]、自定义类数组)。multianewarray:创建多维数组。arraylength:读取长度。iaload/aastore等:按类型读取/存储元素(i=int,a=reference,b=byte……)。
int[] a = new int[3]; // 编译为 newarray + arraylength 初始化为 0
int[] b = {1, 2, 3}; // 语法糖:编译器生成 new + 逐元素 aastore内存布局:连续 + 存值/存地址
int[]:堆里是一块连续的 32 位整数,每个格子直接存值。String[]:堆里是一块连续的引用槽,每个格子存的是「指向堆中 String 对象的地址」。
正因为元素连续,arr[i] 的访问是 O(1):JVM 用「首地址 + i × 元素大小」直接定位,无需遍历。这也是数组随机访问快的根本原因。
数组 vs 集合的第一性差异
数组长度在 new 时就固定(length 是只读字段,不是方法),不能再伸缩;而 ArrayList 内部也是数组,但它用「换更大的数组 + 拷贝」来模拟可变长度(详见集合框架一节)。需要变长容器时优先用集合,需要定长/高性能批量传递时用数组。
二、声明、创建与遍历
int[] a = new int[3]; // 默认 0,0,0
int[] b = {1, 2, 3}; // 直接初始化(语法糖)
String[] names = new String[]{"Tom", "Lucy"};
// 传统 for:需要索引时用
for (int i = 0; i < b.length; i++) {
System.out.println(b[i]);
}
// 增强 for:只读遍历更简洁(底层走数组索引,不是迭代器)
for (int v : b) {
System.out.println(v);
}length 是数组对象的字段(所以是 arr.length,不加括号);String 是方法 .length();集合是 .size()——三者不要混。
三、多维数组:数组的数组
int[][] 不是「整齐的二维块」,而是「数组的数组」:外层数组的每个元素,是指向一个内层 int[] 的引用。因此每行可以长度不同:
int[][] matrix = {
{1, 2, 3},
{4, 5} // 第二行只有 2 个元素,完全合法
};
System.out.println(matrix[1][2]); // 越界!这一行没有下标 2new int[3][] 只创建了外层 3 个引用,每行需单独 new int[...] 才真正有数据。这解释了为什么遍历二维数组要先判每行非空。
四、Arrays 工具类与常用算法
java.util.Arrays 提供了排序、查找、填充、转字符串等。理解两个高频算法,能避免「只会调 API 不懂原理」:
排序:Dual-Pivot Quicksort
Arrays.sort(int[]) 在 JDK 7+ 使用双轴快速排序(Dual-Pivot Quicksort),平均 O(n log n),比经典快排更少的元素交换。对象数组 sort(Object[]) 则用 TimSort(归并+插入的混合)。注意:排序是**原地(in-place)**修改原数组。
int[] arr = {5, 3, 8, 1};
Arrays.sort(arr); // 升序,原数组被改为 [1,3,5,8]
System.out.println(Arrays.toString(arr));// [1, 3, 5, 8]二分查找:必须先排序
Arrays.binarySearch 前提是数组已升序,否则结果未定义:
int idx = Arrays.binarySearch(arr, 5); // 返回下标 2;找不到返回「负数插入点」二分查找每次取中点比较,把搜索区间砍半,O(log n)。若数组无序,中点逻辑完全失效,可能查不到存在的元素。
冒泡排序(理解「交换」思想)
虽生产用 Arrays.sort,但手写一遍能体会「相邻比较 + 交换」:
for (int i = 0; i < arr.length - 1; i++) { // 每轮把最大者推到末尾
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int t = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = t;
}
}
}克隆(clone)的语义
int[] c = arr.clone(); 创建新数组并拷贝元素。对基本类型数组,拷贝的是值(两数组独立);对引用类型数组,clone 只拷贝「地址」,新旧数组仍指向同一批对象——改对象内容会互相影响(浅拷贝)。需要深度独立要逐元素 new。
五、常见误区与注意点
WARNING
- 数组越界:访问
arr[arr.length]抛ArrayIndexOutOfBoundsException。循环条件常错写成<= length。 - 混淆
length/length()/size():数组是属性.length,String是方法.length(),集合是.size()。 - 数组是引用类型:
int[] x = y;只复制引用,两变量指向同一数组,改一个另一个也变。 - 二维数组每行长度可不同:
matrix[i].length可能不一样,遍历前先确认行非空。 - 把数组当方法「返回值」:数组可返回,但若返回内部数组引用,外部能改你的私有数据——必要时
clone或Arrays.copyOf防御性拷贝。
六、最佳实践
- 遍历用 for-each;需要索引或反向遍历才用传统
for。 - 排序、查找优先用
Arrays工具类,别手写轮子(除非为了学算法)。 - 需要动态增删用
ArrayList,而非反复new新数组再System.arraycopy。 - 返回数组给外部前,考虑
Arrays.copyOf做防御性拷贝,防止内部状态被外部篡改。 - 多维数组先判每行长度,避免
NullPointerException/ 越界。
📚 官方文档与参考资料
- Java SE 21 文档:https://docs.oracle.com/en/java/javase/21/
- Java 语言规范 · 数组:https://docs.oracle.com/javase/specs/jls/se21/html/jls-10.html
java.util.ArraysAPI:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Arrays.html- OpenJDK 项目:https://openjdk.org/
✏️ 练习与自测
- 创建一个长度为 10 的
int数组,存入 1~10,用增强 for 打印;再用javap -c观察arraylength指令。 - 写一个方法
double avg(int[] arr)求平均值,并在方法里演示「返回前用Arrays.copyOf防御性拷贝」的必要性(提示:若直接return arr内部字段会有什么问题)。 - 用
Arrays.sort对一个String数组排序,观察字符串按字典序(实际按compareTo,即 UTF-16 码元顺序)排列;解释为何Arrays.binarySearch前必须先排序。 - 构造一个「每行长度不同」的二维数组,遍历时演示
NullPointerException的触发与规避。
本节小结
Java 数组是堆里的特殊对象([I、[Ljava/lang/String; 等),由 newarray/anewarray 创建、arraylength 取长、元素连续存储故随机访问 O(1)。多维数组是「数组的数组」,每行可不等长。length 是字段(区别于 String.length()、集合 size())。Arrays.sort 用双轴快排/sort 对象用 TimSort,二分查找前必须有序。clone 对引用数组是浅拷贝。需要变长时用集合,定长高性能时用数组。