7.Java数组

开篇:一个变量存不下的痛

学完前面的变量和循环,你可能已经可以写点小程序了。但遇到这种场景怎么办?

一个班 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 到底是"值传递"还是"引用传递"。敬请期待!

--- 完 ---

相关推荐
m0_719084111 小时前
限流和获取请求ip的方法
java
码行山野赴时序归途1 小时前
三道经典数组题:从暴力到最优的算法思维
c语言·开发语言·数据结构·算法·leetcode
threerocks1 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
lzhdim1 小时前
提高 SQL 语句执行速度的方法
java·开发语言·数据库·sql·oracle
JavaPub-rodert1 小时前
王仕宇在 Go Context 如何解决协程泄漏与超时控制
开发语言·后端·golang·iphone·javapub·王仕宇
深漂的华哥2 小时前
Ruoyi-Plus前后端分离场景下,数据加密传输
java·spring boot·后端·开源·maven·ruoyi
ly76893 小时前
JavaScript 从入门到进阶:核心语法、异步编程与工程化实践
开发语言·javascript·ecmascript
落魄大学生之流水线上谋生计3 小时前
Java锁全面指南:从基础概念到企业级应用
java·开发语言
for_ever_love__3 小时前
python基础语法学习: 闭包
开发语言·python·学习·闭包