本文是Java基础课程的第四课。本文主要介绍数组的基本概念、Java中如何使用数组、Java中数组的内存分配等内容,并且点明Java中基本数据类型和引用数据类型的核心区别
文章目录
一、数组的概念
之前的章节中出现的
整数类型
、
单精度浮点型
等都是
基本数据类型
,通过
一个变量表示一个数据
。在实际应用中,经常需要处理
具有相同性质
的
一批数据
。例如要处理100个学生的考试成绩,如果将它们视为100个独立的浮点型数据,将需要声明100个变量,极不方便。为此,在Java中(当然,不仅仅是Java,所有的编程语言应该都有此考虑),引入了
数组
,即
用一个变量表示一组相同性质的数据
。
1、数组
数组
是具有
相同数据类型
且
按一定次序
排列的
一组变量
的
集合体
。即用
一个变量
名表示
一组数据
。Java中,数组属于
引用数据类型
。
2、数组元素
构成一个数组的
每一个数据
称为
数组元素
。
3、数组的数据类型
即
数组元素
的
数据类型
,
一个数组中
,所有
数组元素
的
数据类型
应该是
一致
的。
4、数组元素的下标
一个数组中,各元素通过
下标
来区分。
下标
表明了
数组元素
在
数组中
的
位置
。在一个数组中,
数组下标
是用
整数
表示的,
从0开始
,
依次累加1
。
5、数组大小
数组中
元素
的
个数
叫做
数组
的
大小
,也叫
数组
的
长度
。
二、Java中如何使用数组
Java中,数组必须经过
声明
、
内存分配
、
初始化
后才能
使用
。
1、声明数组
声明一个数组的语法是:
数据类型 数组名[];
// 或
数据类型[] 数组名;
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组
int[] nums;
String words[];
char[] chars;
}
}
说明:
-
这里的
数据类型
既是
数组
的
数据类型
,同时也是规定了
数组元素
的
数据类型
。 -
数据类型
可以
是
基本数据类型
,也
可以
是
引用数据类型
。 -
数组名
遵循标示符
的
命名规范
,
建议
使用
名词
的
复数
形式。 -
数组在
声明
时
无法指定数组大小
。
2、分配内存空间
声明一个数组时仅为数组指定了数组名称和元素的类型,并未指定数组元素的个数,系统无法为数组分配存储空间。要让系统
为数组元素分配存储空间
,
必须指定
数组
元素
的
个数
。
通过
new
运算符
可以
为数组元素分配内存空间
。
为数组元素分配内存空间的语法如下:
数组名 = new 数据类型[数组长度];
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组
int[] nums;
String words[];
// 为数组元素分配内存空间
nums = new int[5];
words = new String[10];
}
}
说明:
-
数组元素的内存空间分配之后,
长度无法改变
。
定义数组和为数组元素分配内存,这
两步
可以合并在
一起
写。语法格式如下:
数据类型 数组名[] = new 数据类型[数组长度];
// 或
数据类型[] 数组名 = new 数据类型[数组长度];
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组和为数组元素分配内存空间,可以合并在一起写
char[] chars = new char[3];
}
}
3、初始化
数组声明并为数组元素分配内存空间后,必须为
数组元素初始化
,才能使用数组元素。可以
通过数组下标确定
某一个
数组元素
。
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组
int[] nums;
String words[];
// 为数组元素分配内存空间
nums = new int[5];
words = new String[10];
// 初始化数组元素
nums[0] = 1;
nums[1] = 3;
words[0] = "hello";
words[1] = "world";
}
}
如果数据
元素个数
比较
多
,可以
通过
for
循环
为数组元素
初始化
。把
for
循环的
循环变量
当作
数组
的
下标
来使用即可。
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组
int[] nums;
// 为数组元素分配内存空间
nums = new int[5];
// 初始化数组元素
for (int i = 0; i < nums.length; i ++) {
nums[i] = i;
}
}
}
说明:
-
本例
for
循环的条件中,数组
nums
调用了
length
属性,
length
属性用来
获取数组
的
大小
。数组的
下标
应该
介于
0
和
数组大小之间
,不在这个之间的下标都是非法的,访问时会抛出
数组下标越界异常
(
ArrayIndexOutOfBoundsException
)。
定义数组、为数组元素分配内存、数组元素初始化,这
三步
可以合并在
一起
写。语法格式如下:
数据类型[] 数组名 = {数组元素};
// 或
数据类型[] 数组名 = new 数据类型[]{数组元素};
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明数组、为数组元素分配内存空间、数组元素初始化,可以合并在一起写
int[] nums1 = new int[]{12, 25, 78, 56};
int[] nums2 = {12, 25, 78, 56}; // 省略 new 运算符的写法
String[] worlds = new String[]{"hello", "world", "tom", "jerry", "jack"};
}
}
说明:
-
省略
new
运算符时,
不可以
将
数组声明分开
,而将
为数组元素分配内存
和
数组元素初始化合并
在一起写,比如下面的代码:public class Test { public static void main(String[] args) { int[] nums; // nums = {12, 25, 78, 56}; // 这句代码是不合法的 } }
如果
没有
为数组元素
初始化
,
数组元素
则会使用
默认值
。
byte
、
short
、
int
、
long
类型的
数组元素
的
默认值
是
0
,
float
、
double
类型
数组
的
元素默认值
是
0.0
,
boolean
类型
数组元素
的
默认值
是
false
,
char
类型的
数组元素默认值
是
'\u0000'
,
引用类型数组元素
的
默认值
是
null
。
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 声明八个基本数据类型的数组,并为数组分配内存空间,但是不初始化数组元素
byte[] bytes = new byte[3];
short[] shorts = new short[3];
int[] ints = new int[3];
long[] longs = new long[3];
float[] floats = new float[3];
double[] doubles = new double[3];
char[] chars = new char[3];
boolean[] booleans = new boolean[3];
String[] strings = new String[3];
// 打印数组元素默认初始化的值
System.out.println("bytes[0] = " + bytes[0]);
System.out.println("shorts[0] = " + shorts[0]);
System.out.println("ints[0] = " + ints[0]);
System.out.println("longs[0] = " + longs[0]);
System.out.println("floats[0] = " + floats[0]);
System.out.println("doubles[0] = " + doubles[0]);
System.out.println("chars[0] = " + (int) chars[0]);
System.out.println("booleans[0] = " + booleans[0]);
System.out.println("strings[0] = " + strings[0]);
}
}
4、应用案例
4.1、案例1
下面是一个示例:
public class Test {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
// 声明一个数组存储5名新兵的身高
int[] heights = new int[5];
// 存储最高身高值
int max;
// 存储最低身高
int min;
// 存储总身高,用于计算平均身高
int sum;
// 接受键盘输入的5名新兵的身高
System.out.println("请输入五名新兵的身高:");
for (int i = 0; i < heights.length; i++) {
System.out.println("请输入第" + (i + 1) + "名新兵的身高");
heights[i] = input.nextInt();
}
// 对身高进行比较
max = heights[0];
min = heights[0];
sum = heights[0];
for (int i = 1; i < heights.length; i++) {
if (heights[i] > max) {
max = heights[i];
}
if (heights[i] < min) {
min = heights[i];
}
sum += heights[i];
}
System.out.println("最高的身高为:" + max);
System.out.println("最低的身高为:" + min);
System.out.println("平均的身高为:" + sum / heights.length);
}
}
说明:
-
本例接收键盘输入的五名新兵的身高,并存储到一个
int
型数组中,之后遍历数组,找出五名新兵中最高的身高、最低的身高,另外还计算了五名新兵的平均身高。
4.2、案例2
下面是一个案例:
public class Test {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
// 存储五个新兵的身高
int[] heights = new int[5];
// 循环输入五个新兵的身高
for (int i = 0; i < heights.length; i++) {
System.out.println("请输入第" + (i + 1) + "个新兵的身高:");
heights[i] = input.nextInt();
}
// 定义临时变量
int temp;
// 进行冒泡排序
for (int i = 0; i < heights.length - 1; i++) { // 控制比较多少轮
for (int j = 0; j < heights.length - 1 - i; j++) { // 控制每轮比较多少次
if (heights[j] > heights[j + 1]) {
//进行两数交换
temp = heights[j];
heights[j] = heights[j + 1];
heights[j + 1] = temp;
}
}
}
// 将排序后结果进行输出
System.out.println("从低到高排序后的输出:");
for (int height : heights) {
System.out.println(height);
}
}
}
说明:
-
本例接收键盘输入的五名新兵的身高,并存储到一个
int
型数组中,之后对该数组进行
冒泡排序
,最后按从低到高排序后的顺序打印出五名新兵的身高。 -
本例中打印5名新兵身高时,使用了
For-Each
循环
(或者称
加强型循环
),它能在不使用下标的情况下
遍历数组
(或
集合
)。
For-Each
循环
的基本语法如下:for(数组元素的数据类型 数组元素的临时变量名 : 数组名称) { // 循环体; }
-
冒泡排序
是一种
常用
的
排序算法
,即通过对
相邻元素
的大小进行
比较
,每一次将
最小或最大
的数
放到最后
面,
最终实现
从小到大或从大到小
排序
。下面是冒泡排序的例子图示:
三、由Java中的数组到内存
1、初步了解Java的内存管理
有些编程语言编写的程序会直接向操作系统请求内存,而
Java
语言为
保证
其
平台无关
性,并
不允许程序直接向操作系统请求内存
,而是
由Java虚拟机
来完成这一操作,开发者
只需
要
关心Java虚拟机
是
如何管理内存
空间的,而
不用关心
某一种
操作系统
是
如何管理内存
的。
Java虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,大致有:
-
程序计数器
,也有称作为
PC寄存器
,在JVM中用来指示要执行哪条指令,程序计数器是每个线程所私有的。 -
栈
,也被称作
Java栈
或者
虚拟机栈
,Java栈是Java方法执行的内存模型。存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。虚拟机栈也是每个线程所私有的。 -
本地方法栈
,与栈类似,
HotSopt虚拟机
中直接就把本地方法栈和Java栈合二为一。 -
堆
,Java中的堆是用来存储对象本身以及数组本身的。堆是被所有线程共享的,在JVM中只有一个堆。 -
方法区
,存储类信息、静态变量、常量以及编译器编译后的代码等。方法区同堆一样,也是被线程共享的区域。
在Java程序运行过程中,
栈内存
和
堆内存
是
最需要关注
的内存区域。
2、Java内存中的数组
下面通过一个案例说明数组在JVM中的内存分配情况。
观察下面的代码:
public class Test {
public static void main(String[] args) {
int[] nums;
nums = new int[2];
nums[0] = 1;
nums[1] = 5;
}
}
Java将数组名称存储在栈中,数组元素分配在堆中。下面将用图示粗略的理解上面这段代码执行时JVM中的内存分配过程。
第一步,
Test
类的
main
方法开始执行,创建该
方法对应
的
栈帧
,并将创建的栈帧在
栈内存
中
压栈
(存入栈顶):
第二步,执行
int[] nums;
,在
main
方法对应栈帧的
局部变量表
中,为数组名称
nums
分配
一块
内存
:
第三步,执行
nums = new int[2];
时,首先,JVM会在
堆中分配
能够
连续存储
两个
int
类型数据的
内存
空间,之后,
赋值操作
会将堆内存中已经分配好的两个连续内存空间的
首地址
存储到栈内存
main
方法
对应栈帧
的
局部变量表
中:
第四步,执行
nums[0] = 1; nums[1] = 5;
,在堆内存分配好的两个连续内存空间中
存入
nums[0]
对应的
整型值
1
和
nums[1]
对应的
整型值
5
:
第五步,
main
方法结束,栈内存中
main
方法对应
的
栈帧出栈
,
堆内存
被
回收
(事实上,本例中
main
方法结束意味着整个Java程序结束了,JVM将自己管理的内存交还给操作系统):
3、基本数据类型和引用数据类型
Java将数据类型分为两大类,一类是
基本数据类型
,一类是
引用数据类型
。这两大类数据类型最
核心
的
区别
是:
基本数据类型 的变量中 存储 的是 真实的数据 ; 引用数据类型 的变量中 存储 的是 内存地址编号 (即引用了某一内存地址)!!! |
---|
观察下面的代码:
public class Test {
public static void main(String[] args) {
// 基本数据类型的变量赋值
int num1, num2;
num1 = 3;
num2 = num1;
num2 = 4;
System.out.println("num1 = " + num1);
// 引用数据类型的变量赋值
int[] nums1, nums2;
nums1 = new int[1];
nums1[0] = 3;
nums2 = nums1;
nums2[0] = 4;
System.out.println("nums1[0] = " + nums1[0]);
}
}
说明:
-
System.out.println("num1 = " + num1)
将打印出
num1 = 3
。因为
基本数据类型的变量中存储的是真实的数据
,基本数据类型的变量相互赋值时,
拷贝
的是
真实的数据
,故改变变量
num2
中存储的值不会影响变量
num1
中存储的值。如下图:
-
System.out.println("nums1[0] = " + nums1[0])
将打印出
nums1[0] = 4
。因为
引用数据类型的变量中存储的是内存地址编号
,引用数据类型的变量相互赋值时,
拷贝
的是
内存地址编号
,本例中,变量
nums1
和变量
nums2
最终引用了同一内存地址,改变数组
nums2
中某一数组元素的值即是改变数组
nums1
中数组元素的值。如下图:
4、二维数组
日常工作中涉及的许多数据由若干行若干列组成,例如行列式、矩阵、二维表格等,为了描述和处理其值的某个数据,需要两个下标,即行下标和列下标。有些情况下可能需要3个或多个下标,如描述三维空间中各点的位置就需要3个下标。为了解决这一问题,Java中可以使用
多维数组
。
对于Java中的二维数组或者多维数组而言,并没有什么玄妙,以二维数组为例,只需牢记:
二维数组只是一个特殊的一维数组,特殊在这个一维数组的每一个元素的值都是一个指向另一个一维数组的引用
。
一张图示即可说明二维数组在JVM中的内存分配情况:
多维数组依次类推即可理解。
5、应用案例
5.1、案例1
下面是一个示例:
public class Test {
public static void main(String[] args) {
//定义两个二维数组nums1和nums2
int nums1[][], nums2[][];
//二维数组nums1分配内存并初始化数据
nums1 = new int[][]{{6, 8}, {3, 9}};
//二维数组nums2分配内存
nums2 = new int[2][2];
//数组nums1复制到数组nums2
for (int i = 0; i < nums1.length; i++) {
for (int j = 0; j < nums1[i].length; j++) {
nums2[i][j] = nums1[i][j];
}
}
System.out.println("复制后的数组nums2内容如下:");
for (int[] nums : nums2) {
for (int num : nums) {
System.out.print(num + "\t");
}
System.out.print("\n");
}
}
}
说明:
-
本例中声明了两个
int
类型的二维数组
nums1
和
nums2
,给数组
nums1
的元素分配了内存空间并进行了初始化,同时给数组
nums2
的元素分配了内存空间但并没有初始化,通过两层
for
循环将数组
nums1
中的每个元素值拷贝到数组
nums2
的对应位置中。
5.2、案例2
案例2演示使用二维数组生成并打印杨辉三角。
下面是一个示例:
public class Test {
public static void main(String[] args) {
// 接收键盘输入的数字,作为要生成及打印的杨辉三角的行数
Scanner input = new Scanner(System.in);
System.out.println("请输入行数:");
int rowNum = input.nextInt();
if (rowNum < 3) {
System.out.println("请输入大于2的整数");
} else if (rowNum > 14) {
System.out.println("行数太大了");
} else {
// 声明二维数组,用来存储杨辉三角,并为该二维数组的第一维分配内存空间
int[][] triangle = new int[rowNum][];
for (int i = 0; i < triangle.length; i++) {
// 为二维数组的第二维分配内存空间
triangle[i] = new int[i + 1];
for (int j = 0; j < triangle[i].length; j++) {
if (j == 0 || j == triangle[i].length - 1) {
// 每行的第一个数字和最后一个数字是1
triangle[i][j] = 1;
}else {
// 其他位置的数字是其两肩的数字之和
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
}
}
System.out.println("您要打印的杨辉三角如下:");
for (int[] row : triangle) {
for (int k = 0; k < rowNum - row.length; k ++) {
System.out.print("\t");
}
for (int element : row) {
System.out.print(element + "\t\t");
}
System.out.print("\n");
}
}
}
}
说明:
- 杨辉三角的规则是:每行的第一个数字和最后一个数字是1,其他位置的数字是其两肩的数字之和。