第5章〓数组 本章练习 在Java程序中,经常需要存储大量的、具有相同性质的数据,例如,需要输入60名同学的成绩,并计算其平均值与方差。在这种情况下,Java程序需要实现这60个数的输入与存储,然后进行分析处理。为了完成这个任务,程序声明60个简单变量分别存储这些数据显然是不太现实的。对于这一问题,数组就是一种较好的解决方法。 5.1Java数组 在程序开发设计中,经常需要存储大量相同类型的数据。针对这个问题,Java和大多数其他高级语言一样提供了数组来保存这组数据。数组是相同类型数据的集合,集合的名字就是数组名。 数组用一组连续的内存空间存储数据,每个数据都是数组中的一个元素,数组中的每个元素都有对应的下标,下标是从0开始的整数。由于数组是用一片连续的内存单元存放数据,通过数组名与下标就可以定位并访问数组中的任何一个元素。数组的这种能够被快速访问的特点,让程序代码拥有非常高的访问效率,也使得数组成为程序设计中最常用的数据结构。 在Java语言中,数组与对象一样都是引用类型的变量,需要用关键词new创建数组。Java数组具有如下特点。 (1) 数组中元素的类型相同。 (2) 数组中所有的元素存放在一块连续的内存空间中。 (3) 通过数组名与下标可以访问每个元素,下标从0开始。 (4) 数组的大小一旦定义以后,不可再动态增大或减小。 Java语言提供了一维数组与多维数组,在编程时可以根据需要创建与使用。 观看视频 5.2一维数组 数组是用来存储批量数据的,一个数组中的元素应该都属于同一种数据类型。 同一个数组中的元素在内存中是按照顺序连续存放在一片空间内,因此可以按照它们在内存中的顺序进行编号,也就是每个数组元素对应一个下标,并按照这个数组下标来进行存取访问。数组需要创建后才能访问,数组的下标从0开始,数组的长度就是数组中元素的个数,其大小在数组初始化之后就固定下来。 假设有一个具有10个元素的双精度浮点数数组array,并对该数组完成数据初始化,则可以用图51展示一个双精度类型的数组变量array在内存空间中的存储方式。 图51数组存储空间示意图 Java的数组可以有多个维度,其中一维数组是数组应用的基础,有了一维数组的概念之后,可以很容易地把相关概念推广到多维数组。本节将以一维数组为例介绍如何声明数组、创建数组和存取访问数组中的数据。 5.2.1数组的声明 Java语言的数组属于引用类型,为了正确使用数组,必须声明一个数组变量来引用数组。在声明数组变量时,需要给出数组的名称,以及数组中的数据元素所属的数据类型。声明数组的语法格式有如下两种: 格式1: 数组元素类型 数组名[]; 格式2: 数组元素类型[] 数组名; 其中格式1和C语言的数组声明语法兼容,这种格式从语法上来讲虽然没有什么问题,但在Java中推荐采用格式2,即把[]放在数组名前面。 数组元素的类型本质上是定义数组中每个元素的类型,该类型可以是Java中的任意类型,既可以是基本数据类型,也可以是类等各种引用数据类型。数组中所有元素的数据类型都是完全一样的。例如,下面的代码声明了一个整数数组numbers: int[] numbers; 再比如,可以声明一个数组,其数据元素的类型为引用类型,例如使用用户自定义的Student类,定义一个Students数组的声明如下: Student[] students; 和C语言等高级语言不同,Java在声明数组时,[]内不能指定长度,这时的数组大小尚不确定,也没有分配相应的数据存储空间。如果想要对数组进行正确的存取访问,必须要创建数组,也就是指定数组长度,分配相应的数组存储空间。 5.2.2创建数组 在Java中,数组是一种引用类型,因此它和基本数据类型变量的使用不一样。在声明一个数组时,并不在内存中给数组分配任何空间来存放数组中的元素,仅仅声明了一个引用数组的地址变量(又被称为数组的引用)。 数组声明的目的只是告诉系统一个新的数组的名称和类型,数组名本身不能存放任何数组元素,这意味着该数组变量并没有引用任何数据空间,数组变量当前的值为空(null)。因此,使用数组之前,需要先使用new关键字创建数组,为数组分配指定长度的连续内存空间,并把这片连续内存空间的起始地址赋值给数组变量。 1. 创建数组的语法形式 通常在声明数组的同时可以进行创建数组的操作,声明并创建数组,分配内存空间的语句格式如下: 数组名 = new 数组元素类型[数组的长度]; 数组名就是数组变量名,数组的长度就是数组的容量大小,也就是数组中元素的个数,它是一个整数。数组长度存储在数组的length属性中,可以通过数组名.length引用。 也可以单独进行创建数组的操作,例如在声明了数组numbers与students之后,可以使用下面的语句创建数组: numbers= new int[4]; students = new Student[3]; new int[4]给数组numbers分配了4个整数的连续内存空间,用来保存4个int类型的数据。分配空间之前的numbers数组变量不引用任何空间,其值为null,如图52(a)所示。使用new关键字分配4个连续整数空间后,numbers变量将存放一个空间地址(又称为引用),也即被分配的4个整数的连续空间的起始位置,这个引用地址的取值由JVM自动分配,整个过程如图52(b)所示。 图52创建整数数组分配内存空间 【例51】从键盘输入10个数,计算它们平均值和高于平均值的数量。 import java.util.*; public class Example5_1 { public static void main(String[] args) { final int NUMBER_OF_ELEMENTS = 10; double[] numbers = new double[NUMBER_OF_ELEMENTS]; double sum = 0; System.out.println("下面请输入"+NUMBER_OF_ELEMENTS+"个数"); Scanner input = new Scanner(System.in); for (int i = 0; i < NUMBER_OF_ELEMENTS; i++) { System.out.print("请输入一个数: "); numbers[i] = input.nextDouble(); sum += numbers[i]; } double average = sum / NUMBER_OF_ELEMENTS; int count = 0;// 存储高于平均值的数的个数 for (int i = 0; i < NUMBER_OF_ELEMENTS; i++) if (numbers[i] > average) count++; System.out.println("这些数字的平均值是:" + average); System.out.println("其中高于平均值的数字的数量是:" + count); } } 例51声明了一个一维数组numbers,数组元素的数量用一个常量NUMBER_OF_ELEMENTS表示,并给NUMBER_OF_ELEMENTS赋予初值10,数组元素的数据类型是double,程序首先用一个for循环从键盘读取10个数字并计算它们的平均值,然后再用另一个for循环计算高于平均值的数字的个数。 2. 对象数组的创建 numbers数组中的元素都是基本数据类型int,这种数组一旦完成创建工作,就可以马上对它进行存取访问操作。但是students数组比较特殊,数组中的元素本身也是引用类型Student类,数组中的元素是3个对象,它们都属于引用类型,这是一种对象数组。创建这种元素为引用类型的数组则需要额外进行操作。 当使用students=new Student[3]创建数组时,系统为数组students分配了3个连续空间,但它们仅仅可以被用来存放3个Student对象的引用,3个Student对象本身并没有被创建和分配空间。为了正确对数组中的数据进行存取,还需要为students数组中的这3个数组元素分别构造Student对象实例,否则数组中的每一个元素的引用为null,并不能进行正确的数据存取访问,这时的students数组如图53所示。 图53创建students数组分配内存空间 因此在使用数组元素为引用类型(如某种对象)的数组时,除了需要声明数组和创建数组,还必须进一步对数组中的每一个数据元素创建内存空间(如构造对象实例),否则在程序运行时将会抛出一个NullPointerException的异常。例52的程序展示了如何创建并存取数据元素为对象的数组。 【例52】创建一个元素为引用类型的数组,并进行访问存取。 class Student { String name; int age; String major; Student(String name,int age,String major){ this.name = name; this.age = age; this. major = major; } void study() { System.out.println(name+"在学习"+major); } } public class Example5_2{ public static void main(String[] args) { Student[] students = new Student[3]; // students[0].age=20; 这行代码将不能正常执行 students [0] = new Student("张三",18,"计算机"); students [1] = new Student("李四",20,"电子工程"); students [2] = new Student("王五",19,"光学工程"); students[1].age = 21;//修改第2名学生的年龄为21 for(int i=0;i< students.length;i++) { //遍历所有学生 System.out.println(students[i].name+","+students[i].age); students[i].study(); } } } 例52首先定义了一个学生类Student,包含三个成员变量(name、age、major)、一个构造方法Student(String name,int age,String major)和一个成员方法study()。在main()入口方法中则声明并创建了具有3个元素的Student类型的数组,但这时并不能立即直接对数组进行存取,例如对数组中下标为0的元素进行赋值操作,也就是程序中被注释的“students[0].age=20;” 这行代码将不能正常运行。例52在创建完Student对象数组后,逐一为数组中的3个元素分别构造对象实例,分配空间,然后才能正常对数组进行遍历访问等存取操作。 5.2.3数组长度 在创建数组的时候,数组长度可以是一个整数常量,也可以是整数变量,其值决定了数组中元素的个数。一旦创建好数组并分配了内存空间,就不能再改变它的长度。在使用数组的过程中,可以使用“数组变量名.length”的语法形式来获取数组的长度值,length是数组对象的一个成员属性。 例如,在下面的代码中,通过使用a.length就可以得到数组变量a的长度。 int n=4; int[] a= new int[n]; int len=a.length; System.out.prinln("数组长度为:"+len); 上面的代码首先声明并创建了具有n个元素的整数数组,n的值为4,然后再通过程序读取数组a的长度,并赋值给整数变量len,最后打印输出len的值,输出结果为4。 5.2.4数组的初始化 数组创建后,如不对其进行初始化,系统会根据其类型自动为元素赋初始值。 如果数组的元素是基本类型,数组中元素默认初始化的值是基本类型的默认值,基本数据类型的数组元素的默认初值如表51所示。如果数组元素是对象等引用数据类型,数组元素的默认初值是null。 表51基本数据类型的数组元素的默认初值 数 据 类 型 默 认 初 值 数 据 类 型 默 认 初 值 byte 0 char \\u0000 int 0 float 0.0 short 0 double 0.0 long 0 boolean false 例如,在使用int[] a=new int[4]创建数组a之后,数组a中就有4个整数,每个整数的值都初始化为0。 也可以使用赋值语句对数组元素进行初始化赋值。例如下面的语句给数组a的4个元素分别进行了初始化赋值。 a[0]=12; a[1]=30; a[2]=18; a[3]=55; 为了简化上面这种烦琐的赋值操作,Java语言允许在声明数组的同时就完成数组的初始化操作,例如: int[] a = { 12, 30, 18, 55 }; 这种方法比逐一为数组中每个元素分别赋值要简洁得多,而且自动创建数组的存储空间,不再需要使用new创建数组,其数组长度由{ }中元素的个数决定,是一种很常见的初始化操作。 对于数组元素为引用类型的数组,同样也可以通过这种方法将数组的声明、创建和初始化操作合并在一起。例如将例52中的数组变量students的声明进行如下修改,就可以省去分别为数组中的每个元素赋初值的操作。 Student[] students = new Student[]{ new Student("张三",18,"计算机"), new Student("李四",20,"电子工程"), new Student("王五",19,"光学工程") }; 需要强调的是,数组的初始化操作并不是必需的。对于数组元素为基本类型的数组来说,没有经过初始化操作也能被正常地存取。 但是,对于数组元素为引用类型的数组则不同,正如例52所示,如果没有在数组声明的同时进行初始化,则需要在访问数组之前对数组中的每一个元素赋初值,分配相应的内存引用空间,否则不能对该数组元素进行正确的存取操作。这是初学者在使用数组时很容易忽视的一个地方。 5.2.5访问数组 在创建数组并初始化之后,就可以对数组中的元素进行存取访问了。由于数组中的元素在内存中是连续有序存放的,因此数组的访问可以通过其在存储空间中的顺序编号,也就是下标来完成,数组下标是从0开始的。访问数组元素的语法格式如下: 数组名[数组元素下标] 例如下面的语句给numbers数组中的第3个元素赋值10,然后打印输出到控制台: numbers[2]=10; System.out.println("numbers数组中的第3个元素取值是:"+numbers[2]); 使用数组时要注意下标值不要超出范围,数组元素的下标范围是[0,数组长度-1]。程序执行时如果访问数组超出这个范围将会抛出一个ArrayIndexOutBoundException异常。 数组是连续有序地存放在内存空间中的,在实际应用中,经常可以借助循环来控制对数组元素的访问,访问数组的下标随循环控制变量的变化而变化。因此,数组的访问往往采用for循环,这是因为数组的长度一般都是已知的。例如: int n=100; int[] a = new int[n]; for (int i = 0; i <= n-1; i++) { a[i] = i*i; } 在上面的程序中数组a采用循环变量i作为下标来遍历访问每一个数组元素,数组的长度为100,因此数组下标i的取值范围为0~99的整数,数组的访问只能取a[0]~a[99]的变量。 对于类似数组这种批量数据的遍历操作,Java语言还提供了其他循环结构。可以用枚举的方法处理数组中的每个数据元素,而不必指定下标值。这种循环通常被称作foreach循环。 采用foreach循环来访问数组的语句格式为: for(元素类型 变量名:数组名){ //操作数组元素 } 其中,变量名代表一个临时变量,用来暂存数组中的每一个元素,并在循环体中执行相应语句来操作该临时变量。例如,要输出数组a中的所有元素值,可以用如下的代码段: for (int element : a) { System.out.println( element ); } 这段代码打印输出数组中每一个元素的值,每输出一个元素就换一行。 可以把上面的foreach循环代码理解为“依次循环访问数组a中的每一个元素,将该元素赋值给一个临时变量element并在循环体中进行访问或处理”。实际上,这种方法和下面传统的for循环执行的效果是等价的。 for (int i = 0; i < 100; i++) { System.out.println( a[i]); } 对于数组操作,采用foreach循环更加简洁,更不容易出错,因为不需要为数组下标起始值和终止值操心。但是,在很多情况下仍然需要使用传统的for循环,例如有的时候可能并不需要遍历整个数组,在这种情况下使用下标值来指定访问数组中的部分元素可能更加方便。 当然,采用其他的循环语句来进行数组访问也是一种常见手段,例53采用了多种循环结构对数组中的批量数据进行存取。 【例53】从键盘输入全班同学的成绩,并计算平均值与标准差。 假设全班有n名同学,可以定义一个数组来存放n名同学的成绩。采用下面的公式计算均值与标准差。这里假定n小于200,且当输入成绩小于0时,表示输入结束。 均值(avg)的计算: avg=1n∑ni=1xi 标准差(sd)的计算: sd=1n-1∑n-1i=0(xi-avg)2 程序代码如下: import java.util.Scanner; public class Example5_3 { static int size = 200; public static void main(String[] args) { float[] x = new float[size]; float avg, sd, t,total; int i=0,n; Scanner sc = new Scanner(System.in); System.out.println("输入一名同学的成绩"); total=0; t=sc.nextFloat(); while (i<200 && t>=0){ x[i]=t; i++; total=total+t;//计算总成绩 System.out.println("输入一名同学的成绩,当输入小于0时,结束成绩输入"); t=sc.nextFloat(); } n=i;//共输入了n个数据 if(n>0){ avg=total/n;//计算平均成绩 //计算标准差 sd=0; for (i=0;i1){ sd=(float)Math.sqrt(sd/(n-1)); System.out.println("学生人数为:"+n); System.out.println("平均成绩为:"+avg); System.out.println("成绩的标准差:"+sd); } } } } 程序的一次运行过程及结果如下: 输入一名同学的成绩 88 输入一名同学的成绩,输入负数时,结束成绩输入 56 输入一名同学的成绩,输入负数时,结束成绩输入 0 输入一名同学的成绩,输入负数时,结束成绩输入 99 输入一名同学的成绩,输入负数时,结束成绩输入 -5 学生人数为: 4 平均成绩为: 60.75 成绩的标准差: 44.417526 在该例中,输入控制用while语句实现,在循环中用变量i记录数组元素的下标。 观看视频 5.3数组应用 数组作为一种引用类型,其使用方法与基本数据类型,如int、float等存在许多差异,在很多时候,如数组之间赋值、数组被用作方法参数,数组之间的操作不是基本数据类型变量的“传值”,而是通过所谓的“传引用”方式来完成的。下面结合示例来说明数组的这些特殊应用。 5.3.1数组的赋值 1. 数组赋值 在Java语言中,同类型的数组之间可以用“=”赋值,实现把一个数组变量赋值给另外一个数组的功能。在这种情况下,由于数组本身是引用类型,其值是数组元素的内存空间首地址,因此,数组之间直接赋值,实际上就是数组引用的赋值。例如: int[] num = {4, 6, 3, 7}; int[] numCopy = {8, 1, 0, 9}; numCopy = num; numCopy[2]=5; System.out.println(num[2]); 上面的代码将数组num赋值给数组numCopy,也就是把数组num的引用赋值给numCopy数组,赋值完成后,两个数组都引用了相同的存储空间,因此在修改了numCopy数组的第三个元素的值之后,num数组相应也会发生变化。上面的语句运行后,最后的输出为5。 如图54(a)所示,在赋值前num和numCopy指向了内存中不同的空间,是两个完全不同的数组; 而在赋值之后,num和numCopy实际上指向了同一个内存空间,如图54(b)所示。因此,当修改数组元素numCopy[2]时,num[2]的值也就随之改变了。 图54数组变量赋值过程示意图 2. 数组元素赋值 如果仅仅需要将数组中的元素的数值复制给另外一个数组,同时又要保证两个数组保持各自不同的内存引用空间,可以编写一个for循环,依次将原来数组中每一个元素的值赋值给新数组。例如下面的语句片段: int[] num = {4, 6, 3, 7}; int[] numCopy = {8, 1, 0, 9}; for (int i = 0; i < num.length; i++) { numCopy [i] = num [i]; } 和图54不同,图55中的for循环执行后,num和numCopy两个数组中的元素内容完全一样,但数组变量仍然引用各自不同的内存空间。 图55数组循环复制元素过程示意图 还可以采用更加简便的方法实现数组元素的内容复制,也就是采用System类的arrayCopy()方法,其格式如下: System.arraycopy(src, srcPos, dest, destPos, length) 该方法可以将src源数组中从srcPos开始的连续length个元素复制到dest数组的destPos开始的位置,且src和dest数组指向不同的内存空间。于是,上面的数组复制代码片段可以重写为如下: int[] num = {4, 6, 3, 7}; int[] numCopy = {8, 1, 0, 9}; System.arraycopy(num, 0, numCopy, 0, num.length); 5.3.2数组参数传递 和其他高级语言类似,在Java语言中数组变量也可以作为参数传递给方法。但是和整数、浮点数等基本类型的变量传值不同,数组是一种引用类型,传递的是数组的引用,在使用时需要注意两者之间的区别。 当一个方法的参数是基本类型(如整数、浮点数)时,方法中对形式参数的任何修改不会影响调用时的实际参数。然而,当参数是数组变量等引用类型时,在方法中对形式参数作出的修改,将导致实际参数也发生相应的变化。例如下面的例54。 【例54】数组作为方法的参数示例。 public class Example5_4 { public static void main(String[] args) { int x = 1; int[] y = new int[10]; m(x,y); System.out.println("x is " + x); System.out.println("y[0] is " + y[0]); } public static void m(int number, int[] numbers) { number = 1001; numbers[0] = 5555; } } 程序的运行结果如下: x is 1 y[0]is 5555 在例54的程序中,方法m()修改了两个形式参数number和numbers的值。在main()中调用了m()方法,并传入了实际参数x和y。m()方法执行后,x的值并没有改变,而数组变量y由于是引用类型,在调用m()方法后,其数组元素的值也相应发生了变化。 5.3.3数组作为方法的返回值 数组不但可以是方法的参数,也可以作为一个方法的返回值。例如例55程序中的方法reverse(),它将参数传进来的数组变量list中的数据顺序倒置,并将倒置后的新数组result返回给调用者。 【例55】数组作为方法的返回值。 public class Example5_5{ public static int[] reverse(int[] list) { int[] result = new int[list.length]; for (int i = 0, j = result.length - 1; i < list.length; i++, j--) { result[j] = list[i]; } return result; } public static void main(String[] args) { int[] x={1,2,3,4,5,6,7,8}; x=reverse(x); System.out.println("返回后的x数组中元素:"); for (int t:x) System.out.print(t+"\t"); } } 程序的运行结果如下: 返回后的x数组中元素: 87654321 5.3.4一维数组编程举例 数组是一种有序存储的数据集合,排序是这种数据结构的常见操作,例56和例57为数组在排序方面的相关应用。 【例56】从键盘逐个输入学生的成绩,并存储到数组,然后按从小到大的顺序进行排列并输出。 import java.util.Scanner; public class Example5_6{ public static void main(String[] args) { int numberOfStudent; int scores[]; Scanner sc = new Scanner(System.in); System.out.print("请输入学生人数: "); numberOfStudent = sc.nextInt(); scores = new int[numberOfStudent]; for (int i = 0; i < numberOfStudent; i++) { System.out.print("请输入第 " + (i + 1) + " 位学生的成绩:"); scores[i] = sc.nextInt(); } // 双重循环,进行冒泡排序 for (int j = 0; j < scores.length; j++) { for (int k = 0; k < scores.length - j - 1; k++) { int l; if (scores[k] > scores[k + 1]) { l = scores[k]; scores[k] = scores[k + 1]; scores[k + 1] = l; } } } System.out.println("学生成绩从小到大排列如下:"); for (int i = 0; i < numberOfStudent; i++) System.out.print(scores[i]+"\t"); } } 程序的运行结果如下: 请输入学生人数: 5 请输入第1位学生的成绩: 78 请输入第2位学生的成绩: 98 请输入第3位学生的成绩: 79 请输入第4位学生的成绩: 66 请输入第5位学生的成绩: 93 学生成绩从小到大排列如下: 6678799398 例56把学生的成绩数据保存在一个整数数组中,学生的个数也就是数组的长度,由键盘录入。程序首先用循环语句从键盘读入所有学生的成绩到数组中,然后采用了一种经典的排序算法——冒泡排序法来对数组中的元素按从小到大的顺序进行排序,最后再输出整个数组的数据。 【例57】在一个有序数组中插入一个数据并使该数组保持有序(默认数组为升序排列)。 import java.util.Scanner; public class Example5_7 { public static void main(String[] args) { //在有序数组中插入一个元素,使得插入后所有的元素也保持有序,这里以升序为例 int[] number={1,5,7,9}; //定义一个新数组,长度为老数组的长度+1 int[] number1=new int[number.length+1]; int temp; System.out.println("要插入元素的有序数组如下所示:"); for (int i = 0; i < number.length; i++) { System.out.print(number[i]+"\t"); } System.out.println(); System.out.print("请输入插入的数:"); Scanner scanner=new Scanner(System.in); int insert_number=scanner.nextInt(); for (int i = 0; i < number.length; i++) { number1[i]=number[i]; } number1[number1.length-1]=insert_number;//先将待插入数据放到数组末尾 //通过比较,找到插入数据的位置 for (int i = number1.length-1;i >0 ; i--) { if (number1[i]