开篇:一个变量存不下的痛
学完前面的变量和循环,你可能已经可以写点小程序了。但遇到这种场景怎么办?
一个班 50 个学生,要算平均分、找最高分、排个序------难道要声明 score1, score2, ..., score50 五十个变量?
这显然不现实。
这时你需要一个容器,它能把多个相同类型的数据 打包管理,通过一个名字 + 编号快速访问------这就是数组(Array)。数组是 Java 最基础、也是最重要的数据结构之一,后面要学的集合、字符串、算法题几乎都离不开它。
一、数组是什么?
用大白话讲:数组就是一排编号的格子。每个格子装一个相同类型的数据,格子的编号叫下标(索引),从 0 开始数。
1.1 数组的四大特点
| 特点 | 说明 |
|---|---|
| 长度固定 | 数组一旦创建,大小就不能改变(想扩容必须新建数组复制过去) |
| 类型相同 | 所有元素必须是同一种数据类型,不能 int 和 String 混装 |
| 下标从0开始 | 第一个元素下标是0,最后一个是 length - 1 |
| 引用类型 | 数组变量存的是"地址",不是数据本身(后面会讲) |
1.2 下标和长度
长度为 n 的数组,合法下标范围是 0 ~ n-1。超出范围会抛出 ArrayIndexOutOfBoundsException(下标越界异常),这是新手最常踩的坑之一,没有之一。
length 是数组自带的属性(注意是属性不是方法,不带括号),用来获取数组长度。千万别和字符串的 length() 方法搞混!
⚠️ length 还是 length()?
- 数组的长度是
arr.length(属性,无括号)- 字符串的长度是
str.length()(方法,有括号)- 集合的大小是
list.size()(方法,有括号)三个都不一样,背下来!
二、一维数组的声明与初始化
2.1 声明数组
数组声明只是告诉编译器"这是一个什么类型的数组",并没有真正分配内存空间,不能使用。两种语法:
java
// 格式1(推荐):类型[] 数组名;
int[] scores;
String[] names;
double[] prices;
// 格式2(C语言风格,不推荐)
int scores[]; // 可读性差,新项目不要用
💡 为什么推荐
int[] arr这种写法?这种写法把类型
int[]写在一起,一眼就能看出"这是一个 int 数组",语义清晰。int arr[]是为了兼容 C/C++ 程序员的习惯保留的,Java 官方更推荐前者。
2.2 动态初始化:先定长度,再赋值
动态初始化是指创建数组时指定长度,JVM 先给所有元素赋默认值,后续你再一个一个赋值。适合"知道要存多少个,但暂时不知道具体值"的场景。
java
// 声明 + 创建一体化
int[] scores = new int[5]; // 创建长度为5的int数组,每个格子默认值0
// 通过下标手动赋值
scores[0] = 88;
scores[1] = 92;
scores[2] = 75;
scores[3] = 100;
scores[4] = 60;
System.out.println(scores[0]); // 88
System.out.println(scores.length); // 5
动态初始化时,各类型的默认值如下:
| 数据类型 | 默认值 |
|---|---|
| byte / short / int | 0 |
| long | 0L |
| float / double | 0.0F / 0.0D |
| char | '\u0000'(空字符,打印出来什么都看不到) |
| boolean | false |
| 引用类型(String、数组、对象等) | null |
2.3 静态初始化:创建时直接赋值
静态初始化是在创建数组的同时把元素都列出来,由 JVM 自动计算长度。适合"一开始就知道所有元素值"的场景。
java
// 完整写法(标准)
int[] arr1 = new int[]{1, 2, 3, 4, 5};
// 简化写法(推荐,声明+初始化一体时可用)
int[] arr2 = {10, 20, 30, 40};
String[] names = {"张三", "李四", "王五"};
⚠️ 静态初始化的 3 个坑
int[] arr = new int[3]{1,2,3};→ 编译报错!静态初始化不能同时指定长度int[] arr; arr = {1,2,3};→ 编译报错!简化写法必须在声明时直接用,不能先声明再赋值。可以写成arr = new int[]{1,2,3};int[] arr = new int[-3];→ 运行时NegativeArraySizeException!长度不能为负数
三、数组的访问与遍历
3.1 通过下标访问
数组元素通过 数组名[下标] 的方式访问,既能读也能写:
java
int[] arr = {10, 20, 30, 40, 50};
System.out.println(arr[0]); // 读:10(第一个元素)
System.out.println(arr[4]); // 读:50(最后一个元素)
arr[2] = 99; // 写:把下标2的元素改成99
System.out.println(arr[2]); // 99
3.2 普通 for 循环遍历(推荐掌握)
下标从 0 到 arr.length - 1,配合 for 循环就能把所有元素访问一遍。需要用到下标时必须用这种方式。
java
int[] arr = {10, 20, 30, 40, 50};
for (int i = 0; i < arr.length; i++) {
System.out.println("第" + (i+1) + "个元素:" + arr[i]);
}
💡 循环条件为什么写
i < arr.length而不是i <= arr.length-1?两种写法在逻辑上等价,但
i < arr.length更简洁、更常用,也是 Java 圈的主流写法,记住这一种就够了。
3.3 增强 for 循环(for-each)
JDK 5 开始提供了一种更简洁的遍历方式,不需要操作下标,直接"把每个元素依次拿出来":
java
int[] arr = {10, 20, 30, 40, 50};
for (int num : arr) { // 读作"对于arr中的每一个num"
System.out.println(num);
}
| 遍历方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通for循环 | 能拿到下标i,可修改元素 | 写法稍繁琐 | 需要下标、需要修改元素、需要反向遍历 |
| 增强for-each | 写法简洁,不会越界 | 拿不到下标,不能修改元素(操作的是副本) | 只需要遍历读取,不关心位置 |
⚠️ for-each 不能修改原数组?
是的!for-each 循环里的变量是元素的"副本",你在循环里改
num = 99不会影响原数组。需要修改元素时,老老实实使用带下标的普通 for 循环。
四、数组的两大经典异常
数组相关的异常,99% 就是下面这两个,一定要眼熟:
4.1 ArrayIndexOutOfBoundsException(下标越界)
访问了不存在的下标。最常见的写错是把循环条件写成 i <= arr.length,导致访问到 arr[length](最大合法下标是 length-1)。
java
int[] arr = {1, 2, 3};
System.out.println(arr[3]); // ❌ 越界!最大下标是2
System.out.println(arr[-1]); // ❌ 负下标也是越界
// 经典越界写法:
for (int i = 0; i <= arr.length; i++) { // ❌ 多了个等号
System.out.println(arr[i]);
}
4.2 NullPointerException(空指针异常)
数组变量是 null(没有指向任何数组对象),却想访问它的元素或属性时抛出。
java
int[] arr = null; // 声明了但没创建
System.out.println(arr[0]); // ❌ 空指针
System.out.println(arr.length); // ❌ 空指针
int[] arr2;
System.out.println(arr2[0]); // ❌ 编译都过不去:可能尚未初始化
💡 怎么避免?
声明数组后尽快初始化;使用数组前先判空:
if (arr != null && arr.length > 0) { ... }
五、一维数组常用操作
掌握下面这几个操作,日常遇到的数组题基本就不怕了。
5.1 求和与平均值
java
int[] scores = {88, 92, 75, 100, 60};
int sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
}
double avg = (double) sum / scores.length; // 注意强转double,否则整数除法会截断
System.out.println("总分:" + sum + ",平均分:" + avg);
// 总分:415,平均分:83.0
⚠️ 整数除法的坑
sum / scores.length两个 int 相除结果还是 int,小数部分直接丢弃!想得到小数,必须把其中一个转成 double,如(double)sum / scores.length。
5.2 求最大值 / 最小值
思路:先假设第一个元素是最值,然后遍历剩下的元素,遇到更大/更小的就替换。
java
int[] arr = {88, 92, 75, 100, 60};
int max = arr[0]; // 假设第一个是最大值
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
if (arr[i] < min) min = arr[i];
}
System.out.println("最大值:" + max + ",最小值:" + min); // 100,60
5.3 数组反转(双指针法)
思路:用两个指针,一个指头一个指尾,交换后相向移动,直到相遇。
java
int[] arr = {1, 2, 3, 4, 5};
for (int left = 0, right = arr.length - 1; left < right; left++, right--) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
// 反转后:{5, 4, 3, 2, 1}
5.4 线性查找
在数组中找某个元素第一次出现的位置,从头到尾扫一遍:
java
int[] arr = {88, 92, 75, 100, 60, 92};
int target = 92;
int index = -1; // 用-1表示没找到
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
index = i;
break; // 找到就停,只找第一个
}
}
System.out.println(index == -1 ? "没找到" : "下标是:" + index); // 1
六、数组的拷贝与扩容
Java 数组长度一旦创建就不能改,想实现"扩容"只能新建一个更大的数组,把旧数据复制过去。JDK 提供了两种常用方式:
6.1 System.arraycopy()(最底层最高效)
java
int[] oldArr = {1, 2, 3, 4, 5};
int[] newArr = new int[oldArr.length * 2]; // 新数组长度翻倍
// 参数:源数组, 源起始位置, 目标数组, 目标起始位置, 复制长度
System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
// newArr: {1,2,3,4,5,0,0,0,0,0}
6.2 Arrays.copyOf()(最方便)
Arrays.copyOf() 内部其实就是调用 System.arraycopy(),但写法更简洁,一步到位:
java
import java.util.Arrays;
int[] oldArr = {1, 2, 3, 4, 5};
int[] newArr = Arrays.copyOf(oldArr, oldArr.length * 2); // 扩容到2倍
// newArr: {1,2,3,4,5,0,0,0,0,0}
💡 实际开发怎么选?
日常开发无脑用
Arrays.copyOf(),代码短不易错;面试或需要精细控制(比如复制一部分、拷到中间位置)时用System.arraycopy()。
七、经典排序:冒泡排序
冒泡排序是每个初学者必会的排序算法。核心思想:相邻元素两两比较,大的往后冒,每一轮把当前最大的数"冒泡"到末尾。
java
int[] arr = {5, 2, 9, 1, 5, 6};
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 temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
// 排序后:{1, 2, 5, 5, 6, 9}
记忆口诀:外层 n-1 轮,内层每轮少比 i 个,大的往后冒。实际开发当然直接用 Arrays.sort(arr),但面试和考试要能手写。
八、经典查找:二分查找
二分查找要求数组必须有序,每次猜中间值,把搜索范围砍掉一半。时间复杂度 O(log n),比线性查找快得多(10 亿数据只需要 30 次比较!)。
java
int[] arr = {1, 3, 5, 7, 9, 11, 13, 15}; // 必须有序!
int target = 7;
int left = 0, right = arr.length - 1;
int index = -1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == target) { index = mid; break; }
else if (arr[mid] < target) left = mid + 1; // 目标在右半区
else right = mid - 1; // 目标在左半区
}
System.out.println(index == -1 ? "没找到" : "下标:" + index); // 3
⚠️ 二分查找的前提是数组有序!
如果数组无序,千万不要用二分查找,结果不可预测。先用
Arrays.sort()排好序再查找,或直接用Arrays.binarySearch()。
九、Arrays 工具类:数组操作瑞士军刀
JDK 提供了 java.util.Arrays 类,里面有一堆静态方法帮你搞定数组的常见操作,千万别自己重复造轮子。
| 方法 | 作用 | 示例 |
|---|---|---|
| Arrays.toString(arr) | 把数组转成可读字符串 | Arrays.toString(new int[]{1,2,3}) → "1, 2, 3" |
| Arrays.sort(arr) | 对数组升序排序(底层是双轴快排) | Arrays.sort(arr); |
| Arrays.binarySearch(arr, key) | 二分查找(数组必须有序) | Arrays.binarySearch(arr, 7) |
| Arrays.copyOf(arr, len) | 复制数组(常用于扩容) | Arrays.copyOf(arr, arr.length*2) |
| Arrays.equals(a, b) | 比较两个数组内容是否相同 | Arrays.equals(arr1, arr2) |
| Arrays.fill(arr, val) | 把数组所有元素填充为指定值 | Arrays.fill(arr, 0); |
| Arrays.copyOfRange(arr, from, to) | 复制数组的一段(左闭右开) | Arrays.copyOfRange(arr, 1, 4) |
java
import java.util.Arrays;
public class ArraysDemo {
public static void main(String[] args) {
int[] arr = {5, 2, 9, 1, 5, 6};
System.out.println(Arrays.toString(arr)); // [5, 2, 9, 1, 5, 6]
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 5, 5, 6, 9]
int idx = Arrays.binarySearch(arr, 6);
System.out.println("6的下标:" + idx); // 4
int[] copy = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(copy));// [1,2,5,5,6,9,0,0,0,0]
}
}
⚠️ 打印数组别再用
System.out.println(arr)!直接打印数组名,输出的是类似
[I@1b6d3586这样的内存地址,不是你想要的内容!一定要用Arrays.toString(arr)或者自己循环打印。
十、二维数组:数组的数组
二维数组本质上是一维数组里的每个元素又是一个一维数组,可以理解成"表格"或"矩阵"。比如 3 行 4 列的数据就用 3×4 的二维数组表示。
10.1 声明与初始化
java
// 动态初始化(规则矩阵:3行4列)
int[][] matrix = new int[3][4];
// 静态初始化(规则矩阵)
int[][] a = {{1,2,3}, {4,5,6}, {7,8,9}};
// 不规则二维数组(每行列数不同)------Java特有
int[][] b = new int[3][];
b[0] = new int[2]; // 第0行2个元素
b[1] = new int[5]; // 第1行5个元素
b[2] = new int[3]; // 第2行3个元素
10.2 访问元素与遍历
java
int[][] a = {{1,2,3}, {4,5,6}, {7,8,9}};
System.out.println(a[1][2]); // 6(第2行第3列)
// 双层for循环遍历
for (int i = 0; i < a.length; i++) { // 外层遍历行
for (int j = 0; j < a[i].length; j++) { // 内层遍历列
System.out.print(a[i][j] + " ");
}
System.out.println();
}
10.3 经典案例:杨辉三角
杨辉三角是二维数组最经典的练习题。规律:每行首尾是 1,中间的数等于它上方两个数之和。
java
public class YangHui {
public static void main(String[] args) {
int n = 10; // 打印10行
int[][] triangle = new int[n][];
for (int i = 0; i < n; i++) {
triangle[i] = new int[i + 1]; // 第i行有i+1个元素
triangle[i][0] = 1; // 首尾都是1
triangle[i][i] = 1;
for (int j = 1; j < i; j++) { // 中间元素 = 上方两数之和
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j];
}
}
// 打印
for (int i = 0; i < n; i++) {
for (int k = 0; k < n - i; k++) System.out.print(" ");
for (int j = 0; j <= i; j++) System.out.printf("%-3d", triangle[i][j]);
System.out.println();
}
}
}
十一、数组内存模型(入门必知)
理解数组的内存模型,能帮你彻底弄懂很多"看起来奇怪"的现象。
Java 内存分两块(入门阶段简化理解):栈存基本类型变量和引用(地址),堆存对象和数组本身。
java
int[] a = {1, 2, 3};
int[] b = a; // b和a指向同一个数组!
b[0] = 99;
System.out.println(a[0]); // 99!改b也改了a
这里 b = a 并没有复制数组内容,只是把 a 里存的"地址"拷贝给了 b,两个变量指向堆里同一块内存。所以通过 b 修改元素,a 也会受影响。这就是引用类型的关键特征,面试常考。
如果想真正复制一份独立的数组(互不影响),就要用 Arrays.copyOf() 或 System.arraycopy()。
十二、10 条避坑汇总
| # | 坑点 | 正确做法 |
|---|---|---|
| 1 | 下标从0开始 | arr[0] 是第一个,arr[arr.length-1] 才是最后一个,别访问 arr[arr.length] |
| 2 | 数组长度固定 | 创建后不能改长,想扩容必须新建+拷贝 |
| 3 | length vs length() | 数组用 arr.length(属性无括号),字符串用 str.length()(方法) |
| 4 | 静态初始化简化写法不能拆分 | int[] a; a = {1,2,3}; 编译报错,要么 int[] a = {1,2,3}; 要么 a = new int[]{1,2,3}; |
| 5 | 默认值别忘 | int 默认 0,boolean 默认 false,引用类型默认 null;用 null 数组会空指针 |
| 6 | 打印数组用 Arrays.toString() | 直接 System.out.println(arr) 打印的是地址,不是内容 |
| 7 | for-each 不能修改元素 | for-each 里的变量是副本,想改元素用普通 for 循环 |
| 8 | 整数除法陷阱 | 求平均记得强转:(double)sum / arr.length |
| 9 | 二分查找必须先排序 | 无序数组用 binarySearch 结果不可预测 |
| 10 | 数组引用赋值是地址拷贝 | int[] b = a; 两个变量指向同一个数组,改一个影响另一个 |
十三、实战案例
案例1:评委打分
比赛有 6 个评委打分(0-100),去掉一个最高分、一个最低分,求剩余 4 个的平均分。
java
import java.util.Arrays;
public class JudgeScore {
public static void main(String[] args) {
int[] scores = {88, 95, 72, 100, 83, 91};
int max = scores[0], min = scores[0], sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
if (scores[i] > max) max = scores[i];
if (scores[i] < min) min = scores[i];
}
double avg = (double)(sum - max - min) / (scores.length - 2);
System.out.println("最高分:" + max + " 最低分:" + min);
System.out.println("选手最终得分:" + avg); // 89.25
}
}
案例2:数组元素去重
已知一个数组中存有重复元素,把重复的去掉,返回新数组。思路:先统计有多少个不重复元素,再创建对应长度新数组,依次放入。
java
import java.util.Arrays;
public class ArrayDistinct {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 2, 1, 4, 5, 3, 6};
// 第一步:新建一个临时数组,长度和原数组一样大
int[] temp = new int[arr.length];
int count = 0; // 记录不重复元素的个数
for (int i = 0; i < arr.length; i++) {
boolean exist = false;
for (int j = 0; j < count; j++) { // 看temp里有没有
if (temp[j] == arr[i]) { exist = true; break; }
}
if (!exist) { temp[count++] = arr[i]; }
}
// 第二步:截取有效长度
int[] result = Arrays.copyOf(temp, count);
System.out.println(Arrays.toString(result)); // [1, 2, 3, 4, 5, 6]
}
}
案例3:随机打乱数组(洗牌算法)
从后往前遍历,每个位置随机和前面某个位置交换,经典的 Fisher-Yates 洗牌算法,公平高效。
java
import java.util.Arrays;
import java.util.Random;
public class Shuffle {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Random rand = new Random();
for (int i = arr.length - 1; i > 0; i--) {
int j = rand.nextInt(i + 1); // [0, i]随机一个位置
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
System.out.println(Arrays.toString(arr));
// 例如:[6, 2, 10, 1, 8, 4, 3, 9, 5, 7]
}
}
十四、动手练习
光看不练假把式,来几道题检验一下:
- 练习1 定义一个长度为 10 的 int 数组,用随机数给每个元素赋值 1-100,然后求所有元素的最大值、最小值、总和、平均值。
- 练习2 有一个已经按升序排好的数组,现在输入一个新数,要求按原来的升序规律把它插入到数组中(提示:新建长度+1 的数组,找到插入位置后插入)。
- 练习3 定义一个二维数组表示一个 4×4 矩阵,求对角线元素之和、以及矩阵所有元素的最大值及其位置。
- 练习4(挑战) 实现一个方法:判断一个字符串是否是"回文串"(正读反读都一样,比如"abcba"、"上海自来水来自海上"),用 char 数组实现。
💡 答案提示
- 练习2 插入逻辑:先遍历找到第一个比新数大的位置,该位置之后的元素全部后移一位,新数放进去。
- 练习4 回文判断:把字符串转成
char[],双指针从头尾向中间比较,遇到不相等就不是回文。
下篇预告
数组学完了,接下来你会遇到一个让人"又爱又恨"的东西------方法(Method)。有了方法,你可以把一段重复使用的代码"打包"起来,给它起个名字,想用就调用,代码瞬间清爽十倍。下一篇我们讲:方法的定义、参数传递、重载、递归,以及最重要的------理解 Java 到底是"值传递"还是"引用传递"。敬请期待!
--- 完 ---