04_Java数组操作全解

Java数组操作全解:一维数组、二维数组与Arrays工具类

文章目录

  • Java数组操作全解:一维数组、二维数组与Arrays工具类
    • 前言
    • 一、一维数组
      • [1.1 数组的定义与初始化](#1.1 数组的定义与初始化)
      • [1.2 数组的访问与遍历](#1.2 数组的访问与遍历)
      • [1.3 数组的默认值](#1.3 数组的默认值)
      • [1.4 数组的常见异常](#1.4 数组的常见异常)
        • [1.4.1 ArrayIndexOutOfBoundsException(数组索引越界异常)](#1.4.1 ArrayIndexOutOfBoundsException(数组索引越界异常))
        • [1.4.2 NullPointerException(空指针异常)](#1.4.2 NullPointerException(空指针异常))
    • 二、数组的常见操作
      • [2.1 求最大值和最小值](#2.1 求最大值和最小值)
      • [2.2 数组反转](#2.2 数组反转)
      • [2.3 冒泡排序](#2.3 冒泡排序)
      • [2.4 二分查找](#2.4 二分查找)
    • 三、二维数组
      • [3.1 二维数组的定义](#3.1 二维数组的定义)
      • [3.2 二维数组的遍历](#3.2 二维数组的遍历)
      • [3.3 二维数组实战------杨辉三角](#3.3 二维数组实战——杨辉三角)
      • [3.4 二维数组在游戏开发中的应用](#3.4 二维数组在游戏开发中的应用)
    • 四、Arrays工具类
      • [4.1 toString------数组转字符串](#4.1 toString——数组转字符串)
      • [4.2 sort------数组排序](#4.2 sort——数组排序)
      • [4.3 binarySearch------二分查找](#4.3 binarySearch——二分查找)
      • [4.4 copyOf和copyOfRange------复制数组](#4.4 copyOf和copyOfRange——复制数组)
      • [4.5 equals------比较数组](#4.5 equals——比较数组)
      • [4.6 fill------填充数组](#4.6 fill——填充数组)
      • [4.7 Arrays.asList与List.toArray------数组与集合的相互转换](#4.7 Arrays.asList与List.toArray——数组与集合的相互转换)
      • [4.8 deepToString、parallelSort和Stream的补充介绍](#4.8 deepToString、parallelSort和Stream的补充介绍)
    • 五、数组综合实战------简易图书管理系统
    • 总结
    • [✅ 亮点总结](#✅ 亮点总结)
    • 适用场景
    • 扩展方向

前言

在实际开发中,我们经常需要存储和处理大量同类型的数据。如果为每条数据都定义一个变量,那将是一场灾难。**数组(Array)**正是为了解决这个问题而设计的数据结构------它可以用一个变量名存储多个同类型的数据,并通过索引快速访问。

数组是Java中最基础、最核心的数据结构之一。掌握数组操作,是后续学习集合框架(如ArrayListHashMap)的前提。虽然ArrayList提供了更灵活的增删操作,但在性能敏感的场景下(如底层算法实现、矩阵运算、图像处理),原生数组凭借其内存连续分配、零额外开销的特性,依然是无可替代的选择。通过本文的学习,你将彻底理解数组的存储原理、掌握常见算法,并熟练运用Arrays工具类提升开发效率。

本文将从数组的定义、初始化讲起,深入到二维数组、常见算法,最后介绍Arrays工具类的强大功能,并带你实现一个简易的图书管理系统来巩固所学的所有知识。

一、一维数组

1.1 数组的定义与初始化

数组是存储同一种数据类型多个元素的容器。在Java中,数组有两种常见的初始化方式:

java 复制代码
// 方式一:动态初始化(指定长度,元素使用默认值)
int[] arr1 = new int[5];          // 长度5,默认值都是0
String[] arr2 = new String[3];    // 长度3,默认值都是null
double[] arr3 = new double[4];    // 长度4,默认值都是0.0

// 方式二:静态初始化(指定元素值,长度自动计算)
int[] arr4 = {10, 20, 30, 40, 50};
String[] arr5 = {"Java", "Python", "C++", "Go"};

// 方式三:先声明再初始化
int[] arr6;
arr6 = new int[]{1, 2, 3, 4, 5};

注意 :Java中数组有int[]int arr[]两种声明方式,推荐使用int[],更符合面向对象风格。

三种初始化方式的使用场景

  • 动态初始化(new int[5]:适用于已知元素个数但不知道具体值的场景。例如从数据库查询返回了10条记录,但具体内容需要后续赋值填充。这种方式会为数组分配连续的内存空间,所有元素自动赋为类型的默认值。
  • 静态初始化({10, 20, 30}:适用于元素值在编写代码时就已确定的场景。例如定义一周七天的名称、十二个月的天数等固定数据。这种方式最简洁,代码可读性最高,编译器会自动计算数组长度。
  • 先声明再初始化 :适用于数组的声明和初始化不在同一个作用域的场景。例如将数组声明为类的成员变量,在构造方法中再初始化;或者方法的返回值为数组类型时,在return语句中使用new int[]{...}语法返回临时创建的数组对象。

1.2 数组的访问与遍历

数组通过索引(下标)来访问元素,索引从0开始:

java 复制代码
public class ArrayAccessDemo {
    public static void main(String[] args) {
        int[] scores = {95, 88, 76, 92, 85};
        
        // 通过索引获取元素
        int firstScore = scores[0];    // 第一个元素:95
        int lastScore = scores[4];     // 最后一个元素:85
        
        // 通过索引修改元素
        scores[2] = 80;                // 将76改为80
        
        // 获取数组长度
        int length = scores.length;    // 5
        
        System.out.println("数组长度:" + length);
        System.out.println("第一个成绩:" + firstScore);
        System.out.println("修改后的第三个成绩:" + scores[2]);
        
        // 遍历数组的方式一:普通for循环
        for (int i = 0; i < scores.length; i++) {
            System.out.println("第" + (i + 1) + "个成绩:" + scores[i]);
        }
        
        // 遍历数组的方式二:增强for循环(推荐)
        for (int score : scores) {
            System.out.println("成绩:" + score);
        }
        
        // 遍历数组的方式三:倒序遍历
        for (int i = scores.length - 1; i >= 0; i--) {
            System.out.println("倒序第" + (scores.length - i) + "个:" + scores[i]);
        }
    }
}

普通for循环 vs 增强for循环

在实际开发中,选择哪种遍历方式取决于你的具体需求:

  • 普通for循环(for (int i = 0; i < arr.length; i++) :优势在于可以通过索引i精确控制遍历范围,适合需要知道当前元素位置的场景。比如:只遍历奇数索引位、从中间开始遍历、或者在遍历时同步修改数组元素的值。此外,像冒泡排序这类需要比较相邻元素的算法,也必须使用带索引的for循环。
  • 增强for循环(for (int score : scores):语法简洁,可读性强,适合纯读取操作。如果你只是遍历数组打印或累加求和,增强for循环是更好的选择。它的底层实际上也是通过迭代器工作,避免了你手动操作索引时写出越界错误的风险。但注意:增强for循环中不能修改数组元素的值,也无法获取当前元素的索引位置。
  • 倒序遍历:在某些场景下倒序遍历更高效,例如在删除数组中多个元素时,正序删除会导致索引偏移问题,而倒序删除则不受影响。

经验法则:需要索引就选普通for,纯读取就选增强for,删除操作考虑倒序。

1.3 数组的默认值

不同类型的数组,元素默认值不同:

java 复制代码
public class ArrayDefaults {
    public static void main(String[] args) {
        int[] intArr = new int[3];          // [0, 0, 0]
        double[] doubleArr = new double[3]; // [0.0, 0.0, 0.0]
        boolean[] boolArr = new boolean[3]; // [false, false, false]
        char[] charArr = new char[3];       // [\\u0000, \\u0000, \\u0000]
        String[] strArr = new String[3];    // [null, null, null]
        
        System.out.println("int默认值:" + intArr[0]);
        System.out.println("double默认值:" + doubleArr[0]);
        System.out.println("boolean默认值:" + boolArr[0]);
        System.out.println("String默认值:" + strArr[0]);
    }
}

默认值的内存原理

当使用new关键字创建数组时,JVM会在堆内存中为数组分配一块连续的内存空间。Java语言规范要求所有变量在使用前必须有确定的值,因此JVM会自动为数组中的每个元素赋默认值。不同类型的默认值对应着它们在内存中的二进制零值表示:

  • 整型(byte/short/int/long)默认值0:二进制全零位,直接对应数值0。
  • 浮点型(float/double)默认值0.0:按照IEEE 754标准,二进制全零表示正零(0.0)。
  • boolean默认值false:在JVM内部,boolean常用0表示false,1表示true。
  • char默认值'\u0000':Unicode编码中的空字符,显示为空白。
  • 引用类型(如String)默认值null:引用变量存储的是对象的内存地址,null表示该引用不指向任何堆中的对象,在内存中表现为特殊的空引用值。

关键认知 :数组的默认值初始化是Java内存安全机制的重要组成部分,它确保你不会读取到内存中遗留的"脏数据"。这也解释了为什么局部变量需要手动初始化而数组元素不需要------数组使用new关键字在堆上分配,JVM负责清零;而基本类型的局部变量分配在栈上,不会自动清零。

1.4 数组的常见异常

在数组使用中,有两个异常几乎每位Java开发者都不可避免地会遇到。理解它们的产生原因和排查方法,是提升调试能力的关键。

1.4.1 ArrayIndexOutOfBoundsException(数组索引越界异常)
java 复制代码
int[] arr = {1, 2, 3};
// int x = arr[3];    // 抛出异常!有效索引是0、1、2

异常原因 :Java数组的索引范围是[0, length-1]。当你访问arr[arr.length]或更大的索引,或者使用负数索引(如arr[-1])时,JVM会抛出此异常。这个异常属于运行时异常(RuntimeException),编译器不会帮你检查,只有在程序运行时才会暴露。

真实排查步骤

  1. 查看异常栈信息:首先定位异常抛出的代码行号。错误信息中会明确显示"Index X out of bounds for length Y",告诉你索引X超出了数组长度Y的范围。
  2. 检查循环边界 :最常见的越界发生在for循环中,循环条件写成了i <= arr.length而非i < arr.length。由于数组长度为n时有效索引是0到n-1,使用<=会导致最后一次循环访问arr[n]而越界。
  3. 检查变量或表达式计算的索引 :如果你使用表达式计算索引(如arr[arr.length - i]),确保表达式的结果始终在合法范围内。
  4. 注意空数组 :长度为0的数组(new int[0])虽然合法,但任何索引访问都会越界。在遍历前先判断arr.length > 0是一种防御性编程的好习惯。

经典案例 :在循环中写for (int i = 0; i <= arr.length; i++)会导致最后一次迭代时访问arr[arr.length]而抛出此异常。解决方法是将<=改为<

1.4.2 NullPointerException(空指针异常)
java 复制代码
int[] arr2 = null;
// int x = arr2[0];   // 抛出异常!arr2没有指向任何数组对象

异常原因null是Java中的特殊字面量,表示引用变量不指向任何对象。当你试图通过一个null引用来访问数组的元素(arr2[0])、获取数组的长度(arr2.length)、或者调用任何实例方法时,都会抛出NullPointerException。在Java中,数组本身也是一个对象,arr2只是这个对象的引用变量。

真实排查步骤

  1. 确认数组是否被正确初始化 :检查数组变量是否使用new关键字或静态初始化语法进行了赋值。如果你声明了int[] arr;但忘了初始化就使用,如果是成员变量则arr的值为默认的null,如果是局部变量则编译器会报错。
  2. 检查数组是否被意外置为null :有时候调用了某个返回数组的方法,但该方法在某些条件下返回了null。例如map.get(key)可能返回null,然后你试图使用这个返回值作为数组。
  3. 使用Objects.requireNonNull进行防御 :在方法入口处使用Objects.requireNonNull(arr, "arr不能为null")来快速定位null的来源,这也是一种良好的防御性编程实践。
  4. 检查多线程环境下的竞态条件:在多线程环境中,一个线程可能将数组引用置为null,而另一个线程还在使用它。

经典案例:声明了数组成员变量但忘记在构造方法中初始化,导致后续使用时抛出NullPointerException。解决方法是确保在对象构造完成前为所有引用类型成员变量赋值。

两个异常的对比总结

异常类型 触发条件 最常见原因 排查重点
ArrayIndexOutOfBoundsException 索引<0 或 索引>=length 循环边界写错(<=写成<) 检查循环条件和索引表达式
NullPointerException 通过null引用访问 忘记初始化或方法返回null 追踪数组变量的赋值来源

二、数组的常见操作

2.1 求最大值和最小值

寻找数组中的最大值和最小值是最基础的数组算法,其核心思路来源于生活中的"打擂台"思想:

算法推导过程

  1. 初始化擂台 :假设数组的第一个元素arr[0]既是当前最大值也是当前最小值,这一步相当于在擂台上先站了一个人,暂时没人比他高也没人比他矮。
  2. 逐个挑战:从第二个元素(索引1)开始,依次让每个元素与当前的"擂主"(max/min)比较。这个过程中,每个元素都要向max和min两位擂主分别发起挑战。
  3. 更新擂主:如果当前元素比max大,说明它比目前的最高者还高,它就成为新的max擂主;同理,如果比min小,它就成为新的min擂主。
  4. 遍历结束:当所有元素都挑战完毕后,留存在max和min中的就是真正的最大值和最小值。

时间复杂度分析 :算法只遍历了一次数组,共进行了2*(n-1)次比较,时间复杂度为O(n)。这是理论上寻找最大最小值的最优解------因为要确定最大值,你必须至少查看数组中的每个元素一次。

java 复制代码
public class ArrayMaxMin {
    public static void main(String[] args) {
        int[] arr = {34, 67, 23, 89, 12, 56, 78, 45};
        
        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);  // 89
        System.out.println("最小值:" + min);  // 12
    }
}

小技巧 :如果数组可能为空,在求最大最小值前应先判断arr.length > 0,否则arr[0]会抛出ArrayIndexOutOfBoundsException。在生产代码中,可以对空数组返回Integer.MIN_VALUEInteger.MAX_VALUE或抛出明确的业务异常。

2.2 数组反转

数组反转是将数组元素的顺序完全颠倒------第一个变成最后一个,最后一个变成第一个。想象你有一排从左到右排列的书,反转操作就是把最左边的书和最右边的书交换位置,然后第二本和倒数第二本交换,以此类推。

图解思路

以数组{1, 2, 3, 4, 5, 6}为例:

  • 初始状态[1, 2, 3, 4, 5, 6],数组长度n=6,需要交换的次数是n/2=3次。
  • 第1轮交换(i=0) :交换arr[0]=1arr[5]=6,完成后数组变为[6, 2, 3, 4, 5, 1]
  • 第2轮交换(i=1) :交换arr[1]=2arr[4]=5,完成后数组变为[6, 5, 3, 4, 2, 1]
  • 第3轮交换(i=2) :交换arr[2]=3arr[3]=4,完成后数组变为[6, 5, 4, 3, 2, 1]
  • 完成:当i到达数组中间位置时,所有元素都已完成反转。如果数组长度是奇数,中间的元素恰好不需要移动(因为它与自身交换没有意义)。

通过i < arr.length / 2的条件控制,确保每对首尾元素只交换一次。如果不加这个条件限制,循环会走完全程,导致反转两次后数组恢复原状。

java 复制代码
public class ArrayReverse {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6};
        
        System.out.print("反转前:");
        printArray(arr);
        
        // 反转算法:首尾交换,到中间停止
        for (int i = 0; i < arr.length / 2; i++) {
            int temp = arr[i];
            arr[i] = arr[arr.length - 1 - i];
            arr[arr.length - 1 - i] = temp;
        }
        
        System.out.print("反转后:");
        printArray(arr);  // [6, 5, 4, 3, 2, 1]
    }
    
    public static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

2.3 冒泡排序

冒泡排序(Bubble Sort)是最经典、最直观的排序算法之一,其名称来源于排序过程中较大的元素像气泡一样逐渐"浮"到数组的末尾。虽然它的时间复杂度为O(n²),在数据量大时效率较低,但它的算法思想清晰易懂,非常适合作为排序算法的入门学习。

算法思路推导

以数组[64, 34, 25, 12, 22, 11, 90]为例,我们通过相邻元素的反复比较和交换,让每一轮都将当前未排序部分的最大值"冒泡"到最后。

第1轮(i=0):比较所有相邻元素,将最大值90移到最后。

  • 比较64和34:64>34,交换 → [34, 64, 25, 12, 22, 11, 90]
  • 比较64和25:64>25,交换 → [34, 25, 64, 12, 22, 11, 90]
  • 比较64和12:64>12,交换 → [34, 25, 12, 64, 22, 11, 90]
  • 比较64和22:64>22,交换 → [34, 25, 12, 22, 64, 11, 90]
  • 比较64和11:64>11,交换 → [34, 25, 12, 22, 11, 64, 90]
  • 比较64和90:64<90,不交换 → [34, 25, 12, 22, 11, 64, 90]
  • 第1轮结束,最大值90已到末尾。

第2轮(i=1):最后一位已排好,只需比较前6对。

  • 比较34和25:交换 → [25, 34, 12, 22, 11, 64, 90]
  • 比较34和12:交换 → [25, 12, 34, 22, 11, 64, 90]
  • 比较34和22:交换 → [25, 12, 22, 34, 11, 64, 90]
  • 比较34和11:交换 → [25, 12, 22, 11, 34, 64, 90]
  • 比较34和64:不交换 → [25, 12, 22, 11, 34, 64, 90]
  • 第2轮结束,次大值64已到倒数第二位。

第3轮(i=2) :最后两位已排好,比较前5对 → 结果:[12, 22, 11, 25, 34, 64, 90]

第4轮(i=3) :比较前4对 → 结果:[12, 11, 22, 25, 34, 64, 90]

第5轮(i=4) :比较前3对 → 结果:[11, 12, 22, 25, 34, 64, 90]

第6轮(i=5) :比较前2对 → 结果:[11, 12, 22, 25, 34, 64, 90]

经过6轮(n-1轮)后,数组完全有序。可以看到,每完成一轮,尚未排序的尾部就会多一个已就位的元素。

核心代码分析

  • 外层循环i控制轮数(共n-1轮),同时也表示已排好序的元素数量。
  • 内层循环j控制每轮中要比较的元素对数:arr.length - 1 - i,因为每轮排好的最后一个元素不需要再参与比较。
  • 交换操作使用临时变量temp作为"中转站",这是两数交换的经典写法。
java 复制代码
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        
        System.out.print("排序前:");
        for (int n : arr) System.out.print(n + " ");
        System.out.println();
        
        // 冒泡排序
        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;
                }
            }
        }
        
        System.out.print("排序后:");
        for (int n : arr) System.out.print(n + " ");
        System.out.println();
    }
}

优化版冒泡排序

标准冒泡排序有一个明显的可以优化的点:如果在某一轮比较过程中,没有发生任何一次交换,说明数组已经完全有序,此时就可以提前结束排序,无需继续进行后续无意义的比较。

java 复制代码
public class BubbleSortOptimized {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 5, 4, 6, 7, 8};
        
        System.out.print("排序前:");
        for (int n : arr) System.out.print(n + " ");
        System.out.println();
        
        // 优化版冒泡排序
        for (int i = 0; i < arr.length - 1; i++) {
            boolean swapped = false;  // 本轮是否有交换的标志
            
            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;
                    swapped = true;   // 发生了交换
                }
            }
            
            // 如果本轮没有发生交换,说明已经有序,提前退出
            if (!swapped) {
                System.out.println("第" + (i + 1) + "轮后已有序,提前结束");
                break;
            }
        }
        
        System.out.print("排序后:");
        for (int n : arr) System.out.print(n + " ");
        System.out.println();
    }
}

优化效果 :对于近乎有序的数组,优化后的冒泡排序接近于O(n)的时间复杂度。例如数组[1, 2, 3, 5, 4, 6, 7, 8]只有5和4需要交换,第一轮交换后数组就完全有序,第二轮检测到无交换后立即退出,避免了剩余的n-2轮无意义比较。但最坏情况下(完全逆序)仍为O(n²)。
实际开发建议 :虽然理解冒泡排序是学习算法的必经之路,但在生产代码中请直接使用Arrays.sort(),它的底层基于双轴快速排序(Dual-Pivot Quicksort),性能远超手写的冒泡排序。

2.4 二分查找

二分查找(Binary Search)是一种在有序数组中高效查找目标值的算法。它的核心思想来自"猜数字"游戏------你先猜中间的数字,如果猜大了就排除右半边,猜小了就排除左半边,每次都能将搜索范围缩小一半。

图解式文字描述

假设在有序数组[11, 22, 33, 44, 55, 66, 77, 88, 99]中查找55

  • 步骤1 :设定左边界left=0(指向11),右边界right=8(指向99),中间位置mid=(0+8)/2=4,对应元素arr[4]=55。运气很好,第一次就命中了!返回索引4。
  • 换一个目标------查找66
  • 步骤1left=0, right=8, mid=4, arr[4]=55。55<66,说明目标在右半部分。更新left = mid+1 = 5
  • 步骤2left=5, right=8, mid=(5+8)/2=6, arr[6]=77。77>66,说明目标在左半部分。更新right = mid-1 = 5
  • 步骤3left=5, right=5, mid=5, arr[5]=66。命中!返回索引5。
  • 再查找一个不存在的值------60
  • 步骤1mid=4, arr[4]=55 < 60left=5
  • 步骤2mid=6, arr[6]=77 > 60right=5
  • 步骤3 :此时left=5, right=5,但因为left <= right成立,mid=5, arr[5]=66 > 60right=4
  • 步骤4left=5, right=4left > right,循环终止,返回-1表示未找到。

核心要点

  • left <= right的等号至关重要。如果写成left < right,当区间缩小到只有一个元素时循环就会跳过,导致漏掉最后一个可能的匹配元素。
  • 每次根据比较结果将搜索范围减半,时间复杂度为O(log n)。对于百万级数据,最多只需要约20次比较就能找到目标------这就是二分查找的强大之处。

前提:数组必须是有序的。

java 复制代码
public class BinarySearch {
    public static void main(String[] args) {
        int[] arr = {11, 22, 33, 44, 55, 66, 77, 88, 99};
        int target = 55;
        
        int left = 0;
        int 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;
            }
        }
        
        if (index != -1) {
            System.out.println(target + "在数组中的索引是:" + index);
        } else {
            System.out.println("数组中不存在" + target);
        }
    }
}

递归版二分查找

迭代版的二分查找使用while循环,而递归版本则利用方法自身调用自身来实现不断缩小搜索范围的效果。递归版本的代码更加简洁优雅,但需要注意栈溢出风险。

java 复制代码
public class BinarySearchRecursive {
    public static void main(String[] args) {
        int[] arr = {11, 22, 33, 44, 55, 66, 77, 88, 99};
        int target = 66;
        
        int index = binarySearch(arr, target, 0, arr.length - 1);
        
        if (index != -1) {
            System.out.println(target + "在数组中的索引是:" + index);
        } else {
            System.out.println("数组中不存在" + target);
        }
    }
    
    public static int binarySearch(int[] arr, int target, int left, int right) {
        // 递归终止条件:搜索区间为空
        if (left > right) {
            return -1;
        }
        
        int mid = left + (right - left) / 2;  // 防止整数溢出
        
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            // 目标在右半部分,递归搜索右区间
            return binarySearch(arr, target, mid + 1, right);
        } else {
            // 目标在左半部分,递归搜索左区间
            return binarySearch(arr, target, left, mid - 1);
        }
    }
}

注意 :递归版中计算mid使用了left + (right - left) / 2而非(left + right) / 2,这是为了防止当left和right都很大时相加导致整数溢出。虽然在实际开发中很少遇到,但这是一种良好的编程习惯。
实际开发建议 :对于有序数组的查找,直接使用Arrays.binarySearch()即可,JDK已经为你实现了健壮的二分查找算法。这里的手动实现更多是为了理解算法原理。

三、二维数组

3.1 二维数组的定义

二维数组可以理解为"数组中的数组",常用于存储行和列的表格数据:

java 复制代码
// 方式一:动态初始化(同时指定行数和列数)
int[][] arr1 = new int[3][4];  // 3行4列的矩阵

// 方式二:动态初始化(只指定行数,列数后续指定)
int[][] arr2 = new int[3][];   // 3行,每行列数可以不同
arr2[0] = new int[2];          // 第1行2列
arr2[1] = new int[3];          // 第2行3列
arr2[2] = new int[1];          // 第3行1列

// 方式三:静态初始化
int[][] arr3 = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// 方式四:不规则二维数组
int[][] arr4 = {
    {1},
    {2, 3},
    {4, 5, 6},
    {7, 8, 9, 10}
};

二维数组的内存布局

理解二维数组在内存中的存储方式对于正确使用它至关重要。在Java中,二维数组并不是数学意义上的连续矩阵,而是一个"数组的数组"------外层数组的每个元素都是一个指向内层一维数组的引用。

int[][] arr3 = {``{1,2,3}, {4,5,6}, {7,8,9}}为例:

  • 外层数组arr3存储了3个引用,分别指向3个内层的一维int数组。
  • 每个内层数组是独立分配在堆内存中的一维数组对象,长度可以不同。
  • 访问arr3[1][2]时,JVM先通过外层引用找到第2个内层数组,再通过内层数组的索引2找到元素6。

这也解释了为什么Java可以创建不规则二维数组(如{``{1}, {2,3}, {4,5,6}})------每行的内层数组是独立的对象,长度不需要一致。

3.2 二维数组的遍历

java 复制代码
public class TwoDimensionalArray {
    public static void main(String[] args) {
        int[][] scores = {
            {85, 90, 78},    // 学生1的三科成绩
            {92, 88, 76},    // 学生2的三科成绩
            {79, 95, 88},    // 学生3的三科成绩
            {83, 72, 91}     // 学生4的三科成绩
        };
        
        // 遍历方式一:嵌套for循环
        for (int i = 0; i < scores.length; i++) {
            for (int j = 0; j < scores[i].length; j++) {
                System.out.print(scores[i][j] + "\t");
            }
            System.out.println();
        }
        
        System.out.println("===============");
        
        // 遍历方式二:增强for循环
        for (int[] row : scores) {
            for (int score : row) {
                System.out.print(score + "\t");
            }
            System.out.println();
        }
        
        // 计算每个学生的总分
        for (int i = 0; i < scores.length; i++) {
            int total = 0;
            for (int score : scores[i]) {
                total += score;
            }
            System.out.println("学生" + (i + 1) + "总分:" + total);
        }
    }
}

遍历方式选择

  • 嵌套for循环(scores[i][j] :适合需要知道行列索引的场景,比如打印带格式的表格、定位某个具体位置的元素。通过ij可以灵活计算每个元素的位置信息。
  • 增强for循环(for (int[] row : scores):代码更简洁,适合纯遍历输出。由于不需要显式操作索引,可以减少越界错误的可能。但如果有不规则数组(每行列数不同),增强for循环同样可以安全处理,因为它基于每行实际的length来遍历。

注意 :二维数组的length属性,scores.length返回的是行数(外层数组长度),scores[i].length返回的是第i行的列数(该行内层数组的长度)。

3.3 二维数组实战------杨辉三角

杨辉三角是中国古代数学的杰出成就,在西方被称为"帕斯卡三角"。它的核心规则非常简洁:每个数等于它上方两数之和(即上一行同列和上一行前一列之和)。

算法推导过程

  1. 确定数据结构:杨辉三角天然适合用不规则二维数组来表示------第i行有i+1个元素(i从0开始),恰好对应Java中可以先指定行数再为每行单独分配长度的特性。
  2. 边界值处理:每一行的第一个和最后一个元素始终为1,这是杨辉三角的定义决定的------因为这两个位置"上方"只有一个数,无法求和。
  3. 递推公式 :对于非边界元素triangle[i][j],其值等于triangle[i-1][j-1] + triangle[i-1][j]。这个公式直接体现了"每个数等于它上方两数之和"的规则。
  4. 计算顺序:必须从上到下、从左到右逐行计算。因为第i行的值依赖于第i-1行,所以只有在上一行计算完成后才能计算当前行。从左到右是因为同一个行内各元素之间没有依赖关系。

可视化推导(前6行):

复制代码
第0行:        1
第1行:      1   1
第2行:    1   2   1
第3行:  1   3   3   1
第4行: 1  4   6   4   1
第5行:1  5  10  10  5   1

以第3行的triangle[2][1]=2为例:它由第2行的triangle[1][0]=1triangle[1][1]=1相加得来。同样,第4行的triangle[3][2]=3triangle[2][1]=2triangle[2][2]=1相加得来。

java 复制代码
public class YangHuiTriangle {
    public static void main(String[] args) {
        int rows = 10;
        int[][] triangle = new int[rows][];
        
        for (int i = 0; i < triangle.length; i++) {
            triangle[i] = new int[i + 1];    // 每一行的长度
            triangle[i][0] = 1;              // 每行第一个元素为1
            triangle[i][i] = 1;              // 每行最后一个元素为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 < triangle.length; i++) {
            // 打印空格使三角形居中
            for (int k = 0; k < rows - i; k++) {
                System.out.print("  ");
            }
            for (int j = 0; j < triangle[i].length; j++) {
                System.out.printf("%4d", triangle[i][j]);
            }
            System.out.println();
        }
    }
}

3.4 二维数组在游戏开发中的应用

二维数组在游戏开发中有着广泛的应用,几乎是棋盘类游戏和网格类游戏的标准数据结构。

应用场景一:迷宫地图存储

在迷宫游戏中,地图可以用一个二维int数组来表示,不同的数字代表不同的地形:

java 复制代码
public class MazeMap {
    public static void main(String[] args) {
        // 0=通路, 1=墙壁, 2=起点, 3=终点, 4=已走路径
        int[][] maze = {
            {1, 1, 1, 1, 1, 1, 1},
            {1, 2, 0, 0, 0, 0, 1},
            {1, 1, 1, 0, 1, 1, 1},
            {1, 0, 0, 0, 0, 0, 1},
            {1, 0, 1, 1, 1, 0, 1},
            {1, 0, 0, 0, 0, 3, 1},
            {1, 1, 1, 1, 1, 1, 1}
        };
        
        // 打印迷宫
        for (int[] row : maze) {
            for (int cell : row) {
                switch (cell) {
                    case 0: System.out.print("  "); break;    // 通路
                    case 1: System.out.print("##"); break;    // 墙壁
                    case 2: System.out.print("S "); break;     // 起点
                    case 3: System.out.print("E "); break;     // 终点
                    case 4: System.out.print("**"); break;    // 路径
                }
            }
            System.out.println();
        }
    }
}

应用场景二:棋盘状态存储

五子棋、围棋、国际象棋等棋类游戏的核心就是用一个二维数组记录棋盘上每个位置的棋子状态:

java 复制代码
public class GobangBoard {
    public static void main(String[] args) {
        int size = 15;
        // 0=空位, 1=黑子, 2=白子
        int[][] board = new int[size][size];
        
        // 模拟落子
        board[7][7] = 1;  // 黑子落中央
        board[7][8] = 2;  // 白子落右侧
        
        // 打印棋盘
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (board[i][j] == 0) {
                    System.out.print("+ ");
                } else if (board[i][j] == 1) {
                    System.out.print("● ");
                } else {
                    System.out.print("○ ");
                }
            }
            System.out.println();
        }
        
        // 判断某个位置是否已有棋子
        int targetRow = 7, targetCol = 7;
        if (board[targetRow][targetCol] != 0) {
            System.out.println("位置(" + targetRow + "," + targetCol + ")已有棋子");
        } else {
            System.out.println("位置(" + targetRow + "," + targetCol + ")为空,可以落子");
        }
    }
}

应用场景三:图像像素矩阵

在图像处理中,一张灰度图片本质上就是一个二维数组,每个元素的值代表该像素的亮度(0-255)。对图片的旋转、翻转、滤镜等操作都是对二维数组的各种变换。

java 复制代码
public class ImageMatrix {
    public static void main(String[] args) {
        // 一个5x5的简单"图像"(灰度值矩阵)
        int[][] image = {
            {100, 120, 140, 120, 100},
            {120,  50,  80,  50, 120},
            {140,  80,   0,  80, 140},
            {120,  50,  80,  50, 120},
            {100, 120, 140, 120, 100}
        };
        
        // 将图像水平翻转(沿垂直中轴线镜像)
        int rows = image.length;
        int cols = image[0].length;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols / 2; j++) {
                int temp = image[i][j];
                image[i][j] = image[i][cols - 1 - j];
                image[i][cols - 1 - j] = temp;
            }
        }
        
        System.out.println("水平翻转后的图像矩阵:");
        for (int[] row : image) {
            for (int pixel : row) {
                // 用不同字符表示不同亮度范围
                if (pixel < 50) System.out.print("##");
                else if (pixel < 100) System.out.print("++");
                else if (pixel < 130) System.out.print("..");
                else System.out.print("  ");
            }
            System.out.println();
        }
    }
}

总结:无论多么复杂的游戏画面,其底层逻辑往往最终都可以抽象为一个或多个二维数组。掌握了二维数组的操作,你就具备了实现棋类游戏、迷宫寻路、图像处理等应用的基础能力。

四、Arrays工具类

java.util.Arrays是Java提供的数组操作工具类,位于java.util包下,包含了很多操作数组的实用静态方法。学习这些方法可以大大提高开发效率,避免重复造轮子。

4.1 toString------数组转字符串

使用场景 :当你需要快速查看数组内容,用于日志输出、调试打印或用户界面展示时。在没有Arrays.toString()之前,你必须手动写循环拼接字符串,既繁琐又容易出错。

java 复制代码
import java.util.Arrays;

public class ArraysToStringDemo {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30, 40, 50};
        
        // 不用Arrays:自己拼接(繁琐)
        System.out.println("手动输出:" + arr);  // [I@1b6d3586(内存地址)
        
        // 使用Arrays:一行搞定
        System.out.println("Arrays输出:" + Arrays.toString(arr));
        // 输出:[10, 20, 30, 40, 50]
    }
}

4.2 sort------数组排序

使用场景 :几乎任何需要有序数据的场景都会用到排序------商品按价格排序、成绩按分数排名、日志按时间戳排序等等。对于基本类型数组,Arrays.sort()底层使用双轴快速排序(Dual-Pivot Quicksort),时间复杂度为O(n log n);对于对象数组,使用TimSort(一种改进的归并排序),保证稳定排序。

java 复制代码
import java.util.Arrays;

public class ArraysSortDemo {
    public static void main(String[] args) {
        int[] arr = {42, 15, 87, 23, 56, 91, 34};
        
        System.out.println("排序前:" + Arrays.toString(arr));
        
        // 升序排序(底层使用优化的快速排序)
        Arrays.sort(arr);
        System.out.println("排序后:" + Arrays.toString(arr));
        
        // 只排序指定范围的元素 [fromIndex, toIndex)
        int[] arr2 = {9, 8, 7, 6, 5, 4, 3, 2, 1};
        Arrays.sort(arr2, 2, 6);  // 只排序索引2到5的元素
        System.out.println("部分排序:" + Arrays.toString(arr2));
        // 输出:[9, 8, 5, 6, 7, 4, 3, 2, 1]
    }
}

4.3 binarySearch------二分查找

使用场景 :当你需要在已排序 的大型数组中快速判断某个值是否存在,并获取其位置时。例如在一份排好序的用户ID列表中查找某个用户、在词典中查找某个单词。注意:binarySearch不会帮你排序,如果传入无序数组,返回结果是未定义的,可能返回错误索引或任意负数。

java 复制代码
import java.util.Arrays;

public class ArraysSearchDemo {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30, 40, 50, 60, 70};
        
        int index = Arrays.binarySearch(arr, 40);
        System.out.println("40的索引:" + index);  // 3
        
        // 元素不存在时返回负数(-插入点 - 1)
        int index2 = Arrays.binarySearch(arr, 35);
        System.out.println("35的索引:" + index2);  // -4
        // 如果存在应该插入到索引3的位置,-3-1 = -4
        
        // 注意:binarySearch要求数组必须有序,否则结果不确定
        int[] unordered = {5, 3, 8, 1, 9};
        int badResult = Arrays.binarySearch(unordered, 3);
        System.out.println("无序数组查询:" + badResult);  // 不可预期的结果
    }
}

4.4 copyOf和copyOfRange------复制数组

使用场景 :当你需要扩容数组(Java数组长度不可变,扩容本质是创建新数组并复制)、截取数组的一部分、或者创建数组的备份副本时。在图书管理系统的增删操作中,我们正是通过copyOf来实现数组的"动态"扩容。System.arraycopy是底层更高效的复制方式,而copyOfcopyOfRange是对其的封装,使用更简洁。

java 复制代码
import java.util.Arrays;

public class ArraysCopyDemo {
    public static void main(String[] args) {
        int[] original = {1, 2, 3, 4, 5};
        
        // copyOf:复制指定长度的数组
        int[] copy1 = Arrays.copyOf(original, 3);  // 只复制前3个
        System.out.println("复制前3个:" + Arrays.toString(copy1));
        
        int[] copy2 = Arrays.copyOf(original, 8);  // 复制并扩展,多出的用默认值
        System.out.println("复制后扩展:" + Arrays.toString(copy2));
        
        // copyOfRange:复制指定范围 [from, to)
        int[] copy3 = Arrays.copyOfRange(original, 1, 4);  // 复制索引1、2、3
        System.out.println("复制索引1-3:" + Arrays.toString(copy3));
    }
}

4.5 equals------比较数组

使用场景 :判断两个数组的内容是否完全相同(长度相等且每个对应位置的元素相等)。这里特别要注意Arrays.equals()==的区别------==比较的是引用地址(两个变量是否指向堆中的同一个数组对象),而Arrays.equals()比较的是数组的内容。这在单元测试中验证方法返回值、判断缓存是否命中时非常常用。

java 复制代码
import java.util.Arrays;

public class ArraysEqualsDemo {
    public static void main(String[] args) {
        int[] arr1 = {1, 2, 3, 4, 5};
        int[] arr2 = {1, 2, 3, 4, 5};
        int[] arr3 = {1, 2, 3, 4, 6};
        
        // equals比较两个数组是否相同(长度相同 + 每个元素相同)
        System.out.println("arr1 == arr2:" + Arrays.equals(arr1, arr2));  // true
        System.out.println("arr1 == arr3:" + Arrays.equals(arr1, arr3));  // false
        System.out.println("arr1 == arr2(==):" + (arr1 == arr2));        // false!比较地址
    }
}

4.6 fill------填充数组

使用场景:将数组的全部或部分元素设置为相同的值。比如重置游戏棋盘(所有格子归零)、初始化标志位数组(全部设为false)、批量填充计数器的初始值。在对数组进行算法计算前,填充默认值也是一种常见的初始化手段。

java 复制代码
import java.util.Arrays;

public class ArraysFillDemo {
    public static void main(String[] args) {
        int[] arr = new int[10];
        
        // 全部填充为同一个值
        Arrays.fill(arr, 99);
        System.out.println("全部填充:" + Arrays.toString(arr));
        // 输出:[99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
        
        // 部分填充 [fromIndex, toIndex)
        int[] arr2 = new int[10];
        Arrays.fill(arr2, 2, 6, 88);
        System.out.println("部分填充:" + Arrays.toString(arr2));
        // 输出:[0, 0, 88, 88, 88, 88, 0, 0, 0, 0]
    }
}

4.7 Arrays.asList与List.toArray------数组与集合的相互转换

在实际项目中,数组和集合(尤其是ArrayList)经常需要互相转换。数组长度固定、访问速度快;集合长度可变、提供丰富的增删改查方法。根据不同的使用场景灵活切换,是高效开发的必备技能。

使用场景

  • 数组转List :当你需要使用List接口提供的便捷方法(如containsindexOfremovestream()等)处理数组数据时,先将数组转为List。
  • List转数组:当方法签名要求数组类型参数(如一些遗留API、可变参数方法的底层实现),或者你需要数据在内存中保持连续存储以获得更好的缓存局部性时。
java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;

public class ArrayToListDemo {
    public static void main(String[] args) {
        // ========== 数组 → List ==========
        String[] languages = {"Java", "Python", "C++", "Go"};
        
        // 方式一:Arrays.asList() ------ 返回固定大小的List(视图)
        List<String> list1 = Arrays.asList(languages);
        System.out.println("asList转换:" + list1);
        
        // 注意:asList返回的List与原数组共享数据,修改会互相影响
        languages[0] = "Kotlin";
        System.out.println("修改原数组后:" + list1);  // 会变化!
        // list1.add("Rust");  // 报错!asList返回的List大小固定,不支持add/remove
        
        // 方式二:new ArrayList<>(Arrays.asList()) ------ 创建独立的可变List
        Integer[] nums = {1, 2, 3, 4, 5};
        List<Integer> mutableList = new ArrayList<>(Arrays.asList(nums));
        mutableList.add(6);  // 可以正常操作
        mutableList.remove(0);
        System.out.println("可变List:" + mutableList);  // [2, 3, 4, 5, 6]
        
        // ========== List → 数组 ==========
        List<String> nameList = new ArrayList<>();
        nameList.add("张三");
        nameList.add("李四");
        nameList.add("王五");
        
        // 方式一:toArray(T[] a) ------ 转成指定类型的数组(推荐)
        String[] nameArray = nameList.toArray(new String[0]);
        System.out.println("List转数组:" + Arrays.toString(nameArray));
        
        // 方式二:toArray() ------ 转成Object[](不推荐,需要强制类型转换)
        Object[] objArray = nameList.toArray();
        String first = (String) objArray[0];  // 需要逐个强制类型转换
        
        // 方式三:toArray(new String[list.size()]) ------ 预分配精确大小
        String[] exactArray = nameList.toArray(new String[nameList.size()]);
        System.out.println("精确大小转换:" + Arrays.toString(exactArray));
    }
}

重要提醒

  • Arrays.asList()返回的List是一个视图 (view),与原数组共享底层数据。修改数组会影响List,修改List也会影响数组------但它们都不是真正的"可变集合",不支持add()remove()操作。
  • new ArrayList<>(Arrays.asList(arr))是创建数组独立副本并得到可变List的标准做法。
  • list.toArray(new String[0])是List转数组的推荐写法(零长度数组),JVM会优化并分配正确大小的数组。这种方式比预先new String[list.size()]更简洁,性能几乎无差异。

4.8 deepToString、parallelSort和Stream的补充介绍

除了上面详细介绍的核心方法,Arrays工具类还提供了几个非常实用的高级方法:

deepToString :用于打印多维数组(如二维数组)的内容。普通的toString对于嵌套数组只能打印出内存地址,而deepToString可以递归打印所有层级的内容。

java 复制代码
import java.util.Arrays;

public class ArraysDeepToStringDemo {
    public static void main(String[] args) {
        int[][] matrix = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };
        
        // toString只能打印外层对象的地址
        System.out.println("toString:" + Arrays.toString(matrix));
        // 输出类似:[[I@1b6d3586, [I@4554617c, [I@74a14482]
        
        // deepToString可以递归打印所有内容
        System.out.println("deepToString:" + Arrays.deepToString(matrix));
        // 输出:[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    }
}

parallelSort:并行排序方法,充分利用多核CPU的优势,在数据量较大时(通常超过8192个元素)可以显著提升排序速度。它内部使用Fork/Join框架将数组分成多个子数组并行排序,最后合并。

java 复制代码
import java.util.Arrays;
import java.util.Random;

public class ArraysParallelSortDemo {
    public static void main(String[] args) {
        int size = 100000;
        int[] arr1 = new int[size];
        int[] arr2 = new int[size];
        Random random = new Random();
        
        for (int i = 0; i < size; i++) {
            arr1[i] = random.nextInt();
            arr2[i] = arr1[i];  // 两个数组内容完全相同
        }
        
        // 普通排序
        long start1 = System.currentTimeMillis();
        Arrays.sort(arr1);
        long end1 = System.currentTimeMillis();
        System.out.println("sort耗时:" + (end1 - start1) + "ms");
        
        // 并行排序(数据量大时通常更快)
        long start2 = System.currentTimeMillis();
        Arrays.parallelSort(arr2);
        long end2 = System.currentTimeMillis();
        System.out.println("parallelSort耗时:" + (end2 - start2) + "ms");
    }
}

stream方法 :Java 8引入的Arrays.stream()方法可以将数组转换为流(Stream),从而利用流式API进行函数式编程------过滤、映射、聚合等操作都可以用链式调用优雅地完成。

java 复制代码
import java.util.Arrays;

public class ArraysStreamDemo {
    public static void main(String[] args) {
        int[] scores = {85, 92, 78, 95, 88, 72, 90, 66};
        
        // 过滤出及格(>=60)的成绩并计算平均值
        double average = Arrays.stream(scores)
                .filter(s -> s >= 60)
                .average()
                .orElse(0.0);
        System.out.println("及格成绩的平均分:" + average);
        
        // 找出最高分
        int max = Arrays.stream(scores).max().orElse(-1);
        System.out.println("最高分:" + max);
        
        // 将所有成绩映射为等级并统计A的数量
        long countA = Arrays.stream(scores)
                .filter(s -> s >= 90)
                .count();
        System.out.println("90分以上的人数:" + countA);
        
        // 将所有成绩加5分(加分策略),转为新数组
        int[] bumped = Arrays.stream(scores)
                .map(s -> Math.min(s + 5, 100))
                .toArray();
        System.out.println("加分后:" + Arrays.toString(bumped));
    }
}

小结Arrays工具类提供了从基本操作(排序、查找)到高级操作(并行、流式)的全套方法。记住这些方法可以让你在处理数组时事半功倍。在日常开发中,能用标准库方法的就不要自己手写,标准库的代码经过充分测试和优化,远比手写实现更可靠、更高效。

五、数组综合实战------简易图书管理系统

通过前文的学习,我们已经掌握了数组的定义、遍历、常见算法以及Arrays工具类的使用。现在,我们综合运用这些知识来实现一个简易的图书管理系统。

设计思路

这个系统围绕一个字符串数组books来存储所有的图书名称。由于Java原生数组的长度不可变,我们需要借助Arrays工具类来实现看起来"动态"的增删操作:

  1. 存储设计 :使用String[]存储图书名称,数组初始包含4本图书。String类型的选择是因为图书名称是文本数据,且String自带equals方法便于查找比较。
  2. 查找功能 :使用简单的线性遍历(因为数据量小,无需排序后二分查找),逐个比较equals方法匹配书名。注意这里必须使用equals而非==,因为==比较的是字符串对象的引用地址。
  3. 添加功能 :通过Arrays.copyOf(books, books.length + 1)创建比原数组多1位的新数组,然后将新书放入最后一个位置。这个操作的实质是"创建更大容量的新数组+复制旧数据",时间复杂度为O(n)。
  4. 删除功能 :使用System.arraycopy将删除位置前后的数据分别复制到新数组中,跳过要删除的元素。这是最底层也是最灵活的数组复制方法,需要手动计算各段的起始位置和长度。
  5. 排序功能 :直接调用Arrays.sort()完成字母序排序,底层使用优化后的快速排序算法。

这个系统的核心教育意义在于:让你亲身体验到原生数组的局限性 ------每次增删都需要手动创建新数组和复制数据。这也为后续学习ArrayList(内部自动完成扩容和缩容)埋下伏笔,让你深刻理解为什么在日常开发中集合框架比原生数组更常用。

java 复制代码
import java.util.Arrays;

public class BookManager {
    public static void main(String[] args) {
        // 存储图书名称的数组
        String[] books = {"Java入门", "Python基础", "数据结构", "算法导论"};
        
        System.out.println("=== 当前藏书 ===");
        System.out.println(Arrays.toString(books));
        
        // 查找某本书
        String searchBook = "数据结构";
        int index = -1;
        for (int i = 0; i < books.length; i++) {
            if (books[i].equals(searchBook)) {
                index = i;
                break;
            }
        }
        System.out.println(searchBook + "的位置:" + index);
        
        // 添加新书(数组扩容)
        books = Arrays.copyOf(books, books.length + 1);
        books[books.length - 1] = "网络编程";
        System.out.println("添加后:" + Arrays.toString(books));
        
        // 删除一本书(通过复制覆盖)
        int removeIndex = 1; // 删除"Python基础"
        String[] newBooks = new String[books.length - 1];
        System.arraycopy(books, 0, newBooks, 0, removeIndex);
        System.arraycopy(books, removeIndex + 1, newBooks, removeIndex, 
                        books.length - removeIndex - 1);
        books = newBooks;
        System.out.println("删除后:" + Arrays.toString(books));
        
        // 按字母排序
        Arrays.sort(books);
        System.out.println("排序后:" + Arrays.toString(books));
    }
}

总结

本文详细讲解了Java数组的核心知识点:

  1. 一维数组:定义、初始化方式选择、遍历方式对比、默认值的内存原理、常见异常的深度排查
  2. 常用算法:最大最小值的擂台思想、数组反转的首尾交换、冒泡排序的逐轮推导与优化、二分查找的图解过程与递归实现
  3. 二维数组:内存布局、定义与遍历、不规则数组、杨辉三角的递推实现、游戏开发中的实际应用(迷宫、棋盘)
  4. Arrays工具类toStringsortbinarySearchcopyOfequalsfill六个核心方法的使用场景;deepToStringparallelSortstream三个高级方法;asListtoArray的转换技巧

数组是Java中最基础的数据结构之一,掌握数组操作是学习集合框架的前提。虽然Arrays工具类已经提供了很多便利方法,但理解底层实现(如冒泡排序、二分查找的算法原理)对于培养编程思维非常重要。下一章我们将学习Java方法的定义与使用。

✅ 亮点总结

  • 数组初始化三连 :静态初始化{1,2,3}、动态初始化new int[5]、先声明后赋值,三种方式各有用处,附带使用场景分析
  • 遍历方式选择指南:普通for循环适用需要索引的场景,增强for循环适用纯读取操作,倒序遍历在删除操作中有独特优势
  • 数组常见异常深度剖析ArrayIndexOutOfBoundsException(越界)和NullPointerException(空指针)的产生原因、真实排查步骤和对比总结
  • 数组默认值的内存原理:从JVM堆内存分配到二进制零值表示,理解Java内存安全机制的设计思想
  • 冒泡排序层层推导:从第1轮到第6轮的逐轮推导过程,附带优化版(提前退出)与标准版的对比
  • 二分查找图解式讲解:通过三种查找场景(命中、多轮查找、不存在)的步骤推导,附带迭代版与递归版的双重实现
  • 二维数组游戏开发实战:迷宫地图存储、五子棋棋盘、图像像素矩阵三种实际应用场景的完整代码示例
  • Arrays工具类使用场景大全 :每个方法附带"什么时候用"的实战场景描述,新增deepToStringparallelSortstream等高级方法
  • 数组与集合互转精讲Arrays.asList视图机制的深入剖析、toArray的三种写法和推荐用法

适用场景

  • 处理固定数量的数据集合,如班级30人的成绩、购物车中固定种类的商品
  • 实现矩阵运算与表格数据处理,如电子表格行列转换、图像像素矩阵处理
  • 作为方法参数传递批量数据,如传入int[]给统计方法计算最大值和平均值
  • 游戏开发中的场景建模,如迷宫地图、棋盘状态、像素图像的二维矩阵存储

扩展方向

  • 方法定义与使用 :将数组操作封装成可复用的方法,推荐阅读 05_Java方法定义与使用
  • List集合替代数组 :学习ArrayList动态扩容机制,理解为什么开发中List比原生数组更常用
  • 排序算法深入 :进一步学习快速排序、归并排序,了解JDK中Arrays.sort底层的TimSort算法
相关推荐
AIGS0011 小时前
生产运营三大瓶颈,工业AI怎么破局?
java·人工智能·人工智能ai大模型应用
码不停蹄的玄黓1 小时前
Java 线程池 execute() 和 submit() 对比
java·开发语言
废弃的小码农1 小时前
APP测试--adb使用介绍
python·测试工具·adb
方也_arkling1 小时前
【Java-Day19】集合1(Collect单列集合)
java·开发语言
Xin_ye100861 小时前
C# 零基础到精通教程 - WPF 专题三:高级控件与自定义控件
开发语言·c#·wpf
SoftLipaRZC1 小时前
C语言自定义类型:结构体完全指南
c语言·开发语言
方也_arkling1 小时前
【Java-Day19】集合3 List中常见的方法和5种遍历方式
java·开发语言
曲幽1 小时前
你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透
linux·windows·python·docker·fastapi·web·pyinstaller·nssm·services