6. 方法
方法可以用于定义可重用的代码以及组织和简化编码。
6.1 定义方法
方法的定义由方法名称、参数、返回值类型以及方法体组成。
定义方法的语法如下所示:
修饰符 返回值类型 方法名( 参数列表 ){
// 方法体 ;
}
我们一起来看一个方法的定义,该方法找出两个整数中哪个数比较大。这个名为 max 的方法有两个 int 型参数:num1 和 mnn2, 方法返回两个数中较大的一个。下图解释了这个方法的组成
方法头(method header) 是指方法的修饰符(modifier)、返回值类型(return value type)、方法名(method name) 和方法的参数(parameter)。
方法可以返回一个值。retumValiieType 是方法返回值的数据类型。有些方法只是完成某些要求的操作,而不返回值。在这种情况下,retuniValueType 为关键字 void。例如:在main 方法中 returnValueType 就是 void, 在 System.exit、System.out.println 方法中返回值类型也是如此。如果方法有返回值,则称为带返回值的方法(value-returning method), 否则就称这个该方法为 void 方法(void method)。
定义在方法头中的变量称为形式参教(formal parameter)或者简称为形参( parameter)。参数就像占位符。当调用方法时,就给参数传递一个值,这个值称为实际参数(actual parameter) 或实参(argument)。参数列表( parameter list)指明方法中参数的类型、顺序和个数。方法名和参数列表一起构成方法签名(method signature)。参数是可选的,也就是说,方法可以不包含参数。例如:Math.randomO 方法就没有参数。
方法体中包含一个执行方法的语句集合。max 方法的方法体使用一个if语句来判断哪个数较大,然后返回该数的值。为使带返回值的方法能返回一个结果,必须要使用带关键字 return 的返回语句。执行 return 语句时方法终止。
在其他某些语言中,方法称为过程( procedure) 或函数(fimction)。带返回值的方法称为函数,返回值类型为 void 的方法称为过程。
警告:在方法头中,需要对每一个参数进行独立的数据类型声明。例如:max(int numl,1nt num2)是正确的,而 max(int num1,mim2)是错误的。
注意:我们经常会说 “定义方法” 和 “声明变量”,这里我们谈谈两者的细微差别。定义是指被定义的条目是什么,而声明通常是指为被声明的条目分配内存来存储数据。
6.2 调用方法
方法的调用是执行方法中的代码。
在方法定义中,定义方法要做什么。为了使用方法,必须调用(call 或 invoke)它。根据方法是否有返回值,调用方法有两种途径。
如果方法返回一个值,对方法的调用通常就当作一个值处理。例如:
int larger = max(3, 4);
调用方法 max(3,4)并将其结果赋给变量 larger。另一个把它当作值处理的调用例子是:
System.out.println(max(3 , 4));
这条语句打印调用方法 max(3,4)后的返回值。
如果方法返回 void, 对方法的调用必须是一条语句。例如,println 方法返回 void。下面的调用就是一条语句:
System.out.println("Weicome to Java!");
注意:在 Java 中,带返回值的方法也可以当作语句调用。这种情况下,函数调用者只需忽略返回值即可。虽然这种情况很少见,但是,如果调用者对返回值不感兴趣,这样也是允许的。
当程序调用一个方法时,程序控制就转移到被调用的方法。当执行完 return 语句或执行到表示方法结束的右括号时,被调用的方法将程序控制返还给调用者。
警告:对带返回值的方法而言,return 语句是必需的。下面图 a 中显示的方法在逻辑上是正确的,但它会有编译错误,因为 Java 编译器认为该方法有可能不会返回任何值。
为解决这个问題,删除图 a 中的 if(n<0), 这样,编译器将发现不管 if 语句如何执行,总可以执行到 return 语句。
注意:方法能够带来代码的共享和重用。除了可以在 TestMax 中调用 max 方法,还可以在其他类中调用它。如果创建了一个新类,可以通过使用 “类名 .方法名”(即 TestMax.max )来调用 max 方法。
每当调用一个方法时,系统会创建一个活动记录(也称为活动框架),用于保存方法中的参数和变量。活动记录置于一个内存区域中,称为调用堆栈(call stack)。调用堆栈也称为执行堆栈、运行时堆栈,或者一个机器堆栈,常简称为 “堆栈”。当一个方法调用另一个方法时,调用者的活动记录保持不动,一个新的活动记录被创建用于被调用的新方法。一个方法结束返回到调用者时,其相应的活动记录也被释放。
下图描述了堆栈中用于方法调用的活动记录。
6.3 void 方法示例
void 方法不返回值。
下列程序清单给出的程序定义了一个名为 printGrade 的方法,然后调用它打印出给定分数的
等级。
TestVoidMethod.java
public class TestVoidMethod {
public static void main(String[] args) {
System.out.print("The grade is ");
printGrade(78.5);
System.out.print("The grade is ");
printGradeC59.5);
}
public static void printGrade(double score) {
if (score >= 90.0) {
System,out.println('A');
}
else if (score >= 80.0) {
System.out.println('B');
}
else if (score >=70.0) {
System.out.println('C');
}
else if (score >=60.0) {
System.out.println('D');
}
else {
System.out.println('F');
}
}
}
显示
The grade is C
The grade is F
printGrade 方法是一个 void 方法,它不返回任何值。对 void 方法的调用必须是一条语句。因此,在 main 方法的第 4 行,PrintGrade 方法作为一条语句调用,这条语句同其他 Java 语句一样,以分号结束。
注意:void 方法不需要 return 语句,但它能用于终止方法并返回到方法的调用者。它的语法是:
return;
这种用法很少,但是对于改变 void 方法中的正常流程控制是很有用的。例如:当分数是无效值时,下列代码就用 return 语句结束方法。
public static void printGrade(double score){
if (score < 0||score > 100){
System.out.println("Invalid score");
return;
If (score >= 90.0){
System.out.println('A');
}
else if (score >= 80.0){
System.out.println('B');
}
else if (score >= 70.0){
System.out.println('C');
}
else if (score >= 60.0){
System.out.println('D');
}
else {
System.out.print1n('F');
}
}
6.4 通过传值进行参数传递
调用方法的时候是通过传值的方式将实参传给形参的。
方法的强大之处在于它处理参数的能力。可以使用方法 println 打印任意字符串,用 max 方法求任意两个 int 值的最大值。调用方法时,需要提供实参,它们必须与方法签名中所对应的形参次序相同。这称作参数顺序匹配( parameter order association)。例如,下面的方法打印 message 信息 n 次:
public static void nPrintln(String message , int n) {
for (int i = 0; i < n; i ++){
System.out. println(message);
}
}
可以使用nPrintln("Hello", 3) 打印 "Hello"3 遍。语句 nPrintln("Hello" , 3) 把实际的字符串参数 "Hello"传给参数 massage , 把 3 传给 n , 然后打印 "Hello"3 次。然而,语句 nPrintln(3 ,"Hello") 是错误的。3 的数据类型不匹配第一个参数 message 的数据类型,第二个参数"Hello"不匹配第二个参数 n。
警告:实参必须与方法签名中定义的参数在次序和数量上匹配,在类型上兼容。类型兼容是指不需要经过显式的类型转换,实参的值就可以传递给形参,例如,将 int 型的实参值传递给 double 型形参。
当调用带参数的方法时,实参的值传递给形参,这个过程称为按值传递(pass - by value)。如果实参是变量而不是直接量,则将该变量的值传递给形参。无论形参在方法中是否改变,该变量都不受影响。
注意:为了简便,Java 程序员经常说将实参 x 传给形参 y, 实际含义是指将 x 的值传递给 y。
6.5 模块化代码
模块化使得代码易于维护和调试,并且使得代码可以被重用。
使用方法可以减少冗余的代码,提高代码的复用性。方法也可以用来模块化代码,以提高程序的质量。
6.6 重载方法
重载方法使得你可以使用同样的名字来定义不同方法,只要它们的签名是不同的。
前面用到的 max 方法只能用于 int 型数据类型。但是,如果需要决定两个浮点数中哪个较大,该怎么办呢?解决办法是创建另一个方法名相同但参数不同的方法,代码如下所示:
public static double max(double numl, double num2){
if(numl > num2){
return numl;
}else{
return num2;
}
}
如果调用带 int 型参数的 max 方法,就将调用需要 int 型参数的 max 方法;如果调用带 double 型参数的 max 方法,就将调用需要 double 型参数的 max 方法。这称为方法重载(method overloading)。 也就是说,在一个类中有两个方法,它们具有相同的名字,但有不同的参数列表。Java 编译器根据方法签名决定使用哪个方法。
提示:重载方法可以使得程序更加清楚,以及更加具有可读性。执行同样功能但是具有不同参数类型的方法应该使用同样的名字。
注意:被重载的方法必须具有不同的参数列表。不能基于不同修饰符或返田值类型来重载方法。
注意:有时调用一个方法时,会有两个或更多可能的匹配,但是,编译器无法判断哪个是最精确的匹配。这称为歧义调用(ambiguous invocation)。歧义调用会产生一个编译错误。
考虑如下代码:
public class AmbiguousOverloading {
public static void main(String[] args){
System.out.println(max(1 , 2));
}
public static double max(int num1, double num2){
if (num1 > num2){
return num1;
}else{
return num2;
}
}
public static double max(double num1,int num2){
If (num1 > num2){
return num1;
}else{
return num2;
}
}
}
max(int,double)和 max(double,int)都有可能与 max(1,2)匹配。由于两个方法谁也不比谁更精确,所以这个调用是有歧义的,它会导致一个编译错误。
6.7 变量的作用域
变量的作用域(scope ofa variable) 是指变量可以在程序中引用的范围。
在方法中定义的变量称为局部变量(local variable)。局部变量的作用域从声明变量的地方开始,直到包含该变量的块结束为止。局部变量都必须在使用之前进行声明和赋值。
参数实际上就是一个局部变量。一 个方法的参数的作用域涵盖整个方法。在 for 循环头中初始动作部分声明的变量,其作用域是整个 for 循环。但是在 for 循环体内声明的变量,其作用域只限于循环体内,是从它的声明处开始,到包含该变量的块结束为止,如下图所示:
可以在一个方法中的不同块里声明同名的局部变量,但是,不能在嵌套块中或同一块中两次声明同一个局部变量,如下图所示:
警告:不要在块内声明一个变量然后企图在块外使用它。下面是一个常见错误的例子:
for (int i = 0;i < 10;i++){
}
System,out.println(i);
因为变量 i 没有在 for 循环外定义,所以最后一条语句就会产生一个语法错误。
6.8 方法抽象和逐步求精
开发软件的关键在于应用抽象的概念。
方法抽象( method abstraction) 是通过将方法的使用和它的实现分离来实现的。用户在不知道方法是如何实现的情况下,就可以使用方法。方法的实现细节封装在方法内,对使用该方法的用户来说是隐藏的。 这就称为信息隐藏( information hiding ) 或封装(encapsulation)。如果决定改变方法的实现,但只要不改变方法签名,用户的程序就不受影响。方法的实现对用户隐藏在 “黑盒子” 中,如下图所示。
方法抽象的概念可以应用于程序的开发过程中。当编写一个大程序时,可以使用 “分治” ( divid - and - conquer) 策略,也称为逐步求精( stepwise refinement), 将大问题分解成子问题。子问题又分解成更小、更容易处理的问题。
方法抽象:
- 自顶向下的设计
- 自顶向下和自底向上的实现
- 实现细节
逐步求精的优势:
1. 更简单的程序
打印日历的程序比较长。逐步求精方法将其分解为较小的方法,而不是在一个方法中写很长的语句序列。这样简化了程序,使得整个程序易于阅读和理解。
2. 重用方法
逐步求精提高了一个程序中的方法重用。isLeapYear 方法只定义了一次,从 getTotalNumberOfDays 和getNumberOfDaysInMonth 方法中都进行了调用。这减少了冗余的代码。
3. 易于开发、调试和测试
因为每个子问题在一个方法中解决,而一个方法可以分别的开发、调试以及测试。这隔离了错误,使得开发、调试和测试更加容易。编写大型程序时,可以使用自顶向下或自底向上的方法。不要一次性地编写整个程序a 使用这些方法似乎浪费了更多的开发时间(因为要反复编译和运行程序),但实际上,它会更节省时间并使调试更容易。
4. 更方便团队合作
当一个大问题分解为许多子问题,各个子问题可以分配给不同的编程人员。这更加易于编程人员进行团队工作。
附录
编译运算练习
1.
2.
3.