程序一旦开始处理“同一种数据的很多份”,单个变量就会迅速失去控制。30 名学生的成绩不能靠 score1、score2 一直写到 score30;一年的每日温度也不该对应 365 条几乎相同的计算语句。数组把这些同类型数据放进一个有顺序的容器,再用索引定位其中某一项。于是,一段循环就能替代成百上千次重复操作。
这一章会沿着一条完整路线推进:先弄清声明、创建、默认值、索引和 length,再建立“数组变量保存引用”的内存心智模型;接着处理遍历、方法传参、复制、查找、排序、对象数组和二维数组;最后比较数组与 ArrayList,并用边界测试把常见错误一次理清。
代码约定:带 public class 的代码块可以单独保存并运行;其余片段用于聚焦当前知识点。除非方法的契约明确允许,否则示例不会把 null 当作一个可用数组。
数组的长度在创建后固定,但数组元素通常可以修改。数组变量保存的是数组对象的引用,不是把所有元素直接塞进变量本身。声明、对象和引用是理解后续别名、传参、浅复制与二维数组的共同起点。
假设我们要计算 6 天的客流总量。只用普通变量时,数据与算法被变量名绑死:
int day1 = 128;
int day2 = 156;
int day3 = 141;
int day4 = 175;
int day5 = 163;
int day6 = 190;
int total = day1 + day2 + day3 + day4 + day5 + day6;当“6 天”变成“365 天”,这套写法几乎无法维护。数组把数据组织成一个编号序列:
int[] visitors = {128, 156, 141, 175, 163, 190};
int total = 0;
for (int value : visitors) {
total += value;
}
System.out.println("总客流:" + total);这里的关键变化不只是代码变短,而是数据数量不再决定算法要写多少遍。只要循环边界来自 visitors.length,同一段求和代码可以处理 6 项、60 项或 6000 项。
数组适合同时满足下面几个条件的数据:
int、double、String 或同一类对象。数组不是“任何数据都往里装”的万能箱子。若人数随时增减,固定长度会让插入、删除和扩容变得麻烦,后面会用 ArrayList 处理这种需求。
以 int[] visitors = new int[6]; 为例:
长度可以是 0。new int[0] 会创建一个合法的空数组,只是它没有任何可访问元素。这个细节会影响平均值、最值和访问首元素等算法的前置条件。
数组最容易混淆的地方,是把“声明一个变量”和“创建一个数组对象”当成同一件事。先拆开看:
int[] scores; // 只声明数组变量
scores = new int[5]; // 创建长度为 5 的数组,再把引用赋给 scores第一行只产生一个类型为 int[] 的变量。若它是局部变量,在赋值前不能读取;第二行的 new int[5] 才真正创建 5 个 int 元素。实际代码通常合并为一行:
int[] scores = new int[5];推荐把方括号写在类型旁边,即 int[] scores。Java 也接受 int scores[],但前一种写法更直接地表达“变量类型是 int[]”,也不容易在一条声明中混出不同维度。
创建数组时,所有元素都会先得到确定的默认值:
int[] counts = new int[3]; // [0, 0, 0]
boolean[] visited = new boolean[3]; // [false, false, false]
String[] names = new String[3]; // [null, null, null]默认值属于数组元素。它不意味着所有局部变量都会自动初始化:
int[] numbers;
// System.out.println(numbers.length); // 编译错误:局部变量还没有赋值
若元素在写代码时已经确定,可以直接列出:
int[] primes = {2, 3, 5, 7, 11};
String[] weekdays = {"周一", "周二", "周三", "周四", "周五"};花括号简写只能紧跟变量声明。对已经存在的变量重新赋值时,要写完整的数组创建表达式:
int[] values;
values = new int[] {3, 1, 4, 1, 5};new int[] {3, 1, 4} 也是一个普通表达式,因此可以直接作为实参或返回值:
printAll(new int[] {10, 20, 30});数组长度表达式必须能得到 int 范围内的整数。0 合法;负数能通过编译,但运行时会触发 NegativeArraySizeException。用户输入还可能大得不合理,所以不能只检查是否为负:
import java.util.Scanner;
public class InputArraySize {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入数据项数量(1~1000):");
if (!scanner.hasNextInt()) {
System.out.println("请输入整数。");
return;
}
上限不是 Java 语法规定,而是程序自己的资源与业务边界。明确上限能避免一次错误输入申请过多内存。
长度为 n 的数组,有效索引永远是 0 到 n - 1。可以把它写成半开区间 [0, n):左端 0 包含,右端 n 不包含。这正是经典循环条件 i < array.length 采用“小于号”的原因。
int[] prices = {12, 18, 25, 9};
System.out.println(prices[0]); // 12
System.out.println(prices[prices.length - 1]); // 9数组访问在运行时检查边界。索引小于 0,或大于等于 length,都会触发 ArrayIndexOutOfBoundsException:
int[] values = new int[4];
// values[-1] = 7; // 越过左边界
// values[4] = 7; // 越过右边界,有效末项是 values[3]
下面的循环多执行了一次:
for (int i = 0; i <= values.length; i++) {
System.out.println(values[i]);
}当 i == values.length 时,循环条件仍为真,但该位置不存在。正确写法是:
for (int i = 0; i < values.length; i++) {
System.out.println(values[i]);
}若循环体同时访问 values[i] 和 values[i + 1],边界又要收紧一格:
boolean hasAdjacentDuplicate = false;
for (int i = 0; i < values.length - 1; i++) {
if (values[i] == values[i + 1]) {
hasAdjacentDuplicate = true;
break;
}
}不要机械背诵循环模板。先写出循环体会访问的最大索引,再反推终止条件。访问 i + 1 时,必须保证 i + 1 < length,也就是 i < length - 1。
int[] empty = new int[0];
System.out.println(empty.length); // 0
// System.out.println(empty[0]); // 越界因此,任何读取 array[0] 的算法都应把“数组非空”写进契约,或显式处理空数组。边界问题不是附属细节,它决定算法对哪些输入有定义。
执行 int[] a = new int[3]; 时,可以把内存关系理解为两部分:变量 a 保存一个引用;堆中的数组对象保存 length 和 3 个元素。变量不是数组对象本身。
这解释了三个看似不同的问题。
int[] data = null;
// System.out.println(data.length); // NullPointerException
// System.out.println(data[0]); // NullPointerExceptionnull 与空数组不同。空数组可以读取 length,只是结果为 0;null 连数组对象都没有。
int[] original = {10, 20, 30};
int[] alias = original;
alias[1] = 99;
System.out.println(original[1]); // 99
System.out.println(alias == original); // truealias = original 之后,两者指向同一个数组。alias[1] 与 original[1] 是同一个元素位置的两种访问路径,这种关系叫别名。
int[] left = {1, 2, 3};
int[] right = {1, 2, 3};
System.out.println(left == right); // false
System.out.println(java.util.Arrays.equals(left, right)); // true== 判断两个引用是否指向同一对象;Arrays.equals 才逐项比较一维数组内容。二维数组通常用 Arrays.deepEquals 比较嵌套内容。

看到数组赋值时,先问“这是复制引用,还是创建新数组并复制元素?”如果代码只有 b = a,答案一定是复制引用。修改任意一个别名所指向的元素,其他别名随后都会观察到变化。
数组常见的两种遍历方式各有清晰边界。
当你需要修改元素、倒序、跳步、处理子区间或比较相邻项时,使用索引:
int[] scores = {76, 88, 91, 64};
for (int i = 0; i < scores.length; i++) {
scores[i] += 2;
System.out.printf("第 %d 项调整为 %d%n", i, scores[i]);
}倒序遍历从末项开始:
for (int i = scores.length - 1; i >= 0; i--) {
System.out.println(scores[i]);
}只需要按顺序读取全部元素时,增强 for 更紧凑:
int total = 0;
for (int score : scores) {
total += score;
}循环变量 score 每次得到当前元素值的副本。给循环变量重新赋值,不会改回基本类型数组:
for (int score : scores) {
score = 0; // 只改局部循环变量
}对象数组还要多想一层。增强 for 复制的是对象引用:
class Student {
String name;
}
Student[] students = {new Student(), new Student()};
for (Student student : students) {
student.name = "待命名"; // 修改了对象内部状态,数组仍指向这些对象
student = new Student(); // 只改循环变量,不会替换数组元素
}可以用一句话选择循环:算法关心“在哪个位置”,就保留索引;只关心“每个值是什么”,就考虑增强 for。
数组作为参数时,Java 复制的是引用这个值。形参和实参变量彼此独立,但它们起初指向同一个数组,所以方法可以通过形参修改原数组元素。
public class ArrayParameterDemo {
public static void main(String[] args) {
int[] values = {2, 4, 6};
doubleAll(values);
System.out.println(java.util.Arrays.toString(values)); // [4, 8, 12]
replace(values);
System.out.println(java.util.Arrays.toString(values)); // 仍是 [4, 8, 12]
}
doubleAll 修改共享数组中的元素,所以调用者看得到;replace 只给形参重新赋值,调用者变量 values 没有改变。这两种行为完全符合按值传递。
如果方法承诺“不修改输入”,就创建并返回新数组:
static int[] doubledCopy(int[] source) {
if (source == null) {
throw new IllegalArgumentException("source 不能为 null");
}
int[] result = new int[source.length];
for (int i = 0; i < source.length; i++) {
result[i] = source[i] * 2;
方法名、文档和测试应说明它究竟是“原地修改”还是“返回副本”。模糊的可变性契约比语法错误更难排查。
static double average(double... values) {
if (values.length == 0) {
throw new IllegalArgumentException("至少需要一个数");
}
double sum = 0;
for (double value : values) {
sum += value;
}
return sum / values.length;
}调用者既可以传多个独立值,也可以传一个现成数组:
double a = average(10, 20, 30);
double b = average(new double[] {10, 20, 30});可变参数必须位于形参列表最后,一个方法最多只能有一个。还要注意“允许传 0 个参数”不等于业务上“0 个参数有合理结果”,平均值方法仍应自行定义空输入策略。
数组复制至少有三种完全不同的结果:
int[] source = {10, 20, 30};
int[] alias = source; // 同一个数组
int[] clone = source.clone(); // 新数组,复制全部基本值
int[] longer = java.util.Arrays.copyOf(source, 5); // [10, 20, 30, 0, 0]对基本类型一维数组,clone() 或 Arrays.copyOf 会得到元素彼此独立的新数组。对对象数组,它们只复制每个位置保存的引用,不复制引用所指向的对象:
Student[] first = {new Student(), new Student()};
Student[] second = first.clone();
System.out.println(first == second); // false,外层数组不同
System.out.println(first[0] == second[0]); // true,第一个 Student 仍共享这叫浅复制。若必须让对象内部状态也互不影响,需要根据对象的复制契约逐个创建新对象。
import java.util.Arrays;
int[] data = {5, 8, 13, 21, 34};
int[] sameLength = data.clone();
int[] firstThree = Arrays.copyOf(data, 3); // [5, 8, 13]
int[] middle = Arrays.copyOfRange(data, 1, 4); // [8, 13, 21]copyOfRange 使用半开区间 [from, to)。这与循环和许多 Java API 的区间习惯一致:长度正好是 to - from。
已准备好目标数组,或只想搬运某一段时,可以用 System.arraycopy:
int[] src = {10, 20, 30, 40, 50};
int[] dest = new int[7];
System.arraycopy(src, 1, dest, 2, 3);
// 从 src[1] 开始复制 3 项,写到 dest[2] 开始的位置
// dest 为 [0, 0, 20, 30, 40, 0, 0]参数顺序是 源数组、源起点、目标数组、目标起点、数量。源和目标可以是同一个数组;即使区间重叠,复制结果也按“先保存到临时区域再写回”的语义处理,适合整体右移或左移:
int[] queue = {10, 20, 30, 40, 0};
System.arraycopy(queue, 1, queue, 0, 3);
// [20, 30, 40, 40, 0]
queue[3] = 0;
// [20, 30, 40, 0, 0]matrix.clone() 只复制外层数组,行数组仍然共享。要复制每一行:
static int[][] copyMatrix(int[][] source) {
int[][] result = new int[source.length][];
for (int row = 0; row < source.length; row++) {
result[row] = source[row] == null ? null : source[row].clone();
}
return result;
}这能让二维基本类型数组的行和元素独立;若行中保存的仍是对象引用,还要继续根据需求复制对象本身。
求和、计数、平均值、最值和反转看起来简单,却最能暴露边界设计。一个可靠算法不仅给出循环,还说明空数组、null 和修改输入的策略。
static int sum(int[] values) {
if (values == null) {
throw new IllegalArgumentException("values 不能为 null");
}
int total = 0;
for (int value : values) {
total += value;
}
return total;
}
static int countAtLeast(
求和对空数组有自然结果 0;平均值没有。平均值方法必须拒绝空数组或返回一种能表达“无结果”的类型:
static double average(int[] values) {
if (values == null || values.length == 0) {
throw new IllegalArgumentException("至少需要一个元素");
}
long total = 0; // 比 int 更不容易在累加时溢出
for (int value : values) {
total += value;
}
return (double
若数组全是负数,int max = 0 会得到一个数组中不存在的错误答案。非空数组可以用首元素初始化:
static int max(int[] values) {
if (values == null || values.length == 0) {
throw new IllegalArgumentException("至少需要一个元素");
}
int best = values[0];
for (int i = 1; i < values.length; i++) {
if (values[i] >
static void reverse(int[] values) {
for (int left = 0, right = values.length - 1;
left < right;
left++, right--) {
int temp = values[left];
values[left] = values[right];
values[right] = temp;
}
}循环不变式是:每轮开始时,left 左边和 right 右边都已经放到最终位置。两指针相遇或交错时,所有需要交换的位置都处理完毕。
有时数组容量是 100,但目前只存了 7 项。此时 data.length 是容量,size 才是有效元素数:
int[] data = new int[100];
int size = 0;
data[size] = 42;
size++;
for (int i = 0; i < size; i++) {
System.out.println(data[i]);
}只遍历到 data.length 不一定越界,却会把未使用位置的默认值误当成真实数据。这类逻辑错误往往比异常更隐蔽。
如果暂时不能使用 ArrayList,可以在“容量不足”时创建更大的数组,再把有效区复制过去。扩容改变的是数组变量所引用的对象,原数组自身的 length 从未变化:
import java.util.Arrays;
int[] data = new int[4];
int size = 0;
int value = 42;
if (size == data.length) {
int newCapacity = data.length == 0 ? 1 : data.length * 2;
data = Arrays.copyOf(data, newCapacity);
这里一次扩容要复制 size 个元素,成本是 O(n);但容量按倍数增长时,不必每次追加都复制,连续多次尾部追加的平均成本仍可保持在常数级。不要把“摊销常数时间”误解为“每一次追加都绝对是 O(1)”。
从中间删除也不会让数组自动缩短,而是把后续有效元素左移,并把 size 减 1:
int removeAt = 1;
if (removeAt < 0 || removeAt >= size) {
throw new IndexOutOfBoundsException("非法删除索引:" + removeAt);
}
int moveCount = size - removeAt - 1;
System.arraycopy(data, removeAt + 1, data, removeAt, moveCount);
size--;
data[size] = 0若元素类型是对象,清理末尾位置时应写 data[size] = null,这样不再使用的对象不会仅仅因为数组槽位还保存引用而继续可达。实际项目通常把 data 与 size 封装进一个类;否则调用代码很容易忘记同步维护二者。
查找算法的选择取决于数据是否已经排序,以及后续会查多少次。
static int indexOf(int[] values, int target) {
for (int i = 0; i < values.length; i++) {
if (values[i] == target) {
return i;
}
}
return -1;
}最坏情况下要检查全部 n 个元素,时间复杂度是 O(n)。对未排序数据、数据量较小或只查一次的场景,它通常已经足够清楚。
二分查找的前置条件是搜索区间已经按同一规则排序:
static int binarySearch(int[] values, int target) {
int low = 0;
int high = values.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (values[mid] == target) {
return mid;
}
每轮最多保留原区间的一半,时间复杂度为 O(log n)。但若数据尚未排序,为一次搜索先做 O(n log n) 的排序不一定划算;而且排序会改变元素位置,可能破坏“索引代表原始顺序”的含义。

import java.util.Arrays;
int[] values = {42, 7, 19, 7, 31};
int[] sorted = values.clone();
Arrays.sort(sorted);
System.out.println(Arrays.toString(sorted)); // [7, 7, 19, 31, 42]
int result = Arrays.binarySearch(sorted, 19);
if (result
Arrays.sort 会原地修改数组,所以示例先复制。Arrays.binarySearch 与我们手写方法的“未找到”返回值不同:
>= 0:已经找到,返回某个匹配元素的索引;有重复值时不保证是哪一个。< 0:未找到。插入点为 -result - 1,表示把目标插到哪里仍能保持有序。int[] sorted = {10, 20, 40, 50};
int result = Arrays.binarySearch(sorted, 30); // 负数
int insertionPoint = -result - 1; // 2在未排序数组上调用 Arrays.binarySearch,结果没有定义,不能把“这次碰巧找到了”当成正确性证明。
插入排序把左侧看成已排序区,每次把下一项插到正确位置:
static void insertionSort(int[] values) {
for (int i = 1; i < values.length; i++) {
int current = values[i];
int j = i - 1;
while (j >= 0 && values[j] > current) {
values[j + 1] = values[j];
插入排序特别适合观察“已排序区”如何逐步扩大:接近有序时,内层移动次数很少;逆序输入时,移动次数会增长到平方级。
选择排序采用另一种不变式:每一轮从未排序区找到最小值,把它放到当前起点。它同样是 O(n²),但每轮最多交换一次:
static void selectionSort(int[] values) {
for (int start = 0; start < values.length - 1; start++) {
int minIndex = start;
for (int i = start + 1; i < values.length; i++) {
if (values[i] < values[minIndex]) {
minIndex =
这两段代码适合学习循环边界与排序不变式,不代表生产代码应重新发明排序库。大量数据通常直接使用经过充分测试和优化的 Arrays.sort。
“随机挑两个位置交换很多次”没有清晰的均匀性保证。Fisher–Yates 洗牌从末尾向前处理:当位置 i 待定时,只在闭区间 [0, i] 随机选一个位置与它交换:
import java.util.concurrent.ThreadLocalRandom;
static void shuffle(int[] values) {
for (int i = values.length - 1; i > 0; i--) {
int randomIndex = ThreadLocalRandom.current().nextInt(i + 1);
int temp = values[i];
values[i] = values[randomIndex];
values[randomIndex]
nextInt(i + 1) 的上界不包含在结果中,所以它恰好产生 0 到 i。每轮固定一个末尾位置,总时间为 O(n)。若需求涉及密码、令牌或安全抽签,应根据安全模型使用合适的安全随机数生成器;普通模拟和界面随机化则不必混用这类高成本要求。
数值可以直接比较大小,对字符串和业务对象则必须有一致的顺序规则。String 的自然顺序按字符的 Unicode 次序逐项比较,它不等同于所有语言环境下的字典顺序:
String[] words = {"banana", "Apple", "apple"};
Arrays.sort(words); // 自然顺序
Arrays.sort(words, String.CASE_INSENSITIVE_ORDER); // 忽略大小写的规则自定义对象可以实现 Comparable,也可以在排序时传入 Comparator。无论选择哪种方式,后续二分查找必须使用兼容的比较规则;不能按分数排序后却按姓名做二分查找。面向用户的多语言字典排序还应明确语言环境,不能默认把 Unicode 次序当成本地化规则。
Arrays 还提供 fill、copyOf、copyOfRange、equals、toString、deepEquals 和 deepToString 等常用工具:
Arrays.toString(matrix) 对二维数组只会显示每一行数组的对象标识;要看到行内元素,应使用 Arrays.deepToString(matrix)。同理,一维与嵌套结构的比较方法不能混用。
String[] 最常见的入口就是 main 的参数:
public static void main(String[] args) {
System.out.println("参数数量:" + args.length);
for (int i = 0; i < args.length; i++) {
System.out.printf("args[%d] = %s%n", i, args[i]);
}
}没有命令行参数时,args 通常是长度为 0 的数组。对任意对象数组,new Type[n] 只创建引用槽位,不会自动调用 Type 的构造器。
class Student {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
}
Student[] students = new Student[2];
System.out.println(students[0]); // null
students[0
若忘了给某个槽位放入对象,访问 students[i].name 会因为该槽位是 null 而触发 NullPointerException。
String[] names = {"小林", "阿澄"};
int[] scores = {91, 86};这种写法依赖“相同索引表示同一个学生”的隐含约定。任何一次插入、删除或排序都必须同步操作两个数组。对象数组把相关字段放进同一个对象,关系更稳定:
Student[] students = {
new Student("小林", 91),
new Student("阿澄", 86)
};两个平行数组也常被用来表达键值对应,例如一个数组保存城市代码,另一个数组保存城市名称。更稳妥的做法仍是把一对数据组成对象:
class CityEntry {
final String code;
final String name;
CityEntry(String code, String name) {
this.code = code;
this.name = name;
}
}
static String findCityName(CityEntry[] entries, String targetCode) {
for (CityEntry entry : entries) {
if (entry != null && entry.code.
这仍然是线性查找,最坏要检查全部元素。数据很少、创建后固定时,它简单直接;键很多、频繁增删或大量按键查询时,应进一步考虑 Map。关键不是把所有关联都硬塞进数组,而是看清“位置编号”和“业务键”是两种不同的访问方式。
引用类型数组允许协变:String[] 可以赋给 Object[]。但数组对象仍记得自己的真实元素类型,因此错误写入会在运行时被拒绝:
Object[] objects = new String[2];
objects[0] = "合法";
// objects[1] = 42; // 运行时抛出 ArrayStoreException编译器只看到 objects 的静态类型是 Object[],而运行时数组真实类型是 String[]。这个例子说明:宽泛的数组类型会把一部分类型问题推迟到运行时。API 设计中应尽量保留准确元素类型,不要为“通用”随意改成 Object[]。
表格、棋盘、图像像素和按“城市 × 日期”组织的数据,都可以用二维数组表达:
int[][] temperatures = new int[3][4];
temperatures[1][2] = 27;第一维选择行,第二维选择该行中的列。temperatures.length 是行数,temperatures[row].length 是指定行的列数。
int[][] matrix = {
{3, 5, 8},
{1, 4, 9}
};
for (int row = 0; row < matrix.length; row++) {
for (int col = 0; col < matrix[row].length; col++) {
System.out.printf
内层边界应写 matrix[row].length,不要想当然地使用 matrix[0].length。因为 Java 的二维数组实际是外层数组保存多个行数组引用,各行可以长度不同,甚至为 null。

int[][] triangle = new int[4][];
for (int row = 0; row < triangle.length; row++) {
triangle[row] = new int[row + 1];
}
for (int row = 0; row < triangle.length; row++) {
for (int col
最终每行长度依次是 1、2、3、4。这不是特殊语法,而是逐个给外层数组的元素放入不同长度的 int[]。
矩形矩阵的转置会把 rows × cols 变成 cols × rows:
static int[][] transpose(int[][] source) {
if (source == null || source.length == 0) {
return new int[0][0];
}
int columns = source[0].length;
for (int[] row : source) {
if (row == null ||
这里先验证“矩形”前置条件,再创建交换行列数的新数组。若需求允许不规则输入,转置的含义就需要重新定义,不能假装所有行等长。
若下一轮每个单元格都依赖上一轮邻居状态,就不应边读边覆盖同一数组。否则后面的计算会混用旧值和新值。常见模式是:
boolean[][] next = new boolean[current.length][];
for (int row = 0; row < current.length; row++) {
next[row] = new boolean[current[row].length];
// 只读取 current,计算并写入 next
}
current = next;这和一维数组的“输入不变、返回新结果”是同一思想,只是多了一层结构。细胞自动机就是典型场景:一个格子的下一状态由周围邻居决定。数邻居时,行列边界都必须分别验证:
static int countNeighbors(boolean[][] cells, int row, int col) {
int count = 0;
for (int rowOffset = -1; rowOffset <= 1; rowOffset++) {
for (int colOffset = -1; colOffset <= 1; colOffset++) {
if (rowOffset
这里把数组外视为不存在的格子;另一种需求也可能把上下、左右首尾相接。两种规则都可以实现,但必须在契约中选定一种。棋盘程序还要先决定每格保存什么:0/1/2 等状态码、枚举,或棋子对象引用。状态码紧凑,对象表达力更强;无论采用哪种表示,都应固定 board[row][column] 的顺序,避免在不同方法里把行列颠倒。
数组长度固定,ArrayList 则是可调整大小的列表实现。两者都按 0 开始编号,但 API 不同:
import java.util.ArrayList;
ArrayList<String> tasks = new ArrayList<>();
tasks.add("读取数据");
tasks.add("生成报告");
tasks.add(1, "校验数据");
System.out.println(tasks.get(0));
tasks.set(0, "导入数据");
tasks.remove(

不能写 ArrayList<int>,应写 ArrayList<Integer>:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(42); // int 自动装箱为 Integer
int first = numbers.get(0); // Integer 自动拆箱为 int列表允许保存 null。若把 null 自动拆箱为 int,会触发 NullPointerException,所以装箱并没有取消空值风险。
还要分清 remove 的重载:
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(1); // 删除索引 1 的元素,也就是 20
numbers.remove(Integer.valueOf(30)); // 删除值 30ArrayList 内部通常使用数组保存元素。size() 是当前逻辑元素数;内部容量至少不小于 size,不足时会自动扩容。具体扩容比例不是调用者可以依赖的契约。尾部追加具有摊销常数时间,但中间插入、删除和查找仍可能需要线性时间。
String[] array = {"A", "B", "C"};
ArrayList<String> list = new ArrayList<>(java.util.Arrays.asList(array));
String[] copiedBack = list.toArray(String[]::new);Arrays.asList(array) 返回的是由原数组支撑的固定大小列表,不是可自由增删的 ArrayList。若要增删,像示例一样再交给 new ArrayList<>(...) 创建动态列表。
选择原则可以压缩成两句:数量固定、重视基本类型和紧凑索引访问,用数组;数量会变化、需要现成增删 API,用 ArrayList。不要因为“更高级”就默认选择后者,也不要为了省一个类型名而手写整套扩容逻辑。
数组代码不能只拿“普通的 5 个正数”跑一次。每个方法都应该从契约反推出测试集合:
看到 ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5,先不要只在异常行改成 length - 1。按下面顺序检查:
写出实际数组长度和合法半开区间。例如长度 5 的合法区间是 [0, 5),最后索引是 4。
展开异常那一轮所有索引表达式。若循环体访问 i + 1,就同时计算 i 和 i + 1,不要只看循环变量。
区分容量和有效元素数。部分填充数组即使没有越过 length,也可能读取了 size 之后的无效区域。
加入空数组、单元素和刚好位于左右边界的测试,再验证修复没有遗漏首项或末项。
import java.util.Arrays;
public class ArrayAlgorithmTest {
public static void main(String[] args) {
checkReverse(new int[] {}, new int[] {});
checkReverse(new int[] {7}, new int[] {7});
checkReverse(new int[] {1,
测试代码也应避免只打印结果后靠肉眼判断。把期望值写进断言,失败时同时输出期望与实际,错误才容易复现。
下面的练习把输入验证、数组方法、复制、排序、查找和测试连成一条完整任务。目标不是把所有代码塞进 main,而是让每个方法都拥有明确契约。
需求如下:
int[],每个分数必须在 0 到 100 之间。import java.util.Arrays;
public class ScoreAnalyzer {
public static void main(String[] args) {
int[] scores = {86, 72, 95, 61, 86, 78};
int[] originalSnapshot = scores.clone();
printReport(scores, 60, 86
这份实现刻意保留了几个可以继续优化的方向:把统计结果封装为对象而不是直接打印;定义重复分数时要找首个还是任意一个;为 null、空数组、边界分数、全相同分数和非法分数补充自动测试;再把多次查询优化为“只排序一次,复用排序副本”。
如果你能清楚回答下面四个问题,这一章的核心就已经串起来了:哪个变量保存引用?哪个操作会修改原数组?每个循环访问的索引范围是什么?空数组在当前方法中有没有定义?