数组详解:从基础到进阶的Java必备知识
数组本质上是一种容器,专门用于存储同一数据类型的多个值。你可以把它想象成一个带有编号的储物柜,每个格子存放一个数据,编号从0开始。掌握数组操作是Java编程的核心技能之一。

1. 数组的定义格式与静态初始化
定义数组的语法非常简洁:数据类型[] 数组名。例如:
int [] arr
不过,这仅仅声明了一个数组变量,并没有真正在内存中创建容器来存放数据。要让数组正式投入使用,需要对其进行初始化。
静态初始化是指在内存中为数组分配空间的同时,直接将数据放入其中。其语法格式如下:
数据类型[] 数组名 = {元素1, 元素2, 元素3...};
来看一个具体示例:
int [] arr = {11, 22, 33, 44, 55};
这样一来,数组中就包含了5个元素,依次为11、22、33、44、55。
1.1 数组遍历操作
遍历数组,简单来说就是从头到尾逐个访问数组中的每一个元素。通常使用for循环配合数组的length属性来完成:
public static void main (String [] args){
for(int i = 0; i < arr.length; i++){
// 这里的 arr[i] 就是当前遍历到的元素
}
}
arr.length 表示数组的长度,因此循环从索引0一直运行到length-1,能够完整覆盖所有元素。
1.2 案例:求偶数和
来看一个实际应用:给定一个数组,需要计算其中所有偶数的总和。代码逻辑非常直观:
package com.itheima.test;
public class ArrayTest1 {
public static void main(String[] args) {
int[] arr1 = {-11, -22, -33, -44, -55};
int[] arr2 = {66, 77, 88, 99, 100};
int sum = getSum(arr2);
System.out.println("偶数和为:" + sum);
}
public static int getSum(int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
// arr[i] : 当前遍历到的元素
// i : 当前的索引值
if (arr[i] % 2 == 0) {
sum += arr[i];
}
}
return sum;
}
}
思路非常清晰:遍历数组中的每一个元素,通过 %2==0 判断是否为偶数,如果是则累加到sum变量中。注意这里传入的是arr2数组,最终输出计算结果。
1.3 案例:求最大值
再来看一个经典问题:从一组数字中找出最大值。核心思想是“打擂算法”——先假设第一个元素是最大值,然后逐个比较,遇到更大的就更新。
package com.itheima.test;
public class ArrayTest1 {
public static void main(String[] args) {
int[] arr1 = {5, 44, 33, 55, 22};
int max = getMax(arr1);
System.out.println("最大值为:" + max);
}
public static int getMax(int[] arr) {
// 1. 假设数组中的第一个元素为最大值
int max = arr[0];
// 2. 遍历数组,取出剩余的每一个元素进行比较
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
// 4. 如果找到更大的元素,就更新max变量的值
max = arr[i];
}
}
return max;
}
}
注意循环从 i=1 开始,因为索引0的元素已经作为初始擂主了。
1.4 案例:数组反转操作 ***
需求:将数组 {11, 22, 33, 44, 55} 反转为 {55, 44, 33, 22, 11}。如何实现?使用双指针法向中间靠拢,依次交换头尾对应的元素。
package com.itheima.test;
public class ArrayTest1 {
public static void main(String[] args) {
int[] arr1 = {11, 22, 33, 44, 55};
reverseArray(arr1);
for (int i = 0; i < arr1.length; i++) {
System.out.println(arr1[i]);
}
}
public static void swap() {
int[] arr = {11, 22, 33, 44, 55};
// 第一个元素 arr[0] 最后一个元素 arr[arr.length-1]
int temp = arr[0];
arr[0] = arr[arr.length - 1];
arr[arr.length - 1] = temp;
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
// 需求:对数组中的元素进行反转
public static void reverseArray(int[] arr) {
for (int start = 0, end = arr.length - 1; start < end; start++, end--) {
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
}
}
核心思路:使用两个索引 start 和 end,分别从数组头部和尾部向中间移动,只要 start < end 就交换对应元素,直到两个指针相遇。这个操作非常经典,也是面试中常见的高频考点。
2. 数组的动态初始化
动态初始化指的是先创建一个指定长度的数组,但初始时不填充具体数据,后续再逐个赋值。语法格式:
int[] arr = new int[n]; // 创建一个长度为n的数组,共有n个空位
也就是说,你只需要告诉数组“我需要多少个位置”,系统会自动在内存中开辟空间,每个位置都会被赋予一个默认值(对于int类型,默认值为0)。
2.1 两种数组初始化方式的区别
- 静态初始化:适合一开始就已经明确所有元素值的情况,由开发者自行指定数据。
- 动态初始化:适合只知道数组长度,但具体数据后续再填充的场景,系统会赋予默认初始值。
简单记忆:明确内容用静态,只知长度用动态。
2.2 案例:评委打分
一个经典的实际场景:6位评委为选手打分(分数范围为0~100),去掉一个最高分和一个最低分,剩余4个分数求平均值即为最终得分。使用动态初始化接收分数,然后遍历找出最大值、最小值,计算总和,最后求平均。
// 由6个评委打分,分数0-100,
// 选手最终得分为:去掉一个最高分和一个最低分后,剩余4个评委打分的平均值
package com.itheima.test;
import java.util.Scanner;
public class ArrayTest3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入6个评委的分数: ");
int[] arr = new int[6];
for (int i = 0; i < arr.length; i++) {
System.out.println("第" + (i + 1) + "个: ");
arr[i] = sc.nextInt();
}
// 1. 找出最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 2. 找出最小值
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < min) {
min = arr[i];
}
}
// 3. 求和
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
// 4. 计算最终得分
double avg = (sum - max - min) / 4.0;
System.out.println("平均值为:" + avg);
}
}
注意:总分要先减去最高分和最低分,再除以4.0(这样才能得到小数结果)。
3. 方法参数传递
这里有一个容易踩坑的知识点:Java方法传参时,基本数据类型传递的是值的副本,而引用数据类型传递的是地址值。来看两个对比示例:
基本类型参数
// 方法参数为基本数据类型,传递的是数据值本身
public class ArgsTest1 {
public static void main(String[] args) {
int number = 100;
System.out.println("调用change方法前:" + number); // 100
change(number);
System.out.println("调用change方法后:" + number); // 100
}
public static void change(int number) {
number = 200;
}
}
为什么前后两次输出都是100?因为方法接收到的是number值的副本(相当于复印件),方法内部随意修改,原始值依然保持不变。
超简记忆口诀
基本类型传参数,传递的只是一个值;方法里面随便改,外面原值不动摇。
引用类型参数(数组)
// 方法参数为引用数据类型,传递的是地址值
public class ArgsTest2 {
public static void main(String[] args) {
int[] arr = {11, 22, 33, 44, 55};
System.out.println("调用change方法之前:" + arr[0]); // 11
change(arr);
System.out.println("调用change方法之后:" + arr[0]); // 66
}
public static void change(int[] arr) {
arr[0] = 66;
}
}
这里传入的是数组的内存地址,方法拿着这个地址直接操作堆中的真实数据,所以修改后原数组也会随之改变。
3.1 数组常见问题
3.1.1 数组索引越界异常
当访问的索引超出了数组的有效范围(比如数组长度为3,却尝试访问arr[10]或循环时使用了 i <= length),就会抛出 ArrayIndexOutOfBoundsException。看下面这个例子:
package com.itheima.exception;
public class IndexOutOfBoundsExceptionDemo {
/* 数组索引越界异常: IndexOutOfBoundsException 访问了数组中不存在的索引 */
public static void main(String[] args) {
int[] arr = {11, 22, 33};
System.out.println(arr[10]); // 运行时抛出异常
for(int i = 0; i <= arr.length; i++){ // 这里i的取值范围是0,1,2,3,但最大索引只有2
System.out.println(arr[i]);
}
}
}
一定要牢记:循环条件不能写成 i <= arr.length,而应该使用 i < arr.length。
3.1.2 空指针异常
当将数组变量赋值为 null 时,就切断了它与堆内存中实际对象的连接,此时再访问数组元素就会引发 NullPointerException。
package com.itheima.exception;
public class NullPointerExceptionDemo {
/* 空指针异常: NullPointerException
当引用数据类型变量被赋值为 null 之后,地址的指向被切断,
如果还继续访问堆内存中的数据,就会引发空指针异常 */
public static void main(String[] args) {
int[] arr = {11, 22, 33};
arr = null;
System.out.println(arr[0]); // 触发空指针异常
}
}
记住关键点:数组变量为null时,绝对不能操作它内部的元素。
4. 二维数组
如果说一维数组是一条线,那么二维数组就像一张表格(包含行和列)。定义方式如下:
int[][] arr = new int[][] {{元素1, 元素2}, {元素1, 元素2}};
int[][] arr = {{1, 2}, {3, 4}};
public static void main(String[] args) {
int[][] arr = {{11, 22, 33}, {44, 55, 66}};
}
外层大括号里面包含两个内层大括号,每个内层大括号代表一行数据。
4.1 二维数组遍历
遍历二维数组需要两层循环:外层循环遍历每一行(即每个一维数组),内层循环遍历该行中的每一个元素。
package com.itheima.test;
public class ArrayTest4 {
public static void main(String[] args) {
int[][] arr = {{11, 22, 33}, {44, 55, 66}};
// 外层循环:遍历二维数组,取出每一个一维数组
for (int i = 0; i < arr.length; i++) {
// arr[i] : 当前遍历到的一维数组
// 内层循环:继续遍历这个一维数组,取出每一个元素
for (int j = 0; j < arr[i].length; j++) {
System.out.println(arr[i][j]);
}
}
}
}
这里 arr.length 表示二维数组的行数,arr[i].length 表示第 i 行的列数。
面向对象基础
1. 类和对象
1.1 类和对象的关系
- 依赖关系:必须先定义类(相当于模板),然后才能根据模板创建出具体的对象(实例)。
- 数量关系:同一个类可以new出多个不同的对象,每个对象拥有自己独立的属性值。
1.2 类的组成
一个类通常包含以下两部分:
- 属性(成员变量):定义在方法外部,用于描述对象的状态信息。例如:
public static Student(){
String name;
int age;
}
- 行为(成员方法):去掉
static关键字的方法,用于描述对象能够执行的操作。例如:
public void study(){
System.out.println("学习");
}
1.3 对象的创建和使用
创建对象的语法:类名 对象名 = new 类名();
Student st = new Student();
之后就可以通过 st.name 访问属性,通过 st.study() 调用方法了。
2. 对象内存图
一句话总结内存分配规则:
- 只有执行
new关键字时,才会在堆内存中创建对象,分配实际存储空间。 - 对象名(即引用变量)存放在栈内存中,其中保存的是堆内存中的地址值。
- 真正存储的数据(成员变量值)位于堆内存中。
- 方法传递对象时,传递的是地址值,因此方法内部能够修改堆内存中的数据(前面数组的例子正是这个原理)。
3. 成员变量和局部变量的区别
| 对比项 | 成员变量 | 局部变量 |
|---|---|---|
| 初始化值 | 有默认初始化值(如int类型默认为0) | 没有默认值,使用前必须显式赋值 |
| 内存位置 | 堆内存 | 栈内存 |
| 生命周期 | 随对象创建而存在,随对象被回收而消失 | 随方法调用而存在,方法执行结束即消失 |
| 作用域 | 在整个类的内部都有效 | 仅在所属的 {} 代码块内有效 |
理解这些区别,写代码时就能更清楚地判断变量应该定义在哪个位置。
