一、数组概述
数组(Array)是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。
1.1 数组的特点
长度固定:数组一旦创建,大小不可改变
类型相同:所有元素必须是相同数据类型
有序性:元素在内存中连续存储,通过下标(索引)访问
可以存储基本类型和引用类型
数组本身是对象,在堆内存中分配空间
1.2 数组的分类
按维度:一维数组、二维数组、多维数组
按数据类型:基本数据类型数组、引用数据类型数组
二、一维数组
2.1 数组的声明
语法格式:
数据类型[] 数组名; // 推荐方式
数据类型 数组名[]; // C语言风格,不推荐
示例:
int[] scores; // 推荐
double[] prices;
String[] names;
int scores[]; // 不推荐,C语言风格
2.2 数组的创建(内存分配)
语法格式:
数组名 = new 数据类型[数组长度];
示例:
scores = new int[5]; // 创建长度为5的int数组
prices = new double[10]; // 创建长度为10的double数组
声明并创建合并写法:
int[] scores = new int[5];
String[] names = new String[3];
2.3 数组的初始化
静态初始化(指定内容,长度由系统决定)
// 完整格式 int[] scores = new int[]{100, 98, 95, 88, 90};
// 简化格式(推荐)
int[] scores = {100, 98, 95, 88, 90};
String[] names = {"张三", "李四", "王五"};
动态初始化(指定长度,元素为默认值)
int[] scores = new int[5]; // 元素默认值:0
double[] prices = new double[3]; // 元素默认值:0.0
boolean[] flags = new boolean[2]; // 元素默认值:false
String[] names = new String[4]; // 元素默认值:null
注意:不能同时指定长度和内容!错误示例:int[] arr = new int[3]{1,2,3};
2.4 数组元素的访问
通过下标/索引访问,下标从0开始,到长度-1结束
int[] arr = {10, 20, 30, 40, 50};
// 获取元素
System.out.println(arr[0]); // 输出:10
System.out.println(arr[2]); // 输出:30
// 修改元素
arr[1] = 200; System.out.println(arr[1]); // 输出:200
// 获取数组长度
System.out.println(arr.length); // 输出:5
2.5 数组的遍历
方式一:普通for循环
int[] arr = {10, 20, 30, 40, 50};
for (int i = 0; i < arr.length; i++)
{ System.out.println("第" + i + "个元素:" + arr[i]); }
方式二:增强for循环(foreach)
int[] arr = {10, 20, 30, 40, 50};
// 只能读取,不能修改
for (int num : arr) { System.out.println(num); }
foreach特点:代码简洁,但无法获取索引,只能遍历读取,不能修改元素
三、二维数组与多维数组
.1 二维数组的声明与创建
二维数组本质上是"数组的数组",可以理解为一个表格,有行和列两个维度。创建二维数组时可以同时指定行数和列数,也可以只指定行数不指定列数,这样就形成了不规则数组。
不规则数组是Java的特色功能,每行可以有不同的列数,这在某些特殊场景下非常有用,可以节省内存空间。
3.2 二维数组的初始化
静态初始化
静态初始化二维数组时,使用嵌套的大括号表示每行的元素。既可以创建规则的矩阵形式,也可以创建不规则的形式,每行的元素个数可以不同。
动态初始化
动态初始化二维数组时,指定行数和列数,所有元素自动初始化为对应类型的默认值。
3.3 二维数组的访问与遍历
二维数组需要两个下标来访问元素,第一个下标表示行号,第二个下标表示列号。
遍历二维数组需要使用双层循环,外层循环遍历行,内层循环遍历列。既可以使用普通for循环的嵌套,也可以使用增强for循环的嵌套。使用增强for循环时,外层循环的变量是一维数组类型。
四、数组常用操作
4.1 求最大值与最小值
求数组最大值的算法思路:将第一个元素作为初始最大值,然后依次与后面的每个元素比较,如果遇到更大的元素就更新最大值。求最小值的算法完全相同,只是比较方向相反。
4.2 数组求和与平均值
数组求和:初始化一个累加变量为0,遍历数组将每个元素累加到累加变量中。平均值就是总和除以数组长度,注意要进行类型转换,避免整数除法的精度丢失问题。
4.3 数组反转
数组反转的高效算法:使用对称交换的方式,只需要遍历数组的前半部分,将第i个元素与倒数第i个元素交换。这种方式的时间复杂度是O(n/2),空间复杂度是O(1),不需要额外数组空间。
4.4 数组查找
线性查找:遍历数组逐个比较,找到目标元素则返回下标,遍历完仍未找到则返回-1。线性查找的优点是数组不需要有序,缺点是平均查找效率较低。
二分查找:要求数组必须有序,每次将查找范围缩小一半,效率很高。JDK的Arrays工具类已经提供了二分查找的实现。
4.5 数组复制
数组复制有三种常用方式:
手动复制:使用循环逐个元素复制,最基础但代码繁琐
System.arraycopy():JDK提供的本地方法,效率最高,可以指定源位置和目标位置
Arrays.copyOf():基于System.arraycopy()封装,使用更方便,会创建新数组
注意:直接进行引用赋值(arr1 = arr2)不是数组复制,两个引用会指向同一个数组对象,修改其中一个会影响另一个。
五、Arrays 工具类
java.util.Arrays是JDK提供的专门用于操作数组的工具类,包含大量静态方法,是数组操作的首选工具。使用前需要导入该类。
5.1 常用方法详解
toString():将数组转换为字符串形式,方便打印输出,是调试时最常用的方法
sort():对数组进行升序排序,底层使用优化的快速排序算法,效率很高
binarySearch():二分查找,要求数组必须有序,找到返回下标,找不到返回负数
fill():用指定值填充数组的全部或部分元素
copyOf() / copyOfRange():复制数组,可以指定新数组长度或复制范围
equals():比较两个数组的内容是否完全相同
deepToString() / deepEquals():针对多维数组的字符串转换和比较
六、数组内存分析
6.1 JVM内存区域划分
内存区域 | 主要作用 | 存储内容 |
|---|---|---|
栈(Stack) | 方法执行的内存模型 | 基本数据类型变量、对象引用变量、方法参数 |
堆(Heap) | 对象存储区域 | 所有new出来的对象和数组,是垃圾回收的主要区域 |
方法区 | 存储类的元信息 | 类信息、静态变量、常量池、方法代码 |
6.2 数组内存机制
数组变量存储在栈内存中,保存的是数组对象在堆内存中的地址值,而不是数组内容本身。使用new创建数组时,会在堆内存中分配连续空间,并将首地址赋值给栈中的引用变量。
当进行引用赋值时,只是将地址值复制给另一个引用变量,两个引用指向同一个堆内存中的数组对象。通过任意一个引用修改数组内容,另一个引用也会看到修改后的结果。
七、常见问题与注意事项
7.1 数组下标越界异常
这是数组操作中最常见的运行时异常。当下标小于0或者大于等于数组长度时,就会抛出此异常。编写代码时一定要注意下标的有效范围是0到length-1。
7.2 空指针异常
当数组引用变量为null时,尝试访问数组元素就会抛出空指针异常。使用数组前一定要确保数组已经正确初始化。
7.3 数组长度不可变性
数组一旦创建,长度就固定了,不能修改。length属性是只读的。如果需要改变数组大小,只能创建一个新的更大的数组,然后将原数组内容复制过去。这也是ArrayList等集合类的底层实现原理。
7.4 数组作为方法参数和返回值
数组作为方法参数时,传递的是数组的引用地址,方法内部对数组的修改会影响到原数组。数组也可以作为方法的返回值,返回的同样是数组引用。
八、数组排序算法
8.1 冒泡排序
冒泡排序是最基础的交换排序算法,核心思想是通过相邻元素的比较和交换,使较大的元素逐渐"冒泡"到数组末尾。每一轮排序都会确定一个最大元素的最终位置。
冒泡排序可以优化:如果某一轮比较中没有发生任何交换,说明数组已经有序,可以提前结束排序。
8.2 选择排序
选择排序的思路是每一轮从未排序部分选择最小的元素,放到已排序部分的末尾。选择排序的交换次数比冒泡排序少,但时间复杂度相同。
8.3 插入排序
插入排序将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,插入到已排序部分的合适位置。插入排序在数据基本有序时效率很高。
学习总结:数组是Java中最基础也是最重要的数据结构,是学习集合框架、算法和数据结构的基础。重点掌握数组的内存机制、常用操作和Arrays工具类的使用,这些都是实际开发中每天都会用到的知识。