3.5 方法的使用
通过前面介绍的程序,可以看到对简单的问题,程序也比较简单:一个程序是一个类,在类中包含一个main()方法。但在解决一些比较复杂的问题时,按照现代模块化程序设计的思想,应仔细分析问题,善于将这些复杂问题分解成若干相对简单的问题,即划分为多个模块。这样,解决一个复杂问题就转化为逐个解决一些简单问题。对程序开发和维护而言,小模块比大程序更便于管理。
在Java语言中,类和方法就是程序的模块。在一个类中可以根据需要设计多个方法。在本节中,先介绍Java语言中的方法。在程序设计时,可将一个程序中完成特定功能的程序段定义为方法(类似其他语言的函数)。在需要使用这些功能时,可调用相应的方法。特别是在某些功能多次被使用时,采用方法可大大提高程序代码的可复用性。使用方法要掌握方法定义、方法调用、方法参数传送等方面的内容。
3.5.1 方法的定义与调用
1.方法的定义
方法的定义是描述实现某个特定功能所需的数据及进行的运算和操作。定义形式如下:
[修饰符] 返回类型 方法名([参数表]){ 语句 // 方法体 }
其中,用方括号括住的项目是可选的。方法的返回类型指的是方法的返回值类型,若方法完成的功能是计算值,则计算结果值或计算值的表达式一般要书写到方法体里的return语句中,而且类型一般应与返回类型指明的类型一致。返回类型可以是基本类型、数组、类等。若方法完成的功能不返回值,返回类型处应为void,而且方法体中的return语句不能带表达式或不用return语句。
方法名是一标识符,是对方法的命名。
可选的参数表必须用圆括号括起来,参数表由0 个或多个用逗号分隔的参数构成,每个参数由类型和参数名(标识符)组成,参数可以是基本数据,也可以是数组或类实例,参数类型可以是基本数据类型或类。方法用参数来与外界发生联系(数据传送)。
方法定义中{}括起来的部分称为方法体,在其中书写方法的实现语句,包括数据定义和执行语句。
方法定义前面的修饰符用关键字表示,修饰符是可选的,用来说明方法的某些特性。可用的修饰符有public、static、private等。
Java语言允许一个类中定义多个方法,方法定义形式为并列形式,先后顺序无关紧要。
【例3.16】定义一个计算圆面积的方法area()。计算圆面积需要知道圆的半径r,area()方法应从外部得到这个r,所以可将r设置为一个参数,类型为double。area()方法将计算出一个面积值,类型也应为double,可将它设置为area()返回值类型。area()方法可定义如下:
double area(double r){ double s = Math.PI * r * r; // 方法参数在方法体中可直接引用 return(s); }
【例3.17】定义一个求三个整数中最大数的方法max3()。该方法需要三个整数,因此,需设置三个整数参数。max3()的返回结果是一个整数值(即最大整数),可设置方法返回值类型为整型。max3()可定义如下:
int max3(int x,int y,int z){ int big; big = Math.max(x,y); big = Math.max(big,z); return(big); }
2.方法的调用
在程序中需要某个方法的功能时,要调用该方法。调用方法时,要用实际参数替换方法定义中的参数表中的形式参数(或称虚拟参数)。实际参数(简称实参)的个数、类型、顺序都必须与形式参数(简称形参)一致(调用方法时参数个数可变的方法定义及其调用可参见第4章)。
【例3.18】调用例3.17中定义的max3()方法求变量a、b和c中的最大值。
class MethodDemo{ public static void main(String args[]){ int a = 4,b = 5,c = 2,big; big = max3(a,b,c); // 用3个实参调用方法max3,方法的返回值存入big变量 System.out.println(big); } static int max3(int x,int y,int z){ //方法max3有3个形式参数 int big; big = Math.max(x,y); big = Math.max(big,z); return(big); } }
在例中,方法的返回值类型为基本类型,在调用方法中,用一个与方法返回值相同类型的变量big来接收返回值。也可以对方法调用的返回值直接输出,如上例中输出三个整数中的最大数,可改为System.out.println(max3(a,b,c))。
注意:与例3.17不同,在方法max3()的声明中用了修饰符static,它说明该方法是一个类方法。与类方法main()相同,类方法可以直接调用,而不需要创建实例对象,若方法未用static修饰符修饰,这个方法就是实例方法。实例方法不能像类方法这样被直接调用。
一般而言,有返回值的方法调用形式为表达式形式,即可以在允许表达式出现的地方使用方法调用。若方法无返回值(void类型),方法调用的形式一般为表达式语句的形式,即调用方法的形式为单独的加分号的语句。
3.5.2 方法调用中的数据传送
调用方法与被调方法之间往往需要进行数据传送。例如,调用方法传送数据给被调方法,被调方法得到数据后进行计算,计算结果再传送给调用方法。一般说来,方法间传送数据有如下的几种方式:值传送方式、引用传送方式、返回值方式、实例变量和类变量传送方式。方法的参数可以是基本类型的变量、数组和类对象等。通过实参与形参的对应,数据传送给方法体使用。
1.值传送方式
值传送方式是将调用方法的实参的值计算出来赋予被调方法对应形参的一种数据传送方式。在这种数据传送方式下,被调方法对形参的计算、加工与对应的实参已完全脱离关系。当被调方法执行结束,形参中的值可能发生变化,但是返回后,这些形参中的值将不回带到对应的实参中。因此,值传送方式的特点是“数据的单向传送”。
使用值传送方式时,形式参数一般是基本类型的变量,实参是常量、变量,也可以是表达式。
2.引用传送方式
使用引用传送方式时,方法的参数类型一般为复合类型(引用类型),复合类型变量中存储的是对象的引用。所以在参数传送中是传送引用,方法接收参数的引用,所以任何对形参的改变都会影响到对应的实参。因此,引用传送方式的特点是“引用的单向传送,数据的双向传送”。
3.返回值方式
返回值方式不是在形参和实参之间传送数据,而是被调方法通过方法调用后直接返回值到调用方法中。使用返回值方式时,方法的返回值类型不能为void,且方法体中必须有带表达式的return语句,其中表达式的值就是方法的返回值。在例3.18 中,方法max3()就是利用返回值方式将3 个数据的最大值回送到main()方法中的。这个方法的返回值为int类型,所以在定义方法时,方法的类型要定义为int类型。
4.实例变量和类变量传送方式
实例变量和类变量传送方式也不是在形参和实参之间传送数据,而是利用在类中定义的实例变量和类变量是类中诸方法共享的变量的特点来传送数据。因类变量(static变量)可直接访问,使用较简单,下面是一个使用类变量的例子。
【例3.19】求半径为1.23高为4.567的圆柱体体积。
public class CircleVolume{ static double r = 1.23,h = 4.567,s,v; // 定义类变量 public static void main(String args[]){ area(); // 调用area方法 vol(); // 调用vol方法 System.out.println("半径为1.23的圆面积 = " + s); System.out.println("半径为1.23高为4.567的圆柱体体积 = " + v); } static void area(){ s = Math.PI*r*r; // 访问类变量r和s } static void vol(){ v = s * h; // 访问类变量s、h和v } }
程序的运行结果如下:
半径为1.23的圆面积 = 4.752915525615998 半径为1.23高为4.567的圆柱体体积 = 21.7065652054882
本程序利用类变量使得main()方法、area()方法和vol()方法之间传送了数据。
3.5.3 方法和变量的作用域
在Java语言中,方法与变量的作用域(有效使用范围)是清晰的,根据定义变量的位置不同,作用域也不全相同。当一个方法使用某个变量时,以如下的顺序查找变量:
当前方法、当前类、一级一级向上经过各级父类、import类和包,若都找不到所要的变量定义,则产生编译错误。
下面对方法和变量作用域的讨论限于在一个类中的情况。多个类的情况见第5 章相关章节。
1.局部变量
局部变量是定义在块内、方法内的变量。这种变量的作用域是以块和方法为单位的,仅在定义该变量的块或方法内有效,而且要先定义赋值,然后再使用,即不允许超前引用。因为局部变量在查找时首先被查找,因此若某一局部变量与类的实例变量名或类变量名相同时,则该实例变量或类变量在方法体内被暂时“屏蔽”起来,只有退出这个方法后,实例变量或类变量才起作用。
【例3.20】说明实例变量和局部变量同名。
class A{ int x = 8; // 实例变量 void f(){ int x = 6; // 局部变量与实例变量同名,屏蔽了实例变量 System.out.println(" x = " + x); } }
方法f输出的结果为:x = 6(注:这个程序不能直接运行)
每调用一次方法,都要动态地为方法的局部变量分配内存并初始化。方法体内不能定义静态变量。方法体内的任何语句块内都可以定义新的变量,这些变量仅在定义它的语句块内起作用。当语句块有嵌套时,内层语句块定义的变量不能与外层语句块的变量同名,否则会出现编译错误。另外,方法的参数也属于局部变量,因此声明与参数同名的局部变量也会出错。
2.实例变量和类变量
定义在类内、方法外的变量是实例变量,使用了修饰符static的变量是静态变量(或称类变量)。实例变量和类变量的作用域是以类为单位的。因为实例变量、类变量与局部变量的作用域不同,故可以与局部变量同名。
【例3.21】说明局部变量和实例变量、类变量。
class MyObject { static short s = 400; // 类变量 int i = 200; // 实例变量 void f() { System.out.println("s = " + s); System.out.println("i = " + i); short s = 300; // 局部变量 int i = 100; // 局部变量,与实例变量同名 double d = 1E100; // 局部变量 System.out.println("s = " + s); System.out.println("i = " + i); System.out.println("d = " + d); } } class LocalVariables { public static void main(String args[]) { MyObject myObject = new MyObject();
myObject.f(); } }
程序运行结果如下:
s = 400 i = 200 s = 300 i = 100 d = 1.0E100
一般情况下,变量应该先定义后使用。但实例变量和静态变量可以超前引用,即在定义位置前引用,但静态变量不能超前引用静态变量。例如:
class B{ static int x; static int z = y + 1; // 静态变量不能超前引用静态变量,错 int c = 0; void a(){ int a = b + y; // 超前引用b和y,对 System.out.println(" a = "+ a); } int b = 2; static int y; int c = b; // c重复声明,错 int x = 0; // x重复声明,错 }
3.方法
实例方法和类方法在整个类内均是可见的,可以超前引用。除方法的重载外,声明两个同名实例方法或类方法是错误的,类方法与实例方法同名也是错误的。例如:
class C{ void a(int x){ b(x); // 超前引用方法b(),对 } void a(){ // 方法重载,对 x = 0; // 超前引用实例变量,对 } void b(int x){...} void a(int x){ // 方法a(int x)重复声明了,错 ... } static void b(int x){ // 修饰符不能作为方法重载的标志,错 ... } static void b(){ // 方法重载,对 ... }
}
3.5.4 方法的嵌套和递归调用
1.嵌套调用
在一个方法的调用中,该方法的实现部分又调用了另外的方法,则称为方法的嵌套调用。
【例3.22】求 ,当n = 11,x = 4,6,8,10时的值。
public class MethodNestDemo{ public static void main(String args[]){ int n = 11,x; for(x = 4;x < 11;x += 2) System.out.println("C(" + n + "," + x + ") = " + comb(n , x)); } static int comb(int n,int x){ return( fact( n ) / fact( x ) / fact( n - x )); } static int fact(int n){ if(n == 1) return 1; else return fact(n -1) * n; } }
程序运行结果如下:
C(11,4) = 330 C(11,6) = 462 C(11,8) = 165 C(11,10) = 11
程序中包含三个类方法,在main()类方法中调用comb()类方法,而这个方法又调用fact()类方法。从而形成了方法调用嵌套。
2.递归
在一个方法有调用该方法自身的情况时,称为方法的递归调用。大多数情况是直接递归,即一个方法直接自己调自己。当一个实际问题可用递归形式描述时,该问题的求解用递归方法变得容易。例如,求n!、求前n个自然数的和、求第n个Fibonacci数等,它们的递归描述分别为:
n的阶乘:
前n个自然数之和:
第n个Fibonacci数:
据上述描述,容易写出对应递归方法。在例3.22 中求阶乘的方法fact即为递归方法。下面是求第n个Fibonacci数的递归方法例。
【例3.23】用递归调用求第n个Fibonacci数的方法求前20个Fibonacci数。
public class Fibo{ public static void main(String args[]){ final int n = 20; for(int i = 1;i <= n;i++){ System.out.printf("%5d",f(i)); if(i % 10 == 0)System.out.println(); } } static long f(long n){ if(n == 1||n == 2)return 1; else return f(n -1) + f(n -2); } }
程序的运行结果如下:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 9871597258441816765
从程序设计的角度来说,递归方法必须解决两个问题:一是递归计算的公式,二是递归结束的条件。对求第n个Fibonacci数列的递归方法来说,这两个条件是:
递归计算公式:fib( n )=fib(n -1) + fib(n -2)
递归结束条件:fact( 1 ) = 1
3.5.5 方法的重载
Java语言允许在一个类中定义几个同名的方法,但要求这些方法具有不同的参数集合,即方法参数的个数、类型和顺序要不同。这种做法称为方法的重载。当调用一个重载的方法时,Java编译器可根据方法参数的个数、类型和顺序的不同,来调用相应的方法。
Math类中的方法abs()、max()和min()等都是重载的,它们具有double、float、int、long等参数和返回值类型的重载方法。Java根据方法名及参数集合的不同来区分不同的方法,若调用的两个同名方法中参数个数、类型及顺序均一样,仅仅返回值类型不同,则编译时会产生错误。
重载方法可以有不同类型的返回值。
【例3.24】用方法重载求圆、矩形、梯形面积。
class Area{ static double area(double r){ return Math.PI * r * r; } static double area(double l,double w){ return l * w; }
static double area(double d1,double d2,double h){ return (d1 + d2) * h / 2; } public static void main(String args[]){ double s1 = area(3.0); System.out.println("圆面积 = " + s1); double s2 = area(3.0,4.0); System.out.println("矩形面积 = " + s2); double s3 = area(3.0,4.0,5.0); System.out.println("梯形面积 = " + s3); } }
程序运行结果如下:
圆面积 = 28.274333882308138 矩形面积 = 12.0 梯形面积 = 17.5
在本例中,三个求面积的方法名都相同,但参数个数不同,在main方法中用不同的参数个数去调用area方法,可求不同的面积。