第3章 数组与方法 本章学习目标  了解Java数组的定义。  掌握Java数组的常用操作。  掌握Java的方法定义与使用。  掌握Java方法重载与递归。  理解Java数组的引用传递。 数组能够用来存储固定大小的同类型元素,当在Java开发的过程中遇到需要定义多个相同类型的变量时,使用数组将会是一个很好的选择。例如,要存储80名学生的成绩,如果定义80个变量,会耗费大量的时间和精力,而此时如果使用数组不仅能够达到异曲同工之妙,还会提高代码的简洁性和扩展性。Java中的方法是代码语句的集合,把这些语句组合在一起能够执行某个特定的功能,而且当遇到有些代码需要反复使用的情况时,也可以将代码声明成一个方法,以供程序反复调用,本章将对数组和方法的使用进行详细讲解。 3.1数组 数组是一种数据结构,可以用来存储一系列的数据项,它是按照一定顺序排列的同种类型元素的集合。数组中的每一个元素都可以通过数组名和下标来确定,根据数组的维度可以分为一维数组、二维数组和多维数组等。使用数组时,可以通过数组元素的索引(下标)来访问数组元素,如数组元素的赋值和取值。 3.1.1数组的定义 在Java中数组是相同类型元素的集合,可以存放上千万个数据,在一个数组中,数组元素的类型是唯一的,即一个数组中只能存储同一种数据类型的数据,而不能存储多种数据类型的数据。数组一旦定义完成就不能再修改数组长度,因为数组在内存中所占的大小是固定的,所以数组的长度不能改变,如果要修改就必须重新定义一个新数组或者引用其他的数组,因此数组的灵活性较差。 数组是可以保存一组数据的一种数据结构,它本身也会占用一个内存地址,因此数组是引用类型。定义数组的语法格式如下。 数据类型[] 数组名; 对于数组的声明也可用另外一种形式,其语法格式如下。 数据类型 数组名[]; 上述两种不同语法格式声明的数组中,“[ ]”是一维数组的标识,从语法格式可以看出,它既可放置在数组名前面,也可以放在数组名后面。面向对象程序设计更侧重放在前面,保留放在后面是为了迎合C程序员的使用习惯,在这里推荐使用第一种格式。下面演示不同数据类型的数组声明,具体示例如下。 int[] a;   // 声明一个int类型的数组 double b[];   // 声明一个double类型的数组 上述示例中声明了一个int类型的数组a与一个double类型的数组b,数组名是用来统一这组相同数据类型的元素名称,数组名的命名规则和变量相同。 第 3 章 数组与方法 Java语言程序设计(第2版) 3.1.2数组的初始化 在Java程序开发中,使用数组之前都会对其进行初始化,这是因为数组是引用类型,声明数组只是声明一个引用类型的变量,并不是数组对象本身,只要让数组变量指向有效的数组对象,程序中就可使用该数组变量来访问数组元素。数组初始化,就是让数组名指向数组对象的过程,该过程主要分为两个步骤: 一是对数组对象进行初始化,即为数组中的元素分配内存空间和赋值; 二是对数组名进行初始化,即将数组名赋值为数组对象的引用。 通过两种方式可对数组进行初始化,即静态初始化和动态初始化。下面将演示这两种方式的具体语法。 1. 静态初始化 静态初始化是指由程序员在初始化数组时为数组每个元素赋值,由系统决定数组的长度。 数组的静态初始化有两种方式,具体示例如下。 Int[] array; //声明一个int类型的数组 array = new int[]{1,2,3,4,5}; //静态初始化数组 int[] array = new int[]{1,2,3,4,5}; //声明并初始化数组 对于数组的静态初始化也可简写,具体示例如下。 Int[] array ={1,2,3,4,5}; //声明并初始化一个int类型的数组 上述示例中静态初始化了数组,其中大括号包含数组元素值,元素值之间用逗号“,”分隔。此处注意,只有在定义数组的同时执行数组初始化才支持使用简化的静态初始化。 2. 动态初始化 动态初始化是指由程序员在初始化数组时指定数组的长度,由系统为数组元素分配初始值。 数组动态初始化的具体示例如下。 int[] array = new int[10];   // 动态初始化数组 上述示例会在数组声明的同时分配一块内存空间供该数组使用,其中数组长度是10,由于每个元素都为int型,因此上例中数组占用的内存共有10×4=40B。此外,动态初始化数组时,其元素会根据它的数据类型被设置为默认的初始值。本例数组中每个元素的默认值为0,其他常见的数据类型默认值如表3.1所示。 表3.1数据类型默认值 成员变量类型 初始值 成员变量类型 初始值 byte 0 double 0.0D short 0 char 空字符,'\u0000' int 0 boolean false long 0L 引用数据类型 null float 0.0F 3.1.3数组的常用操作 1. 访问数组 在Java中,数组对象有一个length属性,用于表示数组的长度,所有类型的数组都是如此。 获取数组的长度的语法格式如下。 数组名.length 接下来用length属性获取数组的长度,具体示例如下。 int[] list = new int[10];   // 定义一个int类型的数组 int size = list.length;   // size = 10,数组的长度 数组中的变量又称为元素,考虑到一个数组中的元素可能会很多,为了便于区分它们,每个元素都有下标(索引),下标从0开始,如在 int[] list = new int[10]中,list[0]是第1个元素,list[1]是第2个元素,……,list[9]是第10个元素,也就是最后一个元素。因此,假如数组list有n个元素,那么list[0]是第1个元素,而list[n-1]则是最后一个元素。 如果下标值小于0,或者大于或等于数组长度,编译程序不会报任何错误,但运行时将出现异常: ArrayIndexOutOfBoundsException:N,即数组下标越界异常,N表示试图访问的数组下标。 2. 数组元素的存取 通过操作数组的下标可以访问到数组中的元素,也可以实现数组元素的存取。接下来演示数组元素的存取操作,如例31所示。 例31TestArray.java 1public class TestArray { 2public static void main(String[] args) { 3// 声明数组 4int[] a = new int[5]; 5// 存入数组元素 6a[0] = 5; // 往数组的第一个元素中存入数据 5 7a[1] = 10; // 往数组的第二个元素中存入数据 10 8a[4] = 9; // 往数组的第五个元素中存入数据 9 9// 读取数组元素 10System.out.print("数组中的元素为:"); 11System.out.println(a[0]+","+a[1]+","+a[2]+", "+a[3]+", "+a[4]); 12} 13} 程序运行结果如图3.1所示。 图3.1例31运行结果 从图3.1中可以看出,数组中的元素已经存取成功,而且在例31中,数组下标为2、3的位置中并未存入数据,但是却能取到数据为0的元素,可见声明为int类型的数组元素的默认值为0。 3. 数组遍历 数组的遍历是指依次访问数组中的每个元素。接下来演示循环遍历数组,如例32所示。 例32TestArrayTraversal.java 1public class TestArrayTraversal { 2public static void main(String[] args) { 3int[] list = {1, 2, 3, 4, 5};   // 定义数组 4for (int i = 0; i < list.length; i++) {  // 遍历数组元素 5System.out.println(list[i]);   // 索引访问数组 6} 7} 8} 程序的运行结果如图3.2所示。 图3.2例32运行结果 在例32中,声明并静态初始化一个int类型的数组,然后利用for循环中的循环变量充当数组的索引,依次递增索引,从而遍历数组元素。 4. 数组最大值和最小值 通过前面已经掌握的知识,用数组的基本用法与流程控制语句的使用来实现得到数组中的最大值和最小值,首先把数组的第一个数赋值给变量max和min,分别表示最大值和最小值,再依次判断数组的其他数值的大小,判断当前值是否是最大值或最小值,如果不是则进行替换,最后输出最大值和最小值。接下来通过一个案例来获取数组的最大值和最小值,如例33所示。 例33TestMostValue.java 1public class TestMostValue { 2public static void main(String[] args) { 3// 定义数组 4int[] score = {88, 62, 12, 100, 28}; 5int max = 0;   // 最大值 6int min = 0;   // 最小值 7max = min = score[0];   // 把第一个元素值赋给max和min 8for (int i=1; i max) {  // 依次判断后面元素值是否比max大 10max = score[i];   // 如果大,则修改max的值 11} 12if (score[i] < min) {  // 依次判断后面元素值是否比min小 13min = score[i];   // 如果小,则修改min的值 14} 15} 16System.out.println("最大值:"+ max); 17System.out.println("最小值:"+ min); 18} 19} 程序的运行结果如图3.3所示。 图3.3例33运行结果 在例33中,main()方法声明并静态初始化了score数组,并定义了两个变量max与min,分别用来存储最大值与最小值。接着把score数组第一个元素score[0]分别赋值到max与min中,然后使用for循环对数组进行遍历。下面通过一个图例来分析min和max的比较过程,如图3.4所示。 图3.4数组最大值和最小值比较过程 在图3.4中,max与min最初存储的数值都是score数组的第一个元素88,在遍历过程中只要遇到比max值还大的元素,就将该元素赋值给max,遇到比min还小的元素,就将该元素赋值给min。 5. 数组排序 数组排序是指数组元素按照特定的顺序排列。在实际应用中,经常需要对数据排序,如老师对学生的成绩排序。数组排序有多种算法,本节介绍一种简单的排序算法——冒泡排序。这种算法是不断地比较相邻的两个元素,较小的向上冒,较大的向下沉,排序过程如同水中气泡上升,即两两比较相邻元素,反序则交换,直到没有反序的元素为止,如例34所示。 例34TestBubbleSort.java 1public class TestBubbleSort { 2public static void main(String[] args) { 3int[] array = {88, 62, 12, 100, 28};  // 定义数组 4// 外层循环控制排序轮数 5// 最后一个元素,不用再比较 6for (int i=0; i < array.length-1; i++) { 7// 内层循环控制元素两两比较的次数 8// 每轮循环沉底一个元素,沉底元素不用再参加比较 9for (int j = 0; j < array.length - 1 - i; j++) { 10// 比较相邻元素 11if (array[j] > array[j+1]) { 12// 交换元素 13int tmp = array[j]; 14array[j] = array[j+1]; 15array[j+1] = tmp; 16} 17} 18// 打印每轮排序结果 19System.out.print("第"+(i+1)+"轮排序:"); 20for (int j=0; j 100) { 13System.out.println("成绩输入错误!"); 14return; 15} 16if (score >= 90.0) { 17System.out.println('A'); 18} else if (score >= 80.0) { 19System.out.println('B'); 20} else if (score >= 70.0) { 21System.out.println('C'); 22} else if (score >= 60.0) { 23System.out.println('D'); 24} else { 25System.out.println('F'); 26} 27} 28// 带返回值的方法 29public static char getGrade(double score) { 30if (score >= 90.0) { 31return 'A'; 32} else if (score >= 80.0) { 33return 'B'; 34} else if (score >= 70.0) { 35return 'C'; 36} else if (score >= 60.0) { 37return 'D'; 38} else { 39return 'F'; 40} 41} 42} 程序的运行结果如图3.18所示。 图3.18例38运行结果 例38中,定义了两个方法printGrade()和getGrade(),其中,printGrade()方法是用void修饰的,不返回任何值,而getGrade()方法有返回值。用void修饰的方法不需要return语句,但它能用于终止方法返回到方法的调用者,控制程序的流程。当成绩不在0~100,调用printGrade()方法,程序将打印“成绩输入错误!”,执行return语句后,它后面的语句将不再执行,程序直接返回到调用者。 3.2.2方法的调用 方法在调用时执行方法中的代码,因此要执行方法,必须调用方法。如果方法有返回值,通常将方法调用作为一个值来处理。如果方法没有返回值,方法调用必须是一条语句。具体示例如下。 int large = max(3, 4);   // 将方法的返回值赋给变量 System.out.println(max(3,4));   // 直接打印方法的返回值 System.out.println("Hello World!");   // println方法没有返回值,必须是语句 如果方法定义中包含形参,调用时必须提供实参。实参的类型必须与形参的类型兼容,实参顺序必须与形参的顺序一致。实参的值传递给方法的形参,称为值传递(pass by value),方法内部对形参的修改不影响实参值。当调用方法时,程序控制权转移至被调用的方法。当执行return语句或到达方法结尾时,程序控制权转移至调用者,如例39所示。 例39TestCallMethod.java 1public class TestCallMethod { 2public static void main(String[] args) { 3int n = 5; 4int m =2; 5System.out.println("before main\t:n="+ n + ", m=" + m); 6swap(n, m); 7System.out.println("end main\t:n="+ n + ", m=" + m); 8} 9// 交换两个数 10public static void swap(int n, int m) { 11System.out.println("before swap\t:n="+ n + ", m=" + m); 12int tmp = n; 13n = m; 14m = tmp; 15System.out.println("end swap\t:n="+ n + ", m=" + m); 16} 17} 程序的运行结果如图3.19所示。 图3.19例39运行结果 在例39中,当调用swap方法时,程序将实参n、m的值传递给形参的n、m,然后程序将控制流程转向swap方法。执行swap方法时,交换形参n和m的值,当swap方法执行完毕时,系统释放形参并将控制权返还给它的调用者main方法。因此,swap方法不能交换实参n和m的值。 每当调用一个方法时,JVM将创建一个栈帧,用于保存该方法的形参和变量。当方法调用结束返回到调用者时,JVM释放相应的栈帧。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在JVM中从入栈到出栈的过程。接下来演示堆栈中调用方法的栈帧,如图3.20所示。 图3.20栈帧 在图3.20中,main方法调用swap方法时,调用者main方法的栈帧不变,程序先为swap方法创建一个新的栈帧,用于保存形参n、m和局部变量tmp的值,再将实参值传递给形参,并保存在该栈帧中,方法内部操作的都是该方法栈帧中的值。当swap方法执行结束时,其对应的栈帧将被释放。 3.2.3方法的重载 方法重载(overloading)是指方法名称相同,但形参列表不同的方法。调用重载的方法时,Java编译器会根据实参列表寻找最匹配的方法进行调用,如例310所示。 例310TestOverload.java 1public class TestOverload { 2public static void main(String[] args) { 3// 调用max(int, int)方法 4System.out.println("3和8的最大值:" + max(3, 8)); 5// 调用max(double, double)方法 6System.out.println("3.0和8.0的最大值:" + max(3.0, 8.0)); 7// 调用max(double, double, double)方法 8System.out.println("3.0、5.0和8.0的最大值:" + max(3.0, 5.0, 8.0)); 9// 调用max(double, double)方法 10System.out.println("3和8.0的最大值:" + max(3, 8.0)); 11} 12// 返回两个整数的最大值 13public static int max(int num1, int num2) { 14int result; 15if (num1 > num2) 16result = num1; 17else 18result = num2; 19return result; 20} 21// 返回两个浮点数的最大值 22public static double max(double num1, double num2) { 23double result; 24if (num1 > num2) 25result = num1; 26else 27result = num2; 28return result; 29} 30// 返回三个浮点数的最大值 31public static double max(double num1, double num2, double num3) { 32return max(max(num1, num2), num3); 33} 34} 程序的运行结果如图3.21所示。 图3.21例310运行结果 在图3.21中,从程序运行结果可以发现,max(3, 8)调用的是max(int,int)方法,max(3.0,8.0)调用的是max(double,double)方法,max(3.0,5.0,8.0)调用的是max(double,double,double)方法。而且max(3,8.0)也能被执行,实参3被自动转换为double类型,然后调用max(double,double)方法。 为什么max(3,8)不会调用max(double,double)方法呢?其实,max(double,double)和max(int,int)与max(3,8)都可能匹配。当调用方法时,Java编译器会根据实参的个数和类型寻找最准确的方法进行调用。因为max(int,int)比max(double,double)更精确,所以max(3,8)会调用max(int,int)。 调用一个方法时,出现两个或多个可能的匹配时,编译器无法判断哪个是最精确的匹配,则会产生编译错误,称为歧义调用(ambiguous invocation),如例311所示。 例311TestAmbiguousInvocation.java 1package test; 2public class TestAmbiguousInvocation { 3public static void main(String[] args) { 4System.out.println(max(3, 8)); 5} 6// 返回整数和浮点数的最大值 7public static double max(int num1, double num2) { 8if (num1 > num2) 9return num1; 10else 11return num2; 12} 13// 返回浮点数和整数的最大值 14public static double max(double num1, int num2) { 15if (num1 > num2) 16return num1; 17else 18return num2; 19} 20} 程序的运行结果如图3.22所示。 图3.22例311运行结果 在图3.22中,程序编译错误并提示“对max的引用不明确”,原因在于max(int,double)和max(double,int)与max(3,8)都匹配,从而产生歧义,导致编译错误。 方法只能根据参数列表(参数类型、参数顺序和参数个数)进行重载,而不能通过修饰符或返回值来重载。 注意: 在同一个类中,方法重载指的是可以定义多个名称相同,形参列表不同的方法(即形参的排列顺序、类型、个数,满足任意一个不同即可),它对方法的返回值不做要求。在方法的调用上系统会自动根据传过来的参数数量和类型来决定使用哪一个方法。 3.2.4方法的递归 方法的递归是指一个方法直接或间接调用自身的行为,递归必须要有结束条件,否则会无限地递归。递归用于解决使用简单循环难以实现的问题,如例312所示。 例312TestRecursion.java 1public class TestRecursion { 2public static void main(String[] args) { 3System.out.println("4的阶乘:"+ fact(4)); 4} 5/* 6计算阶乘 7阶乘计算公式: 80!=1 9n! = n * (n-1)!; n>0 10*/ 11public static long fact(int n) { 12// 结束条件 13if (n ==0) 14return 1; 15return n * fact(n - 1); 16} 17} 程序的运行结果如图3.23所示。 图3.23例312运行结果 在例312中,定义fact()方法用于计算阶乘,方法是将数学上的阶乘公式转换为代码。当用n=0调用该方法时,程序立即返回结果,这种简单情况称为结束条件,如果没有终止条件,就会出现无限递归。当用n>0调用该方法时,就将这个原始问题分解成计算n-1的阶乘的子问题,持续分解,直到问题达到最终条件为止,就将结果返回给调用者。然后调用者进行计算并将结果返回给它自己的调用者,过程持续进行,直到结果返回原始调用者为止。原始问题就可以通过将fact(n-1)的结果乘以n得到。调用过程称为递归调用,如图3.24所示。 图3.24递归原理 在图3.24中,描述了例312中的递归调用过程,整个递归过程中fact()方法被调用了5次,每次调用n的值都会递减,当n的值为0时,所有递归调用的方法都会以相反的顺序相继结束,所有的返回值会进行累乘,最终得到结果24。 3.3数组的引用传递 在方法调用时,参数按值传递,即用实参的值去初始化形参。对于基本数据类型,形参和实参是两个不同的存储单元,因此方法执行中形参的改变不影响实参的值; 对于引用数据类型,形参和实参存储的是引用(内存地址),都指向同一内存单元,在方法执行中,对形参的操作实际上就是对实参的操作,即对执行内存单元的操作,因此,方法执行中形参的改变会影响实参。 向方法传递数组时,方法的接收参数必须是符合其类型的数组; 从方法返回数组时,返回值类型必须明确地声明其返回的数组类型。数组属于引用类型,所以在执行方法中对数组的任何操作,结果都将保存下来,如例313所示。 例313TestRefArray.java 1public class TestRefArray { 2public static void main(String[] args) { 3int[] array = {1, 3, 5}; 4rev(array);   // 将数组元素反序 5System.out.print("数组的反序:"); 6printArray(array); // 打印反序后的数组 7int[] copy = copy(array);  // 复制数组 8array[0] = 9;   // 修改源数组 9System.out.print("修改源数组:"); 10printArray(array);   // 打印源数组 11System.out.print("复制的数组:"); 12printArray(copy);   // 打印复制数组 13} 14// 将数组元素反序 15public static void rev(int[] pa) { 16for (int i = 0, j = pa.length-1; i < j; i++, j--) { 17int tmp = pa[i]; 18pa[i] = pa[j]; 19pa[j] = tmp; 20} 21} 22// 复制数组元素 23public static int[] copy(int[] pa) { 24int[] newarray = new int[pa.length]; 25for (int i = 0; i < pa.length; i++) { 26newarray[i] = pa[i]; 27} 28return newarray; 29} 30// 打印数组元素 31public static void printArray(int[] pa) { 32for (int i=0; i