在前面的学习中,我们知道变量能够 保存数据,例如:
int score1 = 90;
int score2 = 85;
int score3 = 92;
int score4 = 88;
int score5 = 95;
如果现在需要保存 5 个学生的成绩,这样写并没有什么问题。
但是,如果需要保存 100 个学生、1000 个学生,甚至更多学生的成绩呢?
难道要定义 100 个甚至上千个变量吗?
int score1;
int score2;
int score3;
...
int score1000;
很显然,这种方式不仅代码冗长,而且维护起来也非常困难。
例如:
- 想计算所有学生的平均成绩;
- 想统计最高分和最低分;
- 想遍历所有成绩;
如果使用普通变量,几乎无法高效完成这些操作。
因此,Java 提供了一种能够存储一组相同类型数据的数据结构——数组(Array)。
数组可以把多个相同类型的数据统一管理,大大提高了开发效率。
一、什么是数组
什么是数组?
数组(Array)是一种用于存储多个相同数据类型元素的数据结构。
简单来说:数组就是一块连续的内存空间,用来保存多个相同类型的数据。
例如:
一个数组可以保存多个学生成绩:
90 85 92 88 95
也可以保存多个年龄:
18 19 20 21 22
还可以保存多个姓名:
Tom Jack Lucy Alice
在 Java 中,可以使用一个数组统一管理这些数据,而不需要定义多个独立的变量。
例如:
int[] scores = {90, 85, 92, 88, 95};
这里:
- scores 表示数组变量;
- 数组中保存了 5 个整数;
- 每一个数据称为数组元素(Element)。
数组的特点
数组具有以下几个特点。
1、数组中只能存储相同类型的数据
例如:
下面的数组是合法的。
int[] nums = {10, 20, 30};
因为:
所有元素都是 int 类型。
下面的代码则是不合法的。
int[] nums = {10, "Hello", true};
因为:
数组中出现了:
intStringboolean
三种不同的数据类型。
Java 不允许同一个数组存放不同类型的数据。
但是:
数组中存储的元素类型必须与数组声明时的数据类型兼容。对于基本类型数组(如 int[]),只能存储对应类型的数据;对于引用类型数组,可以存储该类型及其子类的对象。
// 合法:Object 数组可以存任何对象
Object[] arr = {10, "Hello", true};
// 10 会被自动装箱为 Integer,true 装箱为 Boolean
// 它们都是 Object 的子类,所以合法
2、数组长度固定
数组一旦创建,长度就不能再改变。
例如:
int[] nums = new int[5];
表示:创建了一个长度为 5 的数组。
以后:只能存放 5 个元素。
不能变成:6 个
也不能缩小为:3 个
如果需要更多空间,只能重新创建新的数组。
后续学习 ArrayList 时,就能够实现动态扩容。
3、数组中的元素有顺序
数组中的每个元素都有自己的位置。
Java 使用索引来表示元素的位置。
例如:
int[] nums = {10, 20, 30, 40};
对应关系如下:
| 索引 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 元素 | 10 | 20 | 30 | 40 |
需要注意:Java 数组的索引从 0 开始,而不是从 1 开始。
例如:
System.out.println(nums[0]);
输出:
10
而:
System.out.println(nums[3]);
输出:
40
4、数组属于引用数据类型
很多同学学习数组时,会认为数组和 int、double 一样,也是一种普通数据类型。
实际上,并不是。数组属于 引用数据类型(Reference Type)。
例如:
int[] nums = new int[5];
这里真正创建的是:数组对象
而变量nums保存的是:数组对象在内存中的地址(引用)。
这也是为什么数组作为方法参数时,修改数组元素会影响原数组。
关于数组为什么属于引用类型,以及数组在 JVM 中是如何存储的,我们后续讲。
数组的应用场景
数组适用于:
- 保存学生成绩;
- 保存考试分数;
- 保存商品价格;
- 保存多个姓名;
- 保存每天的气温;
- 保存多个对象。
凡是需要保存一组相同类型的数据,都可以考虑使用数组。
二、为什么需要数组
学习数组之前,我们先来看一个实际需求。
假设需要保存 10 个学生的 Java 成绩。
如果不用数组,只能这样定义变量:
int score1 = 90;
int score2 = 85;
int score3 = 92;
int score4 = 88;
int score5 = 95;
int score6 = 76;
int score7 = 81;
int score8 = 89;
int score9 = 94;
int score10 = 87;
虽然能够保存数据,但会带来很多问题。
例如:
问题一:变量太多
如果有:100 个学生。
就需要:100 个变量。
程序将变得非常臃肿。
问题二:无法方便地遍历
例如:
想输出所有成绩。
只能这样写:
System.out.println(score1);
System.out.println(score2);
System.out.println(score3);
...
代码重复,非常繁琐。
问题三:统计困难
例如:
计算平均分。
需要:
(score1 + score2 + score3 + ... + score10) / 10
如果学生人数发生变化,还需要重新修改代码。
使用数组之后
使用数组,只需要定义一个变量。
int[] scores = {90, 85, 92, 88, 95, 76, 81, 89, 94, 87};
输出所有成绩:
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
计算总分:
int sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
}
可以看到:
使用数组后:
- 数据统一管理;
- 可以使用循环遍历;
- 更容易统计和计算;
- 代码更加简洁。
因此,数组最大的作用就是:使用一个变量统一管理多个相同类型的数据,提高程序的开发效率和可维护性。
三、数组的定义
在 Java 中,定义数组主要有两种方式:
- 动态初始化(Dynamic Initialization)
- 静态初始化(Static Initialization)
3.1 动态初始化
动态初始化是指:只指定数组长度,由系统自动为数组元素赋默认值。
语法格式:
数据类型[] 数组名 = new 数据类型[数组长度];
例如:
int[] nums = new int[5];
表示:创建一个能够存放 5 个整数 的数组。
此时:数组中的元素都有默认值。
0 0 0 0 0
再例如:
double[] prices = new double[3];
默认值:
0.0 0.0 0.0
3.2 静态初始化
静态初始化是指:创建数组的同时,直接指定数组中的元素。
语法格式:
数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3...};
例如:
int[] nums = new int[]{10, 20, 30, 40, 50};
数组长度由元素个数自动决定。
这里:
数组长度就是:5
静态初始化简写
Java 提供了更简洁的写法。
例如:
int[] nums = {10, 20, 30, 40, 50};
这是开发中最常见的数组定义方式。
需要注意:这种简写形式只能在声明数组的同时使用。
例如:
下面的代码是错误的:
int[] nums;
nums = {10, 20, 30};
如果数组已经声明,则必须使用完整写法:
int[] nums;
nums = new int[]{10, 20, 30};
3.3 动态初始化与静态初始化的区别
| 比较项 | 动态初始化 | 静态初始化 |
|---|---|---|
| 是否指定元素 | 否 | 是 |
| 是否指定长度 | 是 | 否,由元素个数决定 |
| 元素默认值 | 有 | 无(由程序员指定) |
| 适用场景 | 暂时不知道具体数据 | 已知全部数据 |
一般来说:
- 已知数据内容,优先使用静态初始化;
- 只知道需要多少个元素,不知道具体值,使用动态初始化。
四、数组的组成
一个完整的数组主要由以下几个部分组成。
1、数组变量
数组变量用于保存数组对象的引用(地址值)。
例如:
int[] nums = {10, 20, 30};
这里:
nums
就是数组变量。
2、数组对象
真正存放数据的是数组对象。
例如:
┌────────────────────┐
│10│20│30│
└────────────────────┘
数组对象创建后,会存储在 JVM 的堆内存中。
但是:
数组元素如果是字符串字面量(如 String[] names = {"Tom", "Jack"}),字符串本身可能存储在字符串常量池(在方法区),而不是堆内存中。
3、数组元素(Array Element)
数组中的每一个数据,都称为数组元素。
例如:
int[] nums = {10, 20, 30};
其中:
- 10
- 20
- 30
都是数组元素。
4、数组索引
索引用于访问数组中的元素。
索引从 0 开始。
例如:
nums[0]
表示第一个元素。
nums[2]
表示第三个元素。
访问数组时,索引必须在 [0, 数组长度-1] 范围内,否则会抛出 ArrayIndexOutOfBoundsException(数组索引越界异常)。
五、JVM 内存分析
前面我们已经学习了数组的定义和初始化方式。
例如:
int[] nums = {10, 20, 30};
这行代码看起来非常简单,但在 JVM 内部却发生了很多事情。
例如:
- 数组到底存放在哪里?
- nums 变量保存的是什么?
- 为什么数组属于引用数据类型?
- 为什么修改数组元素会影响原数组?
要是想知道这些,就要先了解 JVM 的内存结构。
5.1 JVM 内存区域简介
当 Java 程序运行时,JVM(Java Virtual Machine,Java 虚拟机)会向操作系统申请一块内存,用于存放程序运行过程中产生的数据。
内存是一段连续的存储空间,主要用来存储程序运行时数据的。比如:
- 程序运行时代码需要加载到内存
- 程序运行产生的中间数据要存放在内存
- 程序中的常量也要保存
- 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁
如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。比如:一个非常凌乱的房间,和一个非常整洁的房间,如果说需要找一个东西,那么在整洁的房间里面就非常容易找到。
因此JVM也对所使用的内存按照功能的不同进行了划分:

- 程序计数器 (PC Register):只是一个很小的空间, 保存下一条执行的指令的地址。
- 虚拟机栈(JVM Stack):与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
- 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似。只不过保存的内容是Native方法的局部变量。在有些版本的 JVM 实现中(例如HotSpot),本地方法栈和虚拟机栈是一起的。
- 堆(Heap):JVM所管理的最大内存区域,使用 new 创建的对象都是在堆上保存 (例如 new int[]{1, 2, 3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
- 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的的字节码就是保存在这个区域。
现在我们只简单关心堆和虚拟机栈这两块空间,后序JVM中还会更详细介绍。
5.2 栈内存
栈内存主要负责:
- 方法调用;
- 方法参数;
- 局部变量。
例如:
public static void main(String[] args) {
int age = 18;
}
变量 age 直接存放在栈内存中。
当 main() 方法执行结束后:
整个栈帧都会被销毁。
因此:局部变量也随之消失。
5.3 堆内存
堆内存主要负责存放:
- 对象;
- 数组;
- 字符串对象等引用类型数据。
例如:
int[] nums = new int[3];
执行后:
真正创建的是:数组对象
这个数组对象会存放到:堆
而:nums这个变量仍然位于:栈
因此:数组变量和数组对象不是同一个东西。
5.4 一个数组的内存分析
来看下面这段代码。
public class Demo {
public static void main(String[] args) {
int[] nums = new int[3];
}
}
程序运行后:
第一步:
执行:
new int[3]
JVM 会在堆内存中创建一个数组对象。
例如:

这里:
三个元素都为:
0
因为:
int 类型默认值就是:
0
第二步:
变量:
nums
保存在栈内存中。
它里面保存的是:数组对象的地址值
例如:

因此:数组变量保存的是地址值,而真正的数据保存在堆内存中的数组对象里。
这也是为什么数组属于引用数据类型。
5.5 修改数组元素
例如:
int[] nums = new int[3];
nums[0] = 100;
nums[1] = 200;
nums[2] = 300;
此时:
堆内存变成:

而:
栈中的 nums始终指向0x100这个地址。
因此修改的是:数组对象中的数据,而不是 数组变量
5.6 两个数组的内存分析
例如:
int[] arr1 = {1,2,3};
int[] arr2 = {4,5,6};
运行后:
栈内存:

堆内存:

可以看到:
两个数组,分别对应:两个不同的数组对象。
因此,修改:
arr1[0] = 100;
不会影响:
arr2
因为它们指向的是:不同的对象。
5.7 两个引用指向同一个数组
来看下面代码。
int[] arr1 = {1,2,3};
int[] arr2 = arr1;
很多同学会认为:创建了两个数组。
实际上并没有,这里只创建了:一个数组对象。
第二行代码只是把:arr1保存的地址值复制给arr2
内存如下:

可以看到:
虽然有两个变量:arr1、arr2
但是它们都指向:同一个数组对象。
如果执行:
arr2[1] = 999;
堆内存变成:

此时:
输出:
System.out.println(arr1[1]);
结果:
999
因为:arr1 和 arr2 访问的是同一个数组对象。
修改其中一个引用看到的数据,本质上就是修改了同一个对象,因此另一个引用也能看到最新的数据。
六、数组参数传递
例如以下代码:
public class Demo {
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void main(String[] args) {
int[] nums = {10, 20, 30};
printArray(nums);
}
}
这里的printArray(nums);把数组传递给了方法,那么:
- 是把整个数组传递进去了吗?
- 还是把数组复制了一份?
- 为什么方法中能够访问原来的数组?
6.1 数组作为方法参数
数组属于引用数据类型,因此方法参数也可以定义为数组类型。
例如:
public static void printArray(int[] arr) {
}
这里:
int[] arr
就是一个数组类型的形参。
调用时:
int[] nums = {10,20,30};
printArray(nums);
这里nums就是实参。
需要注意的是:这里传递的并不是整个数组对象,而是数组变量中保存的地址值(对象引用)。
6.2 数组参数传递过程
来看下面这段代码:
public class Demo {
public static void print(int[] arr) {
System.out.println(arr[0]);
}
public static void main(String[] args) {
int[] nums = {10,20,30};
print(nums);
}
}
程序执行过程如下。
第一步:创建数组
执行:
int[] nums = {10,20,30};
JVM 创建数组对象:

第二步:调用方法
执行:
print(nums);
很多初学者误以为:Java 把整个数组复制了一份。
其实不是。Java 只是把nums里面保存的:地址值(0x100)
复制了一份。
于是内存情况为:

可以看到,这里有两个变量:nums、arr
但是:它们保存的地址值相同。
因此共同指向:同一个数组对象。
第三步:访问数组元素
执行:
System.out.println(arr[0]);
实际上访问的是地址:0x100 的第一个元素
因此输出:10
6.3 为什么修改数组会影响原数组?
来看下面代码:
public class Demo {
public static void change(int[] arr){
arr[0] = 100;
}
public static void main(String[] args) {
int[] nums = {10,20,30};
change(nums);
System.out.println(nums[0]);
}
}
运行结果:100
为什么?
因为:

两个变量共同指向:同一个数组。
6.4 为什么重新创建数组不会影响原数组?
再来看一个容易混淆的例子。
public class Demo {
public static void change(int[] arr){
arr = new int[]{1,2,3};
}
public static void main(String[] args) {
int[] nums = {10,20,30};
change(nums);
System.out.println(nums[0]);
}
}
运行结果:10
为什么?
运行在没调用方法前:

nums 指向地址 0x100 的对象。
调用方法:
在没执行arr = new int[]{1,2,3};前,arr还是指向地址 0x100 的对象。

执行arr = new int[]{1,2,3};,创建了新数组,随后:

arr就指向了新对象的地址。

1692

被折叠的 条评论
为什么被折叠?



