本文是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,其他位置的数字是其两肩的数字之和。