飞道的博客

Java零基础学习之路(七)Java方法的作用及本质

355人阅读  评论(0)

本章节目标:

理解方法的本质以及作用;掌握方法的定义;掌握方法如何调用;理解栈数据结构;理解方法执行过程中内存是如何变化的;掌握方法的重载机制;掌握方法递归算法。

知识框架:

 

Java方法的本质以及作用

我们先不讲方法是什么,先来看一段代码,分析以下程序存在哪些缺点,应该如何去改进:


  
  1. public static void main(String[] args) {
  2. //请计算10和20的和
  3. int a = 10;
  4. int b = 20;
  5. int c = a + b;
  6. System. out.println(a + "+" + b + "=" + c);
  7. //请计算666和888的和
  8. int x = 666;
  9. int y = 888;
  10. int z = x + y;
  11. System. out.println(x + "+" + y + "=" + z);
  12. //请计算888和999的和
  13. int m = 888;
  14. int n = 999;
  15. int e = m + n;
  16. System. out.println(m + "+" + n + "=" + e);
  17. }

以上代码完成了三个求和的功能,每一次求和的时候都把代码重新写了一遍,显然代码没有得到“重复利用”,表面上看是三个功能,但实际上只是“一个”求和功能,只不过每一次参与求和的实际数值不同。java中有没有一种方式或者语法,能让我们把功能性代码写一次,然后给这个功能性代码传递不同的数据,来完成对应的功能呢?答案是:当然有。这就需要我们掌握java语言中的方法机制,接下来大家看看改进之后的代码(这里先不需要掌握方法是怎么定义的以及怎么调用的,只要看以下代码就行,此小节是为了让大家理解方法的本质以及作用):


  
  1. public static void main(String[] args) {
  2. //调用求和方法计算10和20的和
  3. sumInt( 10 , 20);
  4. //调用求和方法计算666和888的和
  5. sumInt( 666 , 888);
  6. //调用求和方法计算888和999的和
  7. sumInt( 888 , 999);
  8. }
  9. //专门负责求和的方法
  10. public static void sumInt(int a , int b){
  11. int c = a + b;
  12. System. out.println(a + "+" + b + "=" + c);
  13. }

运行结果如下图所示:

图7-1:求和结果

通过以上程序我们可以看出,其实方法也没什么神秘的,方法其实就是一段普通的代码片段,并且这段代码可以完成某个特定的功能,而且可以被重复的调用/使用。java中的方法又叫做method,在C语言中叫做函数。

从现在开始大家以后在写代码的时候就要有定义方法的意识了,只要是可以独立出来的功能,我们都可以定义为单独的一个方法来完成,如果以后需要使用此功能时直接调用这个方法就行了,不要把所有的代码都扔到main方法当中,这样会导致程序的“复用性”很差。

Java方法的定义及调用

通过以上内容的学习,可以看出方法是一段可以完成某个特定功能的并且可以被重复利用的代码片段。接下来我们来学习一下方法应该怎么定义以及怎么调用。

定义/声明方法的语法格式如下所示:


  
  1. [ 修饰符列表] 返回值类型 方法名(形式参数列表){
  2. 方法体;
  3. }
  4. 例如代码:
  5. public static void sumInt(int a , int b){
  6. int c = a + b;
  7. System. out.println(a + "+" + b + "=" + c);
  8. }
  9. public static是修饰符列表;
  10. void是返回值类型;
  11. sumInt是方法名;
  12. ( int a , int b)是形式参数列表,简称形参,每一个形参都是局部变量;
  13. 形参后面使用一对儿大括号括起来的是方法体,方法体是完成功能的核心代码,方法体中的代码有执行顺序的要求,遵循自上而下的顺序依次逐行执行,不存在跳行执行的情况。
  14. 再如代码:
  15. public static int sumInt(int a , int b){
  16. int c = a + b;
  17. return c;
  18. }
  19. 以上程序中sumInt之前的 int是返回值类型。

接下来我将列出方法的相关规则,其中一些规则目前可能需要大家死记硬背,还有一些规则希望大家在理解的前提下进行记忆:

● [修饰符列表],此项是可选项,不是必须的,目前大家统一写成public static,后面的课程会详细讲解。

● 返回值类型,此项可以是java语言当中任何一种数据类型,包括基本数据类型,也包括所有的引用数据类型,当然,如果一个方法执行结束之后不准备返回任何数据,则返回值类型必须写void。返回值类型例如:byte,short,int,long,float,double,boolean,char,String,void等。

● 方法名,此项需要是合法的标识符,开发规范中要求方法名首字母小写,后面每个单词首字母大写,遵循驼峰命名方式,见名知意,例如:login、getUsername、findAllUser等。

● 形式参数列表(int a, int b),此项又被称为形参,其实每一个形参都是“局部变量”,形参的个数为0~N个,如果是多个参数,则采用半角“,”进行分隔,形参中起决定性作用的是参数的数据类型,参数名就是变量名,变量名是可以修改的,也就是说(int a , int b)也可以写成(int x , int y)。

● 方法体,由一对儿大括号括起来,在形参的后面,这个大括号当中的是实现功能的核心代码,方法体由java语句构成,方法体当中的代码只能遵循自上而下的顺序依次逐行执行,不能跳行执行,核心代码在执行过程中如果需要外部提供数据,则通过形参进行获取。

整体来说方法的声明语法是很简单的,我相信每个人都能记住,其实我觉得方法的定义难度最大的不是语法,而是方法在定义的时候,返回值类型定为什么类型比较合适?方法的形式参数列表中定义几个参数合适?每个参数的数据类型定义为什么比较合适?以上的一系列问题实际上还是需要和具体的功能结合在一起才能决定,当然,这不是一天两天的事儿,不是说这一章节的内容学完之后就真正的会定义方法了,我们只能说语法会了,还需要后期不断的做项目,写代码才能找到感觉,找到编程思路。到那时,你自然就会定义返回值类型、形式参数列表了。

当一个方法声明之后,我们应该如何去让这个方法执行呢,当然,这个时候就需要亲自去调用这个方法了,调用方法的语法格式是(前提是方法的修饰符列表中带有static关键字):“类名.方法名(实际参数列表);”,例如以下代码:


  
  1. public class MethodTest {
  2. public static void main(String[] args) {
  3. MethodTest.sumInt( 100, 200);
  4. MethodTest.sumDouble( 1.0, 2.0);
  5. }
  6. public static void sumInt(int x , int y){
  7. System. out.println(x + "+" + y + "=" + (x + y));
  8. }
  9. public static void sumDouble(double a , double b){
  10. System. out.println(a + "+" + b + "=" + (a + b));
  11. }
  12. }

运行结果如下图所示:

图7-2:方法如何调用

需要注意的是,方法在调用的时候,实际传给这个方法的数据被称为实际参数列表,简称实参,java语法中有这样的规定:实参和形参必须一一对应,所谓的一一对应就是,个数要一样,数据类型要对应相同。例如:实参(100 , 200)对应的形参(int x , int y),如果不是一一对应则编译器就会报错。当然也可能会存在自动类型转换,例如:实参(100 , 200)也可以传递给这样的形参(long a , long b),这里我们先不谈这个。

实际上方法在调用的时候,有的情况下“类名.”是可以省略的,我们来看看什么情况下它可以省略不写:


  
  1. public class MethodTest03 {
  2. public static void main(String[] args) {
  3. sumInt( 100, 200); //“类名.”可以省略
  4. sumDouble( 1.0, 2.0); //“类名.”可以省略
  5. //doOther(); //编译报错
  6. Other.doOther(); //“类名.”不能省略
  7. }
  8. public static void sumInt(int x , int y){
  9. System. out.println(x + "+" + y + "=" + (x + y));
  10. }
  11. public static void sumDouble(double a , double b){
  12. System. out.println(a + "+" + b + "=" + (a + b));
  13. }
  14. }

 


  
  1. public class Other{
  2. public static void doOther(){
  3. System. out.println( "Other doOther...");
  4. }
  5. }

运行结果如下图所示:

图7-3:“类名.”什么时候可以省略

通过以上程序的分析,我们得知,当在a()方法执行过程中调用b()方法的时候,并且a()方法和b()方法在同一个类当中,此时“类名.”可以省略不写,但如果a()方法和b()方法不在同一个类当中,“类名.”则不能省略。

Java方法返回值详解

每个方法都是为了完成某个特定的功能,例如:登录功能、求和功能等,既然是功能,那么当这个功能完成之后,大多数情况下都会有一个结果的,比如,登录成功了或者失败了(true/false),求和之后最后的结果是100或者200,当然也有极少数的情况下是没有结果的。这个结果本质上就是一个数据,那么既然是一个数据,就一定会有对应的类型,所以在方法定义的时候需要指定该方法的返回值类型。

java语言中方法的返回值类型可以是任何一种数据类型,包括基本数据类型,也包括引用数据类型,例如:byte,short,int,long,float,double,boolean,char,String,Student(自定义类)等。当然,如果这个方法在执行完之后不需要返回任何数据,返回值类型必须写void关键字,不能空着不写。

关于方法的返回值在使用的时候有哪些注意事项呢,我们来看几段代码:


  
  1. public static int method1(){
  2. }

以上程序在编译的时候,报错了,错误信息是“缺少返回语句”,为什么呢?这是因为该方法在声明的时候指定了方法结束之后返回int类型的数据,但是以上方法体中并没有编写“返回数据”的代码。也就是说当一个方法在声明的时候返回值类型不是void的情况下,要求方法体中必须有负责“返回数据”的语句。这样的语句怎么写呢?答案是:“return 数据;”(大家还记得第六章节控制语句中的返回语句吗?这个就是),并且要求这个“数据”的类型要和声明的返回值类型一致,要不然编译器就会报错了。代码这样写就没问题了:


  
  1. public static int method1(){
  2. /*
  3. return 1;
  4. return 0;
  5. return 100;
  6. */
  7. int a = 100;
  8. int b = 200;
  9. return a + b;
  10. }

如果代码这样写呢?


  
  1. public static int method1(){
  2. return 1;
  3. System. out.println( "hello world!");
  4. }

编译以上程序,我们可以看到编译器报错了,提示的错误信息是:“System.out.println("hello world!");”这行代码是无法访问的语句。这是为什么呢?因为在方法中如果一旦执行了带有“return”关键字的语句,此方法就会结束,所以以上的程序中“System.out.println("hello world!");”这行代码是永远无法执行到的,所以编译报错了。得到的结论是:带有return关键字的语句只要执行,所在的方法则执行结束。

那这样写呢?


  
  1. public static int method1(){
  2. boolean flag = true;
  3. if(flag){
  4. return 1;
  5. }
  6. }

还是编译报错,错误信息是“缺少返回语句”,为什么呢?实际上方法在声明的时候指定了返回值类型为int类型,java语法则要求方法必须能够“百分百的保证”在结束的时候返回int类型的数据,以上程序“return 1;”出现在if语句的分支当中,对于编译器来说,它只知道“return 1;”有可能执行,也有可能不执行,所以java编译器报错了,不允许程序员这样编写代码。来看以下代码:


  
  1. public static int method1(){
  2. boolean flag = true;
  3. if(flag){
  4. return 1;
  5. } else{
  6. return 0;
  7. }
  8. }

这样就能编译通过了,为什么呢?这是因为编译器能够检测出以上的if..else..语句必然会有一个分支执行,这样就不缺少返回语句了。其实以上代码也可以这样写:


  
  1. public static int method1(){
  2. boolean flag = true;
  3. if(flag){
  4. return 1;
  5. }
  6. return 0;
  7. }

以上代码也可以达到同样的效果,因为“return 1;”如果不执行,则一定会执行“return 0;”,当然,如果执行了“return 1;”则方法一定会结束,“return 0;”也就没有执行的机会了。再看下面的代码:


  
  1. public static int method1(){
  2. boolean flag = true;
  3. if(flag){
  4. return 1;
  5. System. out.println( "第1行");
  6. }
  7. System. out.println( "第2行");
  8. return 0;
  9. System. out.println( "第3行");
  10. }

以上程序还是编译报错,哪里出错了,为什么呢?其中“第1行”和“第3行”代码没有执行机会,编译报错,但“第2行”是有执行机会的。通过以上程序我们可以得出这样的结论:在同一个“域”中,“return”语句后面是不能写任何代码的,因为它无法执行到。

如果只是单纯的完成以上代码的功能我们也可以这样写:


  
  1. public static int method1(){
  2. boolean flag = true;
  3. return flag ? 1 : 0;
  4. }

所以,一个功能的实现,代码可以写很多种不同的方式,慢慢培养吧同学们,这需要一个过程,千万戒骄戒躁。我们接着看关于返回值还有哪些注意事项:


  
  1. public static void method2(){
  2. return 10;
  3. }

以上代码编译报错,为什么呢?这是因为一个方法在声明的时候返回值类型定义为void,这就表示该方法执行结束的时候不能返回任何数据,而以上程序中编写了“return 10;”这样的代码,自然编译器会报错的。也就是说前后说法要一致,声明的时候有返回值,那么代码编写的时候就必须有“return 值;”这样的语句。如果声明的时候没有返回值,那么方法结束的时候就不能编写“return 值;”这样的语句。那么,大家再来看看以下的代码:


  
  1. public static void method2(){
  2. return;
  3. }

经过测试,以上的代码编译通过了,也就是说当一个方法的返回值类型是void的时候,方法体当中允许出现“return;”语句(注意:不允许出现“return 值;”),这个语法的作用主要是用来终止方法的执行。当一个方法的返回值类型是void的时候,在方法执行过程中,如果满足了某个条件,则这个方法可能就没必要往下继续执行了,想终止这个方法的执行,此时执行“return;”就行了。有一些同学在最初的学习过程中,对break和return认识的不够清晰,我们来研究一下,请看以下代码:


  
  1. public static void main(String[] args) {
  2. for( int i = 1; i <= 10; i++){
  3. if(i == 5){
  4. break;
  5. }
  6. System. out.println( "i = " + i);
  7. }
  8. System. out.println( "hello world!");
  9. }

运行结果如下图所示:

图7-4:break测试


  
  1. public static void main(String[] args) {
  2. for( int i = 1; i <= 10; i++){
  3. if(i == 5){
  4. return;
  5. }
  6. System. out.println( "i = " + i);
  7. }
  8. System. out.println( "hello world!");
  9. }

运行结果如下图所示:

图7-5:return测试

通过以上的测试,我们可以得出break和return根本不是一个级别的,break用来终止循环,return用来终止一个方法的执行。接下来再看一段代码:


  
  1. public static int method3(){
  2. return;
  3. }

经过测试,以上代码编译报错,错误信息是:缺少返回值,为什么呢?这是因为方法在声明的时候规定了返回int类型的值,但是在方法体当中执行了“return;”语句,并没有返回具体的值,所以“return;”只能出现在返回值类型是void的方法中。

接下来我们来看看,当一个方法执行结束之后怎么接收这个返回值呢?请看下面代码:


  
  1. public static void main(String[] args) {
  2. //可以编译也可以正常运行
  3. sumInt( 10 , 20);
  4. int retValue = sumInt( 100 , 200);
  5. System. out.println( "计算结果 = " + retValue);
  6. //编译报错,返回值类型是int,不能采用byte接收
  7. //byte retValue2 = sumInt(1 , 2);
  8. }
  9. public static int sumInt(int a , int b){
  10. return a + b;
  11. }

运行结果如下图所示:

图7-6:测试怎么接收返回值

通过以上的测试我们得知,方法执行结束之后的返回值我们可以接收,也可以不接收,不是必须的,但大多数情况下方法的返回值还是需要接收的,要不然就没有意义了,另外方法的返回值在接收的时候采用变量的方式接收,要求这个变量的数据类型和返回值类型一致(当然,也可以遵守自动类型转换规则)。

Java栈数据结构

要想理解方法执行过程中内存的分配,我们需要先学习一下栈数据结构,那么什么是数据结构呢?其实数据结构是一门独立的学科,不仅是在java编程中需要使用,在其它编程语言中也会使用,在大学的计算机课程当中,数据结构和算法通常作为必修课出现,而且是在学习任何一门编程语言之前先进行数据结构和算法的学习。数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。

常见的数据结构有哪些呢?例如:栈、队列、链表、数组、树、图、堆、散列表等。目前我们先来学习一下栈(stack)数据结构,这是一种非常简单的数据结构。如下图所示:

 

图7-7:栈数据结构

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是:仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈、退栈或弹栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。如下图所示:

图7-8:栈数据结构

通过以上的学习,我们可以得知栈数据结构存储数据有这样的特点:先进后出,或者后进先出原则。也就是说最先进去的元素一定是最后出去,最后进去的元素一定是最先出去,因为一端是开口的,另一端是封闭的。

对于栈数据结构,目前我们了解这么多就可以了,等学完“方法执行的时候内存是如何变化的”,到那个时候大家再思考一个问题,为什么方法执行过程的内存要采用栈这种数据结构呢,为什么不选择其它数据结构呢?

Java方法执行过程中内存的变化

以上内容中讲解了方法是什么,怎么定义,怎么调用,目前来说大家实际上掌握这些内容就行了,接下来的内容大家尽量去学,实在是掌握不了,也没有关系,后期的内容会对这一部分的知识点进行不断的讲解,慢慢的大家就会了,其实在学习编程的过程中会遇到很多这样的情况,没事,大家不要心急,学习后面内容的过程中你会对前面的内容豁然开朗。

以下要讲解的是程序的内存,例如:代码片段被存储在什么位置?方法调用的时候,在哪里开辟内存空间等等。所以这一部分内容还是很高端大气上档次的。不过话又说回来,要想真正掌握java,内存的分析是必要的。一旦掌握内存的分配,在程序没有运行之前我们就可以很精准的预测到程序的执行结果。

好了,接下来我们开始学习方法执行过程中内存是如何变化的?我们先来看一张图片:

 

图7-9:JVM内存结构图

上图是一张标准的java虚拟机内存结构图,目前我们只看其中的“栈”和“方法区”,其它的后期研究,方法区中存储类的信息,或者也可以理解为代码片段,方法在执行过程中需要的内存空间在栈中分配。java程序开始执行的时候先通过类加载器子系统找到硬盘上的字节码(class)文件,然后将其加载到java虚拟机的方法区当中,开始调用main方法,main方法被调用的瞬间,会给main方法在“栈”内存中分配所属的活动空间,此时发生压栈动作,main方法的活动空间处于栈底。

也就是说,方法只定义不去调用的话,只是把它的代码片段存储在方法区当中,java虚拟机是不会在栈内存当中给该方法分配活动空间的,只有在调用的瞬间,java虚拟机才会在“栈内存”当中给该方法分配活动空间,此时发生压栈动作,直到这个方法执行结束的时候,这个方法在栈内存中所对应的活动空间就会释放掉,此时发生弹栈动作。由于栈的特点是先进后出,所以最先调用的方法(最先压栈)一定是最后结束的(最后弹栈)。比如:main方法最先被调用,那么它一定是最后一个结束的。换句话说:main方法结束了,程序也就结束了(目前来说是这样)。

接下来我们来看一段代码,同时画出内存结构图,以及使用文字描述该程序的内存变化:


  
  1. public class MethodTest {
  2. public static void main(String[] args) {
  3. System. out.println( "main begin");
  4. m1();
  5. System. out.println( "main over");
  6. }
  7. public static void m1() {
  8. System. out.println( "m1 begin");
  9. m2();
  10. System. out.println( "m1 over");
  11. }
  12. public static void m2() {
  13. System. out.println( "m2 begin");
  14. System. out.println( "m2 over");
  15. }
  16. }

 

运行结果如下图所示:

图7-10:方法执行过程中内存变化测试程序

通过上图的执行结果我们了解到,main方法最先被调用,但是它是最后结束的,其中m2方法最后被调用,但它是最先结束的。大家别忘了调用的时候分配内存是压栈,结束的时候是释放内存弹栈哦。为什么会是上图的结果呢,我们来看看它执行的内存变化,请看下图:

图7-11:方法执行过程中内存的变化

通过上图的分析,可以很快明白,为什么输出结果是这样的顺序,接下来我们再采用文字的方式描述它的内存变化:

● 类加载器将class文件加载到方法区。

● 开始调用main方法,在栈内存中给main方法分配空间,开始执行main方法,输出”main begin”。

● 调用m1()方法,在栈内存中给m1()方法分配空间,m1()方法处于栈顶,具备活跃权,输出”m1 begin”。

● 调用m2()方法,在栈内存中给m2()方法分配空间,m2()方法处于栈顶,具备活跃权,输出”m2 begin”,继续输出”m2 over”。

● m2()方法执行结束,内存释放,弹栈。

● m1()方法这时处于栈顶,具备活跃权,输出”m1 over”。

● m1()方法执行结束,内存释放,弹栈。

● main()方法这时处于栈顶,具备活跃权,输出”main over”。

● main()方法执行结束,内存释放,弹栈。

● 栈空了,程序结束。

大家是否还记得之前的课程中曾经提到方法体当中的代码是有执行顺序的,必须遵循自上而下的顺序依次逐行执行,当前行代码必须执行结束之后,下一行代码才能执行,不能跳行执行,还记得吗?现在再和栈数据结构一起联系起来思考一下,为什么要采用栈数据结构呢?是不是只有采用这种先进后出的栈数据结构才可以保证代码的执行顺序呢!此时,你是不是感觉程序的设计者在此处设计的非常巧妙呢!

Java递归方法

什么是方法递归?我们先来看一段代码:


  
  1. public class RecursionTest01 {
  2. public static void main(String[] args) {
  3. m();
  4. }
  5. public static void m(){
  6. System. out.println( "m begin");
  7. m();
  8. System. out.println( "m over");
  9. }
  10. }

以上代码的执行结果如下图所示:

 

图7-17:递归执行结果

我们可以看到以上代码的执行过程中,一直输出“m begin”,“m over”一次也没有输出,直到最终发生了错误:java.lang.StackOverflowError,这个错误是栈内存溢出错误,错误发生后,JVM退出了,程序结束了。

实际上以上代码在m()方法执行过程中又调用了m()方法,方法自身调用自身,这就是方法递归调用。以上程序实际上等同于以下的伪代码(说明问题,但是无法执行的代码):

 

图7-18:说明递归执行原理的伪代码

通过伪代码我们可以看出,m()方法一直在被调用(方法中的代码必须遵循自上而下的顺序依次逐行执行,不能跳行执行),对于栈内存来说一直在进行压栈操作,m()方法从未结束过,所以没有弹栈操作,即使栈内存足够大(也是有限的内存),总有一天栈内存会不够用的,这个时候就会出现栈内存溢出错误。通过以上研究得出递归必须要有合法的结束条件,没有结束条件就一定会发生StackOverflowError。我们再来看看有结束条件的递归,例如以下代码:

 

图7-19:递归的过程中满足了某个条件,递归结束了

综上所述,递归其实就是方法在执行的过程中调用了另一个方法,而另一个方法则是自己本身。在代码角度来看就是在a()方法中调用a()方法,使用递归须谨慎,因为递归在使用的时候必须有结束条件,没有结束条件就会导致无终止的压栈,栈内存最终必然会溢出,程序因错误的发生而终止。

大家再来思考一个问题,一个递归程序有合法有效的结束条件就一定不会发生栈内存溢出错误吗?在实际开发中遇到这个错误应该怎么办?

一个递归程序有的时候存在合法有效的终止条件,但由于递归的太深,在还没有等到条件成立的时候,栈内存已经发生了溢出,这种情况也是存在的,所以实际开发中我们尽可能使用循环来代替递归算法,原则是:能不用递归尽量不用,能用循环代替的尽可能使用循环。当然,如果在开发中遇到了由于使用递归导致栈内存溢出错误的发生,首先,我们要检查递归的终止条件是否合法,如果是合法的还是发生栈内存溢出错误,那么我们可以尝试调整堆栈空间的大小。怎么调整堆栈大小呢,大家可以研究一下下图中的一些参数,这里就不再讲解内存大小的调整了,这不是初级程序员应该掌握的。

图7-20:java虚拟机内存设置参数

接下来我们来研究一下在不使用递归的前提下,完成1~N的求和,这个应该很简单,请看下面代码:


  
  1. public class RecursionTest02 {
  2. public static void main(String[] args) {
  3. int n = 5;
  4. int result = accumulate(n);
  5. System. out.println( "1到" + n + "的和是:" + result);
  6. }
  7. public static int accumulate(int n){
  8. int result = 0;
  9. for( int i = 1;i <= n; i++){
  10. result += i;
  11. }
  12. return result;
  13. }
  14. }

运行结果如下图所示:

 

图7-21:不使用递归计算1~N的和

那么,使用递归应该怎么写呢?请看以下代码:


  
  1. public class RecursionTest03 {
  2. public static void main(String[] args) {
  3. int n = 5;
  4. int result = accumulate(n);
  5. System. out.println( "1到" + n + "的和是:" + result);
  6. }
  7. public static int accumulate(int n){
  8. if(n == 1){
  9. return 1;
  10. }
  11. return n + accumulate(n - 1);
  12. }
  13. }

运行结果如下图所示:

 

图7-22:使用递归计算1~N的和

我们来使用伪代码对以上代码的执行过程进行分析,请看以下伪代码:


  
  1. public static int accumulate(int n){ //假设n是5
  2. if(n == 1){
  3. return 1;
  4. }
  5. return n + accumulate(n - 1);
  6. //return 5 + accumulate(4); return 5 + 4 + 3 + 2 + 1;
  7. }
  8. public static int accumulate(int n){
  9. if(n == 1){
  10. return 1;
  11. }
  12. return n + accumulate(n - 1);
  13. //return 4 + accumulate(3); return 4 + 3 + 2 + 1;
  14. }
  15. public static int accumulate(int n){
  16. if(n == 1){
  17. return 1;
  18. }
  19. return n + accumulate(n - 1);
  20. //return 3 + accumulate(2); return 3 + 2 + 1;
  21. }
  22. public static int accumulate(int n){
  23. if(n == 1){
  24. return 1;
  25. }
  26. return n + accumulate(n - 1);
  27. //return 2 + accumulate(1); return 2 + 1;
  28. }
  29. public static int accumulate(int n){
  30. if(n == 1){
  31. return 1; //这行代码执行了 return 1;
  32. }
  33. }

以上程序的内存变化是这样的,请看下图:

 

图7-23:1~N递归求和内存图

为了加强大家对递归算法的理解,我们再来看一张图:

 

图7-24:另一种形式的递归内存图

其实大家把上图逆时针旋转90度,你会看到一个栈数据结构对吗?

Java方法重载

关于方法重载是什么,以及怎么进行重载,这些我们目前先不去研究,先来看看以下代码不使用方法重载机制,存在哪些缺点?


  
  1. public static void main(String[] args) {
  2. int x1 = 10;
  3. int x2 = 20;
  4. int retValue1 = sumInt(x1 , x2);
  5. System. out.println(x1 + "+" + x2 + "=" + retValue1);
  6. long y1 = 10L;
  7. long y2 = 20L;
  8. long retValue2 = sumLong(y1 , y2);
  9. System. out.println(y1 + "+" + y2 + "=" + retValue2);
  10. double z1 = 10.0;
  11. double z2 = 20.0;
  12. double retValue3 = sumDouble(z1, z2);
  13. System. out.println(z1 + "+" + z2 + "=" + retValue3);
  14. }
  15. public static int sumInt(int a , int b){
  16. return a + b;
  17. }
  18. public static long sumLong(long a , long b){
  19. return a + b;
  20. }
  21. public static double sumDouble(double a , double b){
  22. return a + b;
  23. }

运行结果如下图所示:

 

图7-12:没有重载,分析缺点

我们可以看到以上三个方法功能“相似”,都是求和,只不过参与求和的数据类型不同,因此定义了三个方法,分别起了三个不同的方法名。这种方式会增加程序员编程的压力,因为程序员起码要记忆三个方法名,另外代码也不是很美观。怎么解决呢?我们来看看使用方法重载机制之后会是怎样,请看以下代码以及运行结果:


  
  1. public static void main(String[] args) {
  2. int x1 = 10;
  3. int x2 = 20;
  4. int retValue1 = sum(x1 , x2);
  5. System. out.println(x1 + "+" + x2 + "=" + retValue1);
  6. long y1 = 10L;
  7. long y2 = 20L;
  8. long retValue2 = sum(y1 , y2);
  9. System. out.println(y1 + "+" + y2 + "=" + retValue2);
  10. double z1 = 10.0;
  11. double z2 = 20.0;
  12. double retValue3 = sum(z1, z2);
  13. System. out.println(z1 + "+" + z2 + "=" + retValue3);
  14. }
  15. public static int sum(int a , int b){
  16. return a + b;
  17. }
  18. public static long sum(long a , long b){
  19. return a + b;
  20. }
  21. public static double sum(double a , double b){
  22. return a + b;
  23. }

运行结果如下图所示:

 

图7-13:使用重载机制

以上代码使用了方法重载机制,我们可以看到,三个“功能相似”的方法名字都叫做sum,只不过方法的形参不同,这样对于程序员来说,调用方法时所需要记忆的方法名更少一些,代码更加美观一些。

接下来我们正式的研究一下方法的重载机制:什么是方法重载?什么情况下我们考虑使用方法重载?在代码角度来看,满足什么条件的时候构成方法重载?

那么,什么是方法重载呢?方法重载(overload)是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。调用重载方法时,Java编译器能通过检查调用的方法的参数类型和个数选择一个恰当的方法。方法重载通常用于创建完成一组任务相似但参数的类型或参数的个数不同的方法。调用方法时通过传递给它们的不同个数和类型的实参来决定具体使用哪个方法。

什么情况下我们考虑使用方法重载呢?在同一个类当中,如果多个功能是相似的,可以考虑将它们的方法名定义的一致,使用方法重载机制,这样便于程序员的调用,以及代码美观,但相反,如果两个方法所完成的功能完全不同,那么方法名也一定要不一样,这样才是合理的。

代码满足什么条件的时候构成方法重载呢?满足以下三个条件:

● 在同一个类当中。

● 方法名相同。

● 参数列表不同:个数不同算不同,顺序不同算不同,类型不同也算不同。

接下来我们来看看以下程序哪些方法构成了方法重载,哪些没有:

 

图7-14:哪些方法重载了,哪些没有

编译结果如下图所示:

 

图7-15:编译错误信息提示

通过观察以上代码以及测试结果我们得知,方法5和方法4是一样的,这不是方法重载,这叫“方法重复(哈哈)”,因为之前我们就说过方法形参中起决定性作用的是参数的数据类型,参数的名字随意,因为每一个形参都是局部变量,变量名自然是随意的。其中方法6和方法1相同,显然方法的重载和方法的返回值类型没有关系,这也是合理的,因为之前我们提过,方法执行结束之后的返回值我们可以接收也可以不接收。另外方法7和方法1也无法构成重载,显然方法重载和修饰符无关。

总之,方法1和方法2要想构成方法重载,首先它们在同一个类当中,方法名一样,参数列表不同(类型、个数、顺序),这样java虚拟机在运行的时候就可以分清楚去调用哪个方法了。其实,最终要调用哪个方法,还是取决于调用的时候传递的实际参数列表。所以在java编程中要区分两个方法,首先看方法名,如果方法名一致,则继续看它们的形式参数列表。

接下来我们来看一下方法重载在实际开发中的应用,你有没有觉得每一次输出的时候“System.out.println();”这行代码很麻烦,我们来封装一个工具类,请看以下代码:


  
  1. public class U {
  2. public static void p(){
  3. System. out.println();
  4. }
  5. public static void p(int data){
  6. System. out.println(data);
  7. }
  8. public static void p(long data){
  9. System. out.println(data);
  10. }
  11. public static void p(float data){
  12. System. out.println(data);
  13. }
  14. public static void p(double data){
  15. System. out.println(data);
  16. }
  17. public static void p(boolean data){
  18. System. out.println(data);
  19. }
  20. public static void p(char data){
  21. System. out.println(data);
  22. }
  23. public static void p(String data){
  24. System. out.println(data);
  25. }
  26. }

 


  
  1. public class UTest {
  2. public static void main(String[] args) {
  3. U.p( "hello world!");
  4. U.p( 10);
  5. U.p( 9.0);
  6. U.p( false);
  7. U.p( '国');
  8. int a = 10;
  9. int b = 20;
  10. int c = a + b;
  11. U.p(a + "+" + b + "=" + c);
  12. }
  13. }

运行结果如下图所示:

 

图7-16:测试工具类U

看到以上的代码,你是不是感觉以后要打印数据到控制台就很方便了,代码再也不需要写这么多“System.out.println();”,你只需要“U.p();”,当然,你需要把U.java文件编译生成的U.class文件拷贝到你的硬盘当中,一直携带着,什么时候需要的话,把U.class文件放到classpath当中就可以使用了。


转载:https://blog.csdn.net/bjpowernode_com/article/details/109641006
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场