在Java中,使用方法
来描述一类对象的行为,也就是能做什么。
什么是方法
方法或者说类似的概念,早在面向对象思想出现之前就已经很常用了,在不同的编程语言中被称为“函数”、“过程”、“子程序”等等。无论是什么名称,它们的思路是相似的:把一些常用代码放到一个模块中,使用时可以调用,这样无需编写重复代码。
例如我们现在要写一个网络通信软件,在发送信息、发送文件、语音通话、视频通话等活动前,需要先测试发起者和接受者之间的网络是否连通。假设测试连通性的代码有50行,如果我们每次需要测试连通性时都需要写这些代码,那么每用一次就要写这50行代码。是不是很麻烦?编程编出了小时候被罚写作业的感觉了……
还有更糟糕的呢,那就是程序写完了,我们灵机一动,想到了一个更好的方法,只要20行代码就可以实现原来50行代码的全部功能,我们需要把原来所有的这部分代码找出来一一修改,不仅可能会有遗漏,还可能一不小心改错了……这比被罚写作业的感觉还要糟糕。
于是,科学家们想到了一个办法:把这些重复使用的代码放到一个模块里面,使用时调用这个模块就可以了;如果需要修改,也只修改这一个模块就可以了。这样,我们只需要写一次代码就可以多次使用了。
实际上,我们现在的做法更进了一步:别人写好的模块,我们直接拿来用就可以了,连那“编写一次”都省了。
在Java中,这样的模块被称为“方法”。我们可以通过方法来反复使用一段代码。
自然而然的,我们也可以使用方法来描述一类对象可以做的事情。
怎么定义方法
在Java中,方法不能单独存在,必须包含在一个类中。一个完整的方法包含修饰符、返回值类型、方法名、参数列表、方法体和返回值等几个部分,格式如下:
修饰符 返回值类型 方法名(数据类型 参数1,数据类型 参数2...){
方法体...
return 返回值;
}
其中,修饰符用于限定该方法可以在什么范围内被访问,我们暂时先不使用它,到后面我们再来学习。
现在我们来看一个例子(只展示方法的代码,包含该方法的类请自行定义):
int add(int x,int y){
int sum=x+y;
return sum;
}
在这个示例中,第一个int
就是方法的返回值类型,它表示该方法中的代码执行完毕后会产生一个int类型的结果;add
是方法名;方法名后面的小括号里面就是参数列表,每个参数都需要标明数据类型和参数名,多个参数之间用逗号隔开;花括号中的部分就是方法体,也就是运行方法时要执行的那些代码。在方法体中,最后一句是return
语句,用于返回方法的运行结果。
但也有一些特殊情况:
- 参数列表中可以没有参数,也可以只有一个参数,还可以有多个参数;
- 方法的返回值类型必须和return语句返回的结果的数据类型一致;
- 如果方法没有返回值(比如运行结束后直接输出结果),返回值类型为
void
,这时可以不写return语句; - 一个方法中可以有多个return语句,但执行到第一个return语句,方法就会结束,所以第一个被执行的return语句后面的代码都毫无意义。
如何调用方法
方法的调用也很简单,对于有返回值的方法,可以当成一个运算式,就像x+y一样:变量/对象=方法名(参数1,参数2...);
,或者直接把方法调用的结果当成一个数值或对象来使用也可以。
对于没有返回值,也就是定义为void
的方法,可以直接调用,就像发布一条命令一样:方法名(参数1,参数2...);
调用方法时,参数的个数和顺序必须和定义方法时是一样的。例如,前面定义的add()方法有两个int类型的参数,那么调用这个方法时,也必须提供两个int类型的数据;如果我们定义add方法时,设置了一个int类型参数和一个double类型参数,而且int在前,double在后,那么调用方法时,也必须提供一个int类型和一个double类型的参数,而且int在前,double在后。
我们来看个例子。
创建一个项目,然后创建两个源文件,Main.java包含主方法,调用Cal类生成对象,并使用其中的两个方法;Cal.java用于设计Cal类。代码如下。
Cal.java
//该类定义了两个整数的加法和减法的方法
public class Cal {
//计算两个整数相加
int add(int x,int y) {
int sum=x+y;
return sum;
}
//计算两个整数相减,并直接输出结果
void subtract(int x,int y) {
System.out.println(x+"-"+y+"="+(x-y));
}
}
Main.java
//使用Cal类生成对象,然后调用对象中的加、减方法
public class Main {
public static void main(String[] args) {
Cal calculator=new Cal();
int a=10;
int b=30;
//add方法返回两者的加和,把它放入输出方法中,做为输出方法的一个参数使用
System.out.println(a+"+"+b+"="+calculator.add(a, b));
//subtract方法直接输出结果
calculator.subtract(a, b);
}
}
02
从a、b到x、y,发生了什么
在上面的例子中,我们会发现,定义方法和调用方法的时候,表示参数的变量是不同的。为了方便说明,我们把定义方法时写的那些参数(x、y)叫做形式参数,简称形参;而调用方法时使用的那些参数(a、b)叫做实际参数,简称实参。
其实方法和数学上的函数很像(C语言中同样的功能,就叫做函数)。例如我们定义一个函数:z=x*4+y*y+30
。z就是返回值,x和y就是形式参数。当我们调用这个函数时,可能就是r=a*4+b*b+30
,这时,a和b就是实际参数(a和b也可以是确定的数字),r则是用来接收返回值的变量。
所以,我们在编程时说的“调用方法”,其实和数学上讲的“把变量代入函数”,是相同的。
再来深入一点,我们要学习一个概念,叫做“值传递”:在调用函数时,实参的值会复制给形参,然后执行方法体中的代码,直到结束或者遇到第一个return语句。
在有些文章和教材中,会强调“值传递”和“引用传递”,实际上这种区分毫无必要。我们只要了解其原理,就会知道,无论是什么传递,本质都是:实参的值复制给形参。
为了方便说明,我们来看这个例子:
在上面例子中增加一个Person.java,代码如下:
//关于人物的类
public class Person {
//属性年龄
int age;
}
在Cal.java中增加三个方法:
//改变一个变量参数的值
void changeInt(int x) {
x=500;
}
//改变一个数组的元素的值
void changArray(int[] a) {
a[0]=100;
}
// 改变一个对象的属性值
void changObject(Person e) {
e.age=12;
}
changInt方法可以把参数的值改成500,changString方法可以把参数的值改成”66666666666666”。然后我们在Main.java的主方法中增加:
//定义Person类型的对象,并为其age属性赋值
Person p=new Person();
p.age=30;
// 定义整型变量i,值为40
int i = 40;
// 定义数组并初始化
int[] array= {1,2,3,4};
// 输出他们的值
System.out.println("在调用方法前,p.age="+p.age+",i=" + i + ",array[0]=" + array[0]);
// 调用改变参数值的方法
calculator.changObject(p);
calculator.changeInt(i);
calculator.changArray(array);
// 再次输出他们的值
System.out.println("在调用方法后,p.age="+p.age+",i=" + i + ",a[0]=" + array[0]);
运行后,新增代码的运行结果就是:
在调用方法前,p.age=30,i=40,array[0]=1
在调用方法后,p.age=12,i=40,array[0]=100
我们会发现,如果在方法的定义中修改了形参的值,然后调用这些方法,运行完毕后:对象的属性值改变了,变量值没有改变,数组元素的值改变了。再概括一下就是:如果形参是基本数据类型,那么形参的改变不会引发实参的改变;如果形参是个引用数据类型,那么形参的改变会引发实参的改变。
真这样吗?至少目前看来是这样的。但实际情况比目前总结出来的规律更加复杂。
我们先给出一个关于基本数据类型和引用数据类型的一个底层知识的说明,下一次,我们将深入探讨方法参数的问题。
深挖基本数据类型和引用数据类型
从表面上看,基本数据类型的变量和引用数据类型的对象,有很多相似之处,例如两者都在内存中有自己的存储空间。但实际上,两者的深层次原理有很大的差异。
以整型变量为例,代码int a=50;
,会告诉计算机现在需要划分一块内存空间来保存数据,这个数据是int类型。因此系统会划分一块大小为4字节(按照规定,一个int类型数据占用4字节内存)的内存空间,然后把50这个数据放到这块内存中。为了方便引用,我们为这块内存起了个名字,叫做a,计算机就会把a和这块内存绑定到一起。只要我们引用a,计算机就会直接操作对应的这块内存。如果我们再执行a=90;
,计算机就会把这块内存中的数据改成90,原来的50就会被覆盖。
引用数据类型的对象处理起来,会更加复杂一些。以字符串为例。代码String s="abcd";
,告诉计算机现在需要创建一个字符串对象,名为s,内容为“abcd”。计算机会首先创建一个字符串“abcd”,然后创建一个字符串对象的引用s,最后把字符串“abcd”的地址放到s中。这样,当我们使用对象s时,计算机会通过其中记录的地址找到真正的字符串对象。如果我们再执行s="xyz";
,计算机会创建一个新的字符串“xyz”,然后把其地址放到s中,这时原来的字符串“abcd”还在!!!
举个例子来说,如果我们得到了一块宝石,基本数据类型的处理方法就是把宝石直接给了我们,而引用数据类型的处理方法则是把宝石的位置给了我们。虽然在直接使用时效果基本相同,都是获得了一块宝石。但在一些比较复杂的使用上,例如把宝石换成了支票,基本数据类型的处理方法是把宝石直接销毁,拿一张支票放到我们手上;而引用数据类型的处理方法则是把原地址改成支票的地址,宝石还在,但我们只能找到支票而不是宝石了。
02
变量的作用域
到这里,想必大家一定可以写出包含属性和行为的类了。经常性的,我们会在一个项目的不同位置处无意中定义出同名的变量,这种情况该如何处理呢?我们来看一个程序:
/*
* 展示代码作用域
*/
public class ActionScope {
static int a = 100;// 定义属性,变量a
// show方法用于显示变量a的值
public static void show() {
int a = 200;// 定义变量a
System.out.println("show方法中,a=" + a);// 输出a的值
}
//主方法,也会输出变量a的值
public static void main(String[] args) {
int a = 300;// 定义变量a
System.out.println("主方法中,a=" + a);// 输出a的值
show();// 调用show方法输出a的值
}
}
请大家先不要在意
static
关键字,后面的课程中会有详细说明。
在上面的程序中,一个类中定义了三个变量a:类ActionScope的属性、show方法中的变量和main方法中的变量a。当我们运行这个程序时,结果为:
主方法中,a=300
show方法中,a=200
此时显示的都是各个方法中定义的变量a的值。
当我们将show方法中的代码int a = 200;
注释掉时:
主方法中,a=300
show方法中,a=100
此时show方法中虽然已经不再定义有变量a,但显示了属性a的值。我们再把main方法中的代码int a = 200;
注释掉:
主方法中,a=100
show方法中,a=100
由以上表现可以得出结论:
- 变量是有作用域的,每个变量只能在一定范围内有效。这个范围是由变量所属部分的花括号划定的。例如第一个a,它在类中,那么从定义之处起,直到这个类结束处的花括号为止都有效;第二个变量a位于show方法中,那么从定义之处起,直到这个方法结束处的花括号为止都有效。超出作用域时,变量将不起作用,或者说,系统认为在作用域之外,这个变量是不存在的。
- 当作用域有重叠时,采用“县官不如现管”原则,距离调用变量的语句最近的变量生效。当show方法中定义变量a时,虽然show方法也在类属性a的作用域内,但由于方法中定义的变量a距离输出语句更近(这个近不是指物理距离,而是两者属于同一个单元:show方法),所以输出语句只认得show方法中的变量a,而会忽略更远的属性a。当show方法中的变量a被注释掉后,距离输出语句最近的就是属性a,因此会输出属性a的值。show方法和main方法虽然都定义有变量a,但这两个变量a分属不同的方法,作用域独立,没有相互影响。
细心的同学可能会发现,在第一条中,我把“花括号”加粗了,可能会想:为什么要强调花括号,从定义之处起,到所在“模块”(方法或类)结束为止,不就完了?
这是由于Java中可以把一部分代码用花括号括起来,形成代码块,代码块也是一种作用域。再后面的课程中,我们还会专门谈到代码块。
鉴于有些朋友可能还不太熟悉类、方法等知识,而还有代码块等新知识在前面等着,所以这里介绍一个确定变量作用域的小技巧:
- 确定定义该变量的位置,这就是该变量作用域的起点;
- 从定义位置,向上找第一个出现的左花括号
{
; - 然后找到与该左花括号匹配的右花括号
}
(绝大多数情况下,把鼠标指针移动到这个左花括号上,IDE和文本编辑器都会高亮显示匹配的另一半); - 这个优化括号就是变量作用域的终点
此外,另一种做法是尽量避免在同一个程序中使用同名的变量。
02