Java方法详解

方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上来看,方法完全类似于传统结构化程序设计里的函数。值得指出的是,Java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

1.* 方法的所属性

不论是从定义方法的语法来看,还是从方法的功能来看,都不难发现方法和函数之间的相似性。实际上,方法确实是由传统的函数发展而来的,方法与传统的函数有着显著不同:在结构化编程语言里,函数是一等公民,整个软件由一个个的函数组成;在面向对象编程语言里,类才是一等公民,整个系统由一个个的类组成。因此在Java语言里,方法不能独立存在,方法必须属于类或对象。

因此,如果需要定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的实例。

Java语言是静态的。一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,永远都不会改变。

因为Java里的方法不能独立存在,它必须属于一个类或一个对象,因此方法也不能像函数那样被独立执行,执行方法时必须使用类或对象来作为调用者,即所有方法都必须使用“类.方法”或“对象.方法”的形式来调用。这里可能产生一个问题:同一个类里不同方法之间相互调用时,不就可以直接调用吗?这里需要指出:同一个类的一个方法调用另外一个方法时,如果被调方法是普通方法,则默认使用this作为调用者;如果被调方法是静态方法,则默认使用类作为调用者。也就是说,表面上看起来某些方法可以被独立执行,但实际上还是使用this或者类来作为调用者。

永远不要把方法当成独立存在的实体,正如现实世界由类和对象组成,而方法只能作为类和对象的附属,Java语言里的方法也是一样。Java语言里方法的所属性主要体现在如下几个方面:

  • 方法不能独立定义,方法只能在类体里定义。
  • 从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象。
  • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

使用static修饰的方法属于这个类本身,使用static修饰的方法既可以使用类作为调用者来调用,也可以使用对象作为调用者来调用。但值得指出的是,因为使用static修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,因为实际上还是使用这些实例所属的类作为调用者。

没有static修饰的方法则属于该类的对象,不属于这个类本身。因此没有static修饰的方法只能使用对象作为调用者调用,不能使用类作为调用者调用。使用不同对象作为调用者来调用同一个普通方法,可能得到不同的结果。

1.* 方法的参数传递机制

Java里的方法是不能独立存在的,调用方法也必须使用类或对象作为主调者。如果声明方法时包含了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值也被称为实参。

那么,Java的实参值是如何传入方法的呢?这是由Java方法的参数传递机制来控制的,Java里方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

注意:Java里的参数传递类似于《西游记》里的孙悟空,孙悟空复制了一个假孙悟空,这个假孙悟空具有和孙悟空相同的能力,可除妖或被砍头。但不管这个假孙悟空遇到什么事,真孙悟空不会受到任何影响。与此类似,传入方法的是实际参数值的复制品,不管方法中对这个复制品如何操作,实际参数值本身不会受到任何影响。

下面程序演示了方法参数传递的效果:

查看代码

运行上面程序,看到如下运行结果:

点击查看代码
swap方法里,a的值是9;b的值是6
交换结束后,变量a的值是6;变量b的值是9

从上面运行结果来看,swap方法里a和b的值是9、6,交换结束后,变量a和b的值依然是6、9。从这个运行结果可以看出,main方法里的变量a和b,并不是swap方法里的a和b。正如前面讲的,swap方法的a和b只是main方法里变量a和b的复制品。

下面通过示意图来说明上面程序的执行过程,Java程序总是从main方法开始执行,main方法开始定义了a、b两个局部变量,两个变量在内存中的存储示意图如图:

image

当程序执行swap方法时,系统进入swap方法,并将main方法中的a、b变量作为参数值传入swap方法,传入swap方法的只是a、b的副本,而不是a、b本身,进入swap方法后系统中产生了4个变量,这4个变量在内存中的存储示意图如图所示:

image

在main方法中调用swap方法时,main方法还未结束。因此,系统分别为main方法和swap方法分配两块栈区,用于保存main方法和swap方法的局部变量。main方法中的a、b变量作为参数值传入swap方法,实际上是在swap方法栈区中重新产生了两个变量a、b,并将main方法栈区中a、b变量的值分别赋给swap方法栈区中的a、b参数(就是对swap方法的a、b形参进行了初始化)。此时,系统存在两个a变量、两个b变量,只是存在于不同的方法栈区中而已。

程序在swap方法中交换a、b两个变量的值,实际上是对上图中灰色覆盖区域的a、b变量进行交换,交换结束后swap方法中输出a、b变量的值,看到a的值为9,b的值为6,此时内存中的存储示意图如图所示:

image

实际上main方法栈区中a、b的值并未有任何改变,程序改变的只是swap方法栈区中的a、b。这就是值传递的实质:当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法里操作的并不是实际的实参变量。

前面看到的是基本类型的参数传递,Java对于引用类型的参数传递,一样采用的是值传递方式。但许多初学者可能对引用类型的参数传递会产生一些误会。

下面程序示范了引用类型的参数传递的效果:

查看代码

从上面运行结果来看,在swap方法里,a、b两个Field值被交换成功。不仅如此,main方法里swap方法执行结束后,a、b两个Field值也被交换了。这很容易造成一种错觉:调用swap方法时,传入swap方法的就是dw对象本身,而不是它的复制品。但这只是一种错觉,下面还是结合示意图来说明程序的执行过程。

程序从main方法开始执行,main方法开始创建了一个DataWrap对象,并定义了一个dw引用变量来指向DataWrap对象,这是一个与基本类型不同的地方。创建一个对象时,系统内存中有两个东西:堆内存中保存了对象本身,栈内存中保存了引用该对象的引用变量。接着程序通过引用来操作DataWrap对象,把该对象的a、b两个Field分别赋值为6、9。此时系统内存中的存储示意图如图所示:

image

接下来,main方法中开始调用swap方法,main方法并未结束,系统会分别开辟出main和swap两个栈区,用于存放main和swap方法的局部变量。调用swap方法时,dw变量作为实参传入swap方法,同样采用值传递方式:把main方法里dw变量的值赋给swap方法里的dw形参,从而完成swap方法的dw形参的初始化。值得指出的是,main方法中的dw是一个引用(也就是一个指针),它保存了DataWrap对象的地址值,当把dw的值赋给swap方法的dw形参后,即让swap方法的dw形参也保存这个地址值,即也会引用到堆内存中的DataWrap对象。

下图显示了dw传入swap方法后的存储示意图:

image

这种参数传递方式是不折不扣的值传递方式,系统一样复制了dw的副本传入swap方法,但关键在于dw只是一个引用变量,所以系统复制了dw变量,但并未复制DataWrap对象。

当程序在swap方法中操作dw形参时,由于dw只是一个引用变量,故实际操作的还是堆内存中的DataWrap对象。此时,不管是操作main方法里的dw变量,还是操作swap方法里的dw参数,其实都是操作它所引用的DataWrap对象,它们操作的是同一个对象。因此,当swap方法中交换dw参数所引用DataWrap对象的a、b两个Field值后,我们看到main方法中dw变量所引用DataWrap对象的a、b两个Field值也被交换了。

为了更好地证明main方法中的dw和swap方法中的dw是两个变量,我们在swap方法的最后一行增加如下代码:

点击查看代码
//把dw直接赋值为null,让它不再指向任何有效地址
dw = null;

执行上面代码的结果是swap方法中的dw变量不再指向任何有效内存,程序其他地方不做任何修改。main方法调用了swap方法后,再次访问dw变量的a、b两个Field,依然可以输出9、6。可见main方法中的dw变量没有受到任何影响。实际上,当swap方法中增加dw = null代码之后,内存中的存储示意图:

image

把swap方法中的dw赋值为null后,swap方法中失去了DataWrap的引用,不可再访问堆内存中的DataWraper对象。但main方法中的dw变量不受任何影响,依然引用DataWrap对象,所以依然可以输出DataWrap对象的a、b的Field值。

1.* 形参个数可变的方法

从JDK 1.5之后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点...,则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

下面程序定义了一个形参个数可变的方法:

查看代码

运行上面程序,看到如下运行结果:

点击查看代码
疯狂Java讲义
轻量级JavaEE企业应用实战
5

从上面运行结果可以看出,当调用test方法时,books参数可以传入多个字符串作为参数值。从test的方法体代码来看,形参个数可变的参数其实就是一个数组参数,也就是说,下面两个方法签名的效果完全一样:

点击查看代码
//以可变个数形参来定义方法
public static void test(int a, String... books);
//下面采用数组形参来定义方法
public static void test(int a, String[] books);

这两种形式都包含了一个名为books的形参,在两个方法的方法体内都可以把books当成数组处理。但区别是调用两个方法时存在差别,对于以可变形参的形式定义的方法,调用方法时更加简洁,如下面代码所示:

点击查看代码
test(5, "疯狂Java讲义", "轻量级JavaEE企业应用实战");

传给books参数的实参数值无须是一个数组,但如果采用数组形参来声明方法,调用时则必须传给该形参一个数组,如下所示:

点击查看代码
//调用test方法时传入一个数组
test(23, new String[]{"疯狂Java讲义", "轻量级JavaEE企业应用实战"});

对比两种调用test方法的代码,明显第一种形式更加简洁。实际上,即使是采用形参个数可变的形式来定义方法,调用该方法时也一样可以为个数可变的形参传入一个数组。

最后还要指出的是,数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能处于形参列表的最后。也就是说,一个方法中最多只能有一个长度可变的形参。

注意:长度可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个长度可变的形参。调用包含一个长度可变形参的方法时,这个长度可变的形参既可以传入多个参数,也可以传入一个数组。

1.* 递归方法

一个方法体内调用它自身,被称为方法递归。方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

如有如下数学题。已知有一个数列:f(0) = 1,f(1) = 4,f(n+2) = 2 * f(n+1) + f(n),其中n是大于0的整数,求f(10)的值。这个题可以使用递归来求得。看下面程序将定义一个fn方法,用于计算f(10)的值:

查看代码

在上面的fn方法体中,再次调用了fn方法,这就是方法递归。注意fn方法里调用fn的形式:

点击查看代码
return 2 * fn(n - 1) + fn(n - 2)

对于fn(10),即等于2 * fn(9) + fn(8),其中fn(9)又等于2 * fn(8) + fn(7)……依此类推,最终会计算到fn(2)等于2 * fn(1) + fn(0),即fn(2)是可计算的,然后一路反算回去,就可以最终得到fn(10)的值。

仔细看上面递归的过程,当一个方法不断地调用它本身时,必须在某个时刻方法的返回值是确定的,即不再调用它本身,否则这种递归就变成了无穷递归,类似于死循环。因此定义递归方法时有一条最重要的规定:递归一定要向已知方向递归。

例如,如果把上面数学题改为如此。已知有一个数列:f(20) = 1,f(21) = 4,f(n+2) = 2 * f(n+1) + f(n),其中n是大于0的整数,求f(10)的值。那么fn的方法体就应该改为如下:

点击查看代码
public static int fn(int n) {
    if (n == 20) {
        return 1;
    } else if (n == 21) {
        return 4;
    } else {
        //方法中调用它自身,就是方法递归
        return fn(n + 2) - 2 * fn(n + 1);
    }
}

从上面的fn方法来看,当我们要计算fn(10)的值时,fn(10)等于fn(12) – 2 × fn(11),而fn(11)等于fn(13) – 2 × fn(12)……依此类推,直到fn(19)等于fn(21) – 2 × fn(20),此时就可以得到fn(19)的值了,然后依次反算到fn(10)的值。这就是递归的重要规则:对于求fn(10)而言,如果fn(0)和fn(1)是已知的,则应该采用fn(n) = 2 × fn(n-1) + fn(n-2)的形式递归,因为小的一端已知;如果fn(20)和fn(21)是已知的,则应该采用fn(n) = fn(n + 2) – 2 × fn(n + 1)的形式递归,因为大的一端已知。

递归是非常有用的。例如,我们希望遍历某个路径下的所有文件,但这个路径下文件夹的深度是未知的,那么就可以使用递归来实现这个需求。系统可定义一个方法,该方法接受一个文件路径作为参数,该方法可遍历当前路径下的所有文件和文件路径——该方法中再次调用该方法本身来处理该路径下的所有文件路径。

总之,只要一个方法的方法体实现中再次调用了方法本身,就是递归方法。递归一定要向已知方向递归。

1.* 方法重载

Java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。

从上面介绍可以看出,在Java程序中确定一个方法需要三个要素:

  • 调用者,也就是方法的所属者,既可以是类,也可以是对象
  • 方法名,方法的标识
  • 形参列表,当调用方法时,系统将会根据传入的实参列表匹配

方法重载的要求就是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。

下面程序中包含了方法重载的示例:

查看代码

编译、运行上面程序完全正常,虽然两个test方法的方法名相同,但因为它们的形参列表不同,所以系统可以正常区分出这两个方法。

为什么方法的返回值类型不能用于区分重载的方法?

对于int f()和void f()两个方法,如果这样调用int result = f()的话,系统可以识别是调用返回值类型为int的方法;但Java调用方法时可以忽略方法返回值,如果采用f()的方式来调用的话,你能判断是调用哪个方法吗?如果你尚且不能判断,那么Java系统也会糊涂。在编程过程中有一条重要规则:不要让系统糊涂,系统一糊涂,肯定就是你错了。因此,Java里不能使用方法返回值类型作为区分方法重载的依据。

不仅如此,如果被重载的方法里包含了长度可变的形参,则需要注意。

看下面程序里定义的两个重载的方法:

查看代码

编译、运行上面程序,将看到olv.test()和olv.test(“aa”, “bb”)两次调用的是test(String… books)方法,而olv.test(“aa”)则调用的是test(String msg)方法。通过这个程序可以看出,如果同一个类中定义了test(String… books)方法,同时还定义了一个test(String msg)方法,则test(String… books)方法的books不可能通过直接传入一个字符串参数,如果只传入一个参数,系统会执行重载的test(String msg)方法。

如果需要调用test(String… books)方法,又只想传入一个字符串参数,则可采用传入字符串数组的形式,如下代码所示:

点击查看代码
olv.test(new String[]{"aa"});

大部分时候,我们不推荐重载形参长度可变的方法,因为这样做确实没有太大的意义,而且容易降低程序的可读性。

原文地址:http://www.cnblogs.com/hzhiping/p/16863375.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性