【Java零基础·第4章】数组 - 详解
数组是Java中最基础也最常用的数据结构之一,它就像一个有序的容器,可以存储多个相同类型的数据。本文将从数组的基本概念讲起,逐步深入到各种实用的数组操作技巧和算法,帮助你全面掌握Java数组的使用。
一、数组基础概念
1.1 什么是数组?
数组是用来管理一组相同数据类型变量的数据结构。想象一下,如果你需要记录一个班级50名学生的成绩,使用单个变量需要声明50个变量,而使用数组只需要一个数组变量即可,极大地简化了代码。
数组的核心特点:
- 使用
[]作为标志性符号 - 包含多个相同数据类型的元素
- 每个元素通过"下标"(index)来访问,下标从0开始
- 数组长度固定,一旦创建无法更改
- 数组长度通过
数组名.length获取
1.2 数组的基本操作
数组的"遍历"是最基本也最重要的操作,即逐个访问数组中的元素,通常使用for循环实现:
public class ArrayTraversal {
public static void main(String[] args) {
// 定义一个存储成绩的数组
int[] scores = {86, 98, 75, 88, 93};
// 遍历数组(快捷键:itar 或 数组名.fori)
for (int i = 0; i < scores.length; i++) {
System.out.println("第" + (i + 1) + "个同学的成绩:" + scores[i]);
}
}
}
⚠️ 注意:数组下标范围是
[0, 数组名.length - 1],如果访问超出这个范围的下标,会抛出ArrayIndexOutOfBoundsException(数组下标越界异常)。
二、数组的声明与初始化
数组的使用分为两步:声明和初始化。初始化就是确定数组的长度和元素值。
2.1 静态初始化
当你已经知道数组中每个元素的值时,可以使用静态初始化:
// 方式一:声明和初始化同时完成(推荐)
元素类型[] 数组名 = {元素1, 元素2, 元素3, ...};
// 方式二:声明和初始化分开
元素类型[] 数组名;
数组名 = new 元素类型[]{元素1, 元素2, 元素3, ...};
示例:
public class StaticInitialization {
public static void main(String[] args) {
// 存储一周七天的英文名称
String[] weekDays = {"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"};
// 存储平年每个月的天数
int[] daysInMonth;
daysInMonth = new int[]{31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
// 遍历月份天数
for (int i = 0; i < daysInMonth.length; i++) {
System.out.println((i + 1) + "月有" + daysInMonth[i] + "天");
}
}
}
2.2 动态初始化
当你只知道数组长度,不知道具体元素值时,使用动态初始化。此时数组元素会被赋予默认值:
元素类型[] 数组名 = new 元素类型[长度];
不同数据类型的默认值:
| 数据类型 | 默认值 |
|---|---|
| byte, short, int | 0 |
| long | 0L |
| float | 0.0F |
| double | 0.0 |
| char | ‘\u0000’(空字符) |
| boolean | false |
| 引用类型(如String) | null |
示例:
import java.util.Scanner;
public class DynamicInitialization {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入班级人数:");
int count = input.nextInt();
// 动态初始化数组,存储学生成绩
int[] scores = new int[count];
// 从键盘输入成绩
for (int i = 0; i < scores.length; i++) {
System.out.print("请输入第" + (i + 1) + "个学生的成绩:");
scores[i] = input.nextInt();
}
// 显示所有成绩
System.out.println("班级成绩如下:");
for (int i = 0; i < scores.length; i++) {
System.out.print(scores[i] + " ");
}
input.close();
}
}
三、数组的内存理解
理解数组在内存中的存储方式,有助于更好地掌握数组的特性。
- 数组名存储的是数组在内存中的首地址(引用)
- 数组元素在内存中连续存储
- 访问元素时,通过"首地址 + 元素宽度 × 下标"计算元素位置
在Java中,数组的内存分配与管理是理解数组特性的核心,以下从内存模型角度详细详细说明:
1、数组的内存分配机制
Java中数组是引用数据类型,其内存分配涉及两种关键内存区域:
- 栈内存(Stack):存储局部变量(包括数组引用变量),特点是内存自动分配和释放,速度快。
- 堆内存(Heap):存储数组的实际元素数据,特点是动态分配内存,需要JVM的垃圾回收机制回收,速度较慢。
2、一维数组的内存解析
以int[] arr = new int[3];为例,内存分配过程如下:
- 声明数组引用:
int[] arr在栈内存中创建一个引用变量arr,此时它还未指向任何对象(值为null)。 - 创建数组对象:
new int[3]在堆内存中开辟一块连续的内存空间,用于存储3个int类型元素。- 数组长度一旦确定不可修改(因为堆内存空间大小固定)。
- 元素会被赋予默认值(
int类型默认值为0)。
- 关联引用与对象:堆内存会为数组分配一个唯一的内存地址,栈中的
arr变量存储该地址,从而指向堆中的数组对象。
栈内存 堆内存
+--------+ +--------+
| arr | --> | [0] 0 | // 索引0
+--------+ +--------+
| [1] 0 | // 索引1
+--------+
| [2] 0 | // 索引2
+--------+
(地址:0x0012)
3、二维数组的内存解析
二维数组本质是“数组的数组”,即外层数组的元素是内层数组的引用。以int[][] arr = new int[2][3];为例:
- 外层数组分配:
new int[2][3]先在堆内存中创建外层数组(长度为2),其元素是两个int[]类型的引用(默认值为null)。 - 内层数组分配:自动为外层数组的每个元素创建内层数组(长度为3),并将内层数组的地址赋值给外层数组的对应元素。
- 引用关联:栈中的
arr变量指向外层数组的地址。
栈内存 堆内存(外层数组) 堆内存(内层数组)
+--------+ +--------------+ +--------+
| arr | --> | [0] 0x1122 | --> | [0] 0 |
+--------+ +--------------+ +--------+
| [1] 0 |
+--------+
| [2] 0 |
+--------+
+--------------+ +--------+
--> | [1] 0x3344 | --> | [0] 0 |
+--------------+ +--------+
| [1] 0 |
+--------+
| [2] 0 |
+--------+
注意:不规则二维数组
如int[][] arr = new int[2][]; arr[0] = new int[2]; arr[1] = new int[4];,内层数组长度可不同,内存分布如下:
堆内存(外层数组) 堆内存(内层数组)
+--------------+ +--------+
| [0] 0x1122 | --> | [0] 0 |
+--------------+ +--------+
| [1] 0 |
+--------------+ +--------+
| [1] 0x3344 | --> | [0] 0 |
+--------------+ +--------+
| [1] 0 |
+--------+
| [2] 0 |
+--------+
| [3] 0 |
+--------+
4、数组赋值与引用的内存表现
数组引用赋值:如
int[] arr2 = arr;,此时arr2和arr指向堆中同一个数组对象,修改arr2[0]会影响arr[0]。栈内存 堆内存 栈内存 +--------+ +--------+ +--------+ | arr | --> | [0] 10 | <-- | arr2 | +--------+ +--------+ | [1] 20 | // 同一块内存 +--------+数组拷贝(新对象):如
int[] arr2 = Arrays.copyOf(arr, arr.length);,会在堆中创建新数组,并复制原数组元素,此时arr和arr2指向不同对象,修改互不影响。
5、数组的生命周期
- 当数组引用变量超出作用域(如方法执行结束),栈内存中的引用被释放。
- 若堆中的数组对象不再被任何引用指向,会被JVM的垃圾回收器(GC)标记为“可回收”,最终释放内存。
总结
- 数组的引用存储在栈内存,实际元素存储在堆内存。
- 一维数组是连续的单一内存块,二维数组是“数组的数组”,内层数组可独立分配内存。
- 数组长度固定的本质:堆内存一旦分配,空间大小不可改变。
- 引用赋值与对象拷贝的区别:前者共享内存,后者独立内存。
数组引用特性
- 两个变量指向同一个数组:修改一个变量所指的元素,另一个变量也会看到变化
public class ArrayReference1 {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = a; // b和a指向同一个数组
b[0] = 100; // 修改b所指的数组元素
System.out.println(a[0]); // 输出100,因为a和b指向同一个数组
}
}
- 两个变量指向不同数组:修改一个数组的元素,不影响另一个数组
public class ArrayReference2 {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = {4, 5, 6}; // b指向新的数组
b[0] = 100;
System.out.println(a[0]); // 输出1,a和b是不同的数组
}
}
四、二维数组
二维数组可以理解为"数组的数组",适合存储表格形式的数据,如矩阵、成绩单等。
4.1 二维数组的概念
- 二维数组的标志性符号是
[][] - 二维数组中的每个元素都是一个一维数组
- 例如:
int[][] scores可以存储多个班级的成绩,每个班级的成绩是一个一维数组
4.2 二维数组的初始化
静态初始化
// 直接指定每个元素(每个元素都是一个一维数组)
元素类型[][] 数组名 = {
{元素1, 元素2, ...},
{元素1, 元素2, ...},
...
};
示例:
public class TwoDStaticInit {
public static void main(String[] args) {
// 存储3个班级的成绩
int[][] classScores = {
{89, 85, 86, 75}, // 一班成绩
{99, 98, 93, 92, 91}, // 二班成绩
{63, 76} // 三班成绩
};
// 遍历二维数组
for (int i = 0; i < classScores.length; i++) {
System.out.println("第" + (i + 1) + "班成绩:");
for (int j = 0; j < classScores[i].length; j++) {
System.out.print(classScores[i][j] + " ");
}
System.out.println();
}
}
}
动态初始化
- 规则的二维数组(每行长度相同):
元素类型[][] 数组名 = new 元素类型[行数][每行元素数];
示例:
public class TwoDRectangular {
public static void main(String[] args) {
// 创建3行4列的二维数组(规则矩阵)
int[][] matrix = new int[3][4];
// 给数组赋值
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
matrix[i][j] = i * 10 + j;
}
}
// 打印数组
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j] + "\t");
}
System.out.println();
}
}
}
- 不规则的二维数组(每行长度可以不同):
// 先确定行数,再分别确定每行的长度
元素类型[][] 数组名 = new 元素类型[行数][];
数组名[0] = new 元素类型[第1行长度];
数组名[1] = new 元素类型[第2行长度];
...
示例:
import java.util.Scanner;
public class TwoDIrregular {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
// 创建3行的二维数组,每行长度不确定
int[][] scores = new int[3][];
scores[0] = new int[5]; // 第1组有5人
scores[1] = new int[3]; // 第2组有3人
scores[2] = new int[4]; // 第3组有4人
// 输入成绩
for (int i = 0; i < scores.length; i++) {
for (int j = 0; j < scores[i].length; j++) {
System.out.print("请输入第" + (i + 1) + "组第" + (j + 1) + "人的成绩:");
scores[i][j] = input.nextInt();
}
}
// 显示成绩
for (int i = 0; i < scores.length; i++) {
System.out.print("第" + (i + 1) + "组成绩:");
for (int j = 0; j < scores[i].length; j++) {
System.out.print(scores[i][j] + " ");
}
System.out.println();
}
input.close();
}
}
五、一维数组常用算法
掌握这些算法,能帮你解决大部分数组操作问题。
5.1 求数组的最大值和最小值
思路:
- 假设数组第一个元素是最值
- 用后面的每个元素与当前最值比较
- 如果发现更大/更小的元素,更新最值
public class ArrayMaxMin {
public static void main(String[] args) {
int[] numbers = {5, 6, 2, 7, 8, 1, 3};
// 求最大值
int max = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] > max) {
max = numbers[i];
}
}
// 求最小值
int min = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] < min) {
min = numbers[i];
}
}
System.out.println("最大值:" + max); // 输出8
System.out.println("最小值:" + min); // 输出1
}
}
5.2 查找最值及其下标
不仅要找到最值,还要记录它在数组中的位置:
public class ArrayMaxIndex {
public static void main(String[] args) {
int[] numbers = {5, 6, 2, 7, 8, 1, 3};
// 记录最大值的下标
int maxIndex = 0;
// 遍历数组,找到最大值的下标
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] > numbers[maxIndex]) {
maxIndex = i;
}
}
System.out.println("最大值是:" + numbers[maxIndex]); // 输出8
System.out.println("最大值下标是:" + maxIndex); // 输出4
System.out.println("最大值是第" + (maxIndex + 1) + "个元素"); // 输出第5个
}
}
如果数组中有重复的最值元素:
public class ArrayMaxDuplicate {
public static void main(String[] args) {
int[] numbers = {5, 9, 2, 9, 8, 1, 9};
// 1. 先找到最大值
int max = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] > max) {
max = numbers[i];
}
}
// 2. 再找出所有等于最大值的元素下标
System.out.println("最大值是:" + max);
System.out.println("最大值的下标有:");
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == max) {
System.out.print(i + " "); // 输出1 3 6
}
}
}
}
5.3 查找目标元素
顺序查找
逐个比较数组中的元素,直到找到目标或遍历完数组:
import java.util.Scanner;
public class SequentialSearch {
public static void main(String[] args) {
int[] numbers = {5, 6, 2, 7, 8, 1, 3};
Scanner input = new Scanner(System.in);
System.out.print("请输入要查找的数字:");
int target = input.nextInt();
boolean found = false;
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == target) {
found = true;
System.out.println("找到了,下标是:" + i);
break; // 找到后退出循环
}
}
if (!found) {
System.out.println("没找到这个数字");
}
input.close();
}
}
如果数组是有序的,可以优化顺序查找:
public class OptimizedSearch {
public static void main(String[] args) {
int[] sortedArray = {2, 4, 6, 8, 12, 24}; // 有序数组(从小到大)
int target = 7;
boolean found = false;
for (int i = 0; i < sortedArray.length; i++) {
if (sortedArray[i] == target) {
found = true;
break;
} else if (sortedArray[i] > target) {
// 因为数组有序,后面的元素只会更大,所以可以提前退出
break;
}
}
System.out.println(found ? "找到了" : "没找到"); // 输出没找到
}
}
二分查找
对于有序数组,二分查找是更高效的查找方式,时间复杂度为O(logn):
public class BinarySearch {
public static void main(String[] args) {
int[] sortedArray = {2, 4, 6, 8, 12, 24}; // 必须是有序数组
int target = 8;
int left = 0; // 左边界
int right = sortedArray.length - 1; // 右边界
int index = -1; // 记录找到的下标,初始为-1表示没找到
while (left <= right) {
// 计算中间位置
int mid = left + (right - left) / 2; // 避免溢出,等价于(left + right) / 2
if (sortedArray[mid] == target) {
index = mid;
break; // 找到目标,退出循环
} else if (sortedArray[mid] < target) {
// 目标在右侧,调整左边界
left = mid + 1;
} else {
// 目标在左侧,调整右边界
right = mid - 1;
}
}
if (index != -1) {
System.out.println("找到了,下标是:" + index); // 输出3
} else {
System.out.println("没找到");
}
}
}
5.4 数组反转
将数组元素的顺序颠倒过来:
public class ArrayReverse {
public static void main(String[] args) {
int[] numbers = {5, 6, 7, 2, 1};
System.out.print("反转前:");
for (int num : numbers) {
System.out.print(num + " "); // 输出5 6 7 2 1
}
// 方案一:对称位置交换(推荐,节省空间)
for (int left = 0, right = numbers.length - 1; left < right; left++, right--) {
// 交换left和right位置的元素
int temp = numbers[left];
numbers[left] = numbers[right];
numbers[right] = temp;
}
/*
// 方案二:使用新数组
int[] newArray = new int[numbers.length];
for (int i = 0; i < numbers.length; i++) {
newArray[numbers.length - 1 - i] = numbers[i];
}
numbers = newArray;
*/
System.out.print("\n反转后:");
for (int num : numbers) {
System.out.print(num + " "); // 输出1 2 7 6 5
}
}
}
5.5 数组排序
冒泡排序
冒泡排序的基本思想是重复地走访数组,一次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来:
public class BubbleSort {
public static void main(String[] args) {
int[] numbers = {2, 8, 6, 3, 1, 7, 4};
System.out.print("排序前:");
for (int num : numbers) {
System.out.print(num + " ");
}
// 冒泡排序
for (int i = 1; i < numbers.length; i++) { // 控制轮数
boolean swapped = false; // 标记本轮是否发生交换
// 每轮比较次数递减
for (int j = 0; j < numbers.length - i; j++) {
if (numbers[j] > numbers[j + 1]) {
// 交换元素
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
swapped = true; // 发生了交换
}
}
// 如果本轮没有交换,说明数组已经有序,可以提前退出
if (!swapped) {
break;
}
}
System.out.print("\n排序后:");
for (int num : numbers) {
System.out.print(num + " "); // 输出1 2 3 4 6 7 8
}
}
}
选择排序
选择排序的基本思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完:
public class SelectionSort {
public static void main(String[] args) {
int[] numbers = {5, 6, 3, 4, 1};
System.out.print("排序前:");
for (int num : numbers) {
System.out.print(num + " ");
}
// 选择排序
for (int i = 0; i < numbers.length - 1; i++) {
// 找到从i开始的最小值及其下标
int minIndex = i;
for (int j = i + 1; j < numbers.length; j++) {
if (numbers[j] < numbers[minIndex]) {
minIndex = j;
}
}
// 如果最小值不在i位置,交换
if (minIndex != i) {
int temp = numbers[i];
numbers[i] = numbers[minIndex];
numbers[minIndex] = temp;
}
}
System.out.print("\n排序后:");
for (int num : numbers) {
System.out.print(num + " "); // 输出1 3 4 5 6
}
}
}
5.6 数组扩容
由于数组长度固定,当需要存储更多元素时,需要进行扩容:
import java.util.Scanner;
public class ArrayGrow {
public static void main(String[] args) {
// 初始数组长度为5
int[] numbers = new int[5];
Scanner input = new Scanner(System.in);
int count = 0; // 记录已存入的元素个数
while (true) {
System.out.print("请输入一个整数(输入0结束):");
int num = input.nextInt();
if (num == 0) {
break; // 输入0,结束循环
}
// 存入数组
numbers[count] = num;
count++;
// 如果数组已满,进行扩容
if (count >= numbers.length) {
// 创建一个更大的数组(原长度的1.5倍)
int[] newArray = new int[numbers.length + (numbers.length >> 1)];
// 复制原数组元素到新数组
for (int i = 0; i < numbers.length; i++) {
newArray[i] = numbers[i];
}
// 让numbers指向新数组
numbers = newArray;
System.out.println("数组已扩容,新长度:" + numbers.length);
}
}
System.out.println("最终数组元素:");
for (int i = 0; i < count; i++) {
System.out.print(numbers[i] + " ");
}
input.close();
}
}
5.7 数组元素的插入和删除
插入元素
import java.util.Scanner;
public class ArrayInsert {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40, 50};
Scanner input = new Scanner(System.in);
System.out.print("请输入要插入的元素:");
int newNum = input.nextInt();
// 1. 先扩容数组(增加一个位置)
int[] newArray = new int[numbers.length + 1];
for (int i = 0; i < numbers.length; i++) {
newArray[i] = numbers[i];
}
numbers = newArray;
// 2. 将要插入位置及其后面的元素右移
// 这里插入到下标为1的位置
for (int i = numbers.length - 1; i > 1; i--) {
numbers[i] = numbers[i - 1];
}
// 3. 插入新元素
numbers[1] = newNum;
// 4. 显示结果
System.out.println("插入后数组:");
for (int num : numbers) {
System.out.print(num + " ");
}
input.close();
}
}
删除元素
public class ArrayDelete {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40, 50};
System.out.print("删除前:");
for (int num : numbers) {
System.out.print(num + " "); // 输出10 20 30 40 50
}
// 删除下标为1的元素(值为20)
// 1. 把要删除位置后面的元素左移
for (int i = 1; i < numbers.length - 1; i++) {
numbers[i] = numbers[i + 1];
}
// 2. 清空最后一个位置(可选)
numbers[numbers.length - 1] = 0;
System.out.print("\n删除后:");
for (int num : numbers) {
System.out.print(num + " "); // 输出10 30 40 50 0
}
}
}
六、数组的实际应用案例
统计学生成绩
import java.util.Scanner;
public class ScoreStatistics {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入学生人数:");
int count = input.nextInt();
// 存储学生成绩
int[] scores = new int[count];
// 输入成绩
for (int i = 0; i < scores.length; i++) {
System.out.print("请输入第" + (i + 1) + "个学生的成绩:");
scores[i] = input.nextInt();
}
// 计算总分
int sum = 0;
for (int score : scores) {
sum += score;
}
// 计算平均分
double average = (double) sum / scores.length;
// 找最高分
int max = scores[0];
for (int score : scores) {
if (score > max) {
max = score;
}
}
// 找最低分
int min = scores[0];
for (int score : scores) {
if (score < min) {
min = score;
}
}
// 统计及格人数(60分及以上)
int passCount = 0;
for (int score : scores) {
if (score >= 60) {
passCount++;
}
}
// 输出统计结果
System.out.println("总分:" + sum);
System.out.println("平均分:" + average);
System.out.println("最高分:" + max);
System.out.println("最低分:" + min);
System.out.println("及格人数:" + passCount);
System.out.println("及格率:" + (double) passCount / scores.length * 100 + "%");
input.close();
}
}
总结
数组是Java编程的基础,掌握数组的操作是学好Java的关键一步。本文介绍了数组的基本概念、声明初始化方式、内存模型以及常用算法,包括查找、排序、扩容等操作。
通过大量的示例代码,我们学习了如何:
- 创建和初始化一维数组和二维数组
- 遍历数组并访问元素
- 查找数组中的最值和目标元素
- 对数组进行排序和反转
- 动态扩容数组以及插入删除元素
这些知识不仅适用于Java,也适用于其他编程语言,是计算机科学的基础。多加练习,熟练掌握这些操作,将为你的编程之路打下坚实的基础。
浙公网安备 33010602011771号