一、方法的定义
我们在学习Java编程以后接触到的第一个程序就是"Hello World”,在这当中涉及到两个主要的结构:类和main方法,当时我们只是说明了main方法是程序的入口,那么当我们想要自己定义一个方法时应该如何下手呢?
1. 概念与作用
首先我们要明确方法的概念和作用,从名称上来说,方法也可以被称为函数,是用来解决同一类的问题的。从代码的结构上来说,定义方法可以减少重复的代码,也能使得整个程序结构更加清爽。
- 假如我们需要计算两个数的加和
public class Test{ public static void main(String[] args){ // 定义两个变量,so easy int a = 10,b = 5; int c = a + b; System.out.println(c);// 15 } }
- 如果我们需要多次反复执行同一个逻辑,那么就会产生很多相同的代码
public class Test{ public static void main(String[] args){ int a = 10,b = 5; int c = 20,d = 10; // 可以看到,虽然变量名称不同,但是计算的逻辑是相同的 // 如果某一段代码反复出现,我们可以考虑将他提取出来变成一个方法 int e = a + b; System.out.println(e);// 15 int f = c + d; System.out.println(f);// 30 int g = e + f; System.out.println(g);// 45 } }
- 定义方法后调用
public class Test{ public static void main(String[] args){ int a = 10,b = 5; int c = 20,d = 10; // 原有的代码逻辑将转变为方法的调用 plus(a,b);// 执行方法时输出:15 plus(c,d);// 执行方法时输出:30 plus(e,f);// 执行方法时输出:45 } // 定义一个用于计算两个数加和的方法,计算后输出结果 public static void plus(int m,int n){ int result = m + n; System.out.println(result); } }
从以上的例子我们可以看到:
- 从结构上来说,方法就是由多行代码所组成的集合
- 从使用的角度来看,定义方法的目的是抽取出通用的部分,可以减少重复代码的出现
- 从最终的效果来看,多行代码的执行转化为了方法的调用
2. 定义的格式
如果我们想定义一个方法,那就需要先了解定义方法的结构,按照次序分别为:
- 修饰符:对方法进行相关的限定,出现在返回值类型之前
- 权限修饰符:一般我们会将权限修饰符写在方法定义的最前面,它指明了这个方法都可以在什么地方被调用,最开始都声明为public即可
- 其他修饰符:可以修饰方法的关键词还有static,final等,会在其他文章中逐一介绍,修饰符的先后顺序没有严格要求
- 返回值类型:指明了方法执行后是否需要进行返回,以及相应的类型
- 方法名:指定方法的名称,方法被调用时使用,在同一类中同名方法将构成重载
- 参数列表:声明调用方法时需要传入的参数,可以为空,也可以多个
- 方法体:方法被调用时所执行的代码,是方法的核心部分,需要与方法的返回值类型呼应
3. 方法的签名
方法名称和参数列表构成了方法签名,方法签名可以唯一的确定一个方法,并且对鉴别是否构成重载十分有用。
public class Test{ // 方法签名:main(String[] args) public static void main(String[] args){ int a = 10,b = 5; int c = plus(a,b); } // 方法签名:plus(int m,int n) public static int plus(int m,int n){ return m + n; } }
4. 方法的注释
在定义一个方法后,我们在使用编译器调用时只能够查看到方法签名及返回值类型,我们希望对于相近或重载的方法进一步进行描述,有利于使用者对方法的区分。
对方法添加注释时需要使用文档注释,称之为javadoc,这样在进行调用时就可以显示方法的相关信息,对于方法的注释主要包括以下几个部分:
- 方法作用描述:描述方法的作用
- 方法参数描述:@param,解释每个参数代表的含义
- 返回类型描述:@return,解释返回值代表的含义
在编译器中可以输入/**
快速生成一个方法的模板,效果如下:
public class Test{ /** * 计算两个数的加和 * @param a 第一个加数 * @param b 第二个加数 * @return 两个数的加和 */ public int plus(int a,int b){ return a + b; } }
二、方法的设计
明确了方法的定义结构之后,我们需要做的就是希望在解决实际问题时知道如何去定义一个方法,并且有一个清晰的思路。
1. 方法设计的思路
笔者认为一个方法的设计其实更像是整个编程思想的缩影,无论是完成一个复杂的功能还是某一个方法的定义都可以按照下面三个步骤来进行:
- What I want?
要定义一个方法,就要先明确:我需要完成怎样一个功能,用于解决一个什么样的问题?明确了之后我们就可以知道这个方法的用途,进而确定方法的名称、返回值类型、调用访问的权限、是否有其他修饰符。
- What I need?
接下来要我们要根据方法的用途,考虑这个方法执行时都需要什么,是否需要传入一些参数?于是我们可以确定参数列表的部分了。
- How to do?
在明确了方法要解决的问题以及所需要的参数之后,我们就可以分析方法中用该编写什么样的代码来解决问题,也就是最后确定方法体的部分,用上传递进来的参数,最后返回应该返回的变量或进行打印输出。
2. 方法名称的确定
方法名称的定义比较容易,因为自定义的程度较高,没有什么强制性的规则,只要满足标识符的规定就可以了。一般来说,方法的命名也需要做到见名知意,以小写字母开头,如果遇到多个单词首字母大写,可以是字母和数字的组合。
3. 参数列表的确定
参数列表的确定主要就是考虑调用方法时需要传入的参数的类型,可以为空,也可以为一个至多个,分别需要声明类型和名称。
- 声明的类型用于限制调用方法时传入参数的类型
- 声明的名称用于代表传递进来的参数
除此之外,我们还需要了解一下各种参数类型之间的差别:
- 基本数据类型:对于基本数据类型,我们可以认为是值的传递,即:这是一个值拷贝之后,复制的过程,我们在方法中如果对参数的值进行修改,也不会改变原有的值。
public class Test{ public static void main(String[] args){ int a = 10; test(a);// 进行方法的调用,方法中对值进行了修改 System.out.println(a);// 结果为10 } public static void test(int n){ System.out.println(n);// 接收到值,结果为10 n = 100;// 修改n的值,不会影响传入的参数a的值 System.out.println(n);// 结果为100 } }
- 引用类型:包括数组在内的引用类型,也就是除了基本数据类型以外的其他类型,在进行传递时发生的是引用传递,也就是说参数接收到的是一个引用,相当于多了一个变量指向了同一个位置,这样在方法中进行的修改直接会作用在对象实例上。
public class Test{ public static void main(String[] args){ int[] a = {1,2,3}; test(a);// 进行方法的调用,方法中对数组a进行了修改 for(int i = 0;i < a.length;i++){ System.out.println(n);// 结果为10,20,30 } } public static void test(int[] n){ for(int i = 0;i < n.length;i++){ System.out.println(n[i]);// 接收数组的引用,结果为:1,2,3 } for(int i = 0;i < n.length;i++){ n[i] = n[i] * 10;// 修改数组的值,每个元素变为原来的10倍 } // 对于修改对象的属性值同理,直接作用在对象本身 } }
- 可变参数:可变参数与数组类似,但是却有所不同,允许调用时以罗列的方式将参数传进来
- 可变参数又叫不定参数,从字面解释就是:有的时候我不确定参数到底有几个,但是又不想每次都构建一个数组,这个时候就可以使用不定参数
- 可变参数在一个方法的定义中只能出现一个
- 可变参数只能出现在参数列表的最后一个位置
- 不建议使用Object类型作为可变参数类型,将在方法重载时说明
- 声明格式:参数类型… 参数名称,如:int… nums
public class Test{ public static void main(String[] args){ int a = 1; int b = 2; int c = 3; test(null);// 调用成功,此时参数为null test();// 调用成功,此时参数个数为0 test(a);// 调用成功,传入1个参数 test(a,b);// 调用成功,传入2个参数 test(new int[]{a,b,c});// 调用成功,也可构建成数组后传入 } public static void test(int... nums){ // 将nums当成数组一样使用即可,可以通过判断数组长度确定传入参数的个数 // 前提是传入的参数不为null,否则会出现空指针异常 if(nums == null){ System.out.println("传入的参数为null"); }else{ System.out.println("传入的参数个数为:" + nums.length); } } }
4. 返回类型的确定
如何确定一个方法是否需要有返回值呢?在上述的方法中,在返回值类型的部分我们使用的都是void关键字,代表此方法返回值为空,或无需返回。其实,对于一个方法是否需要返回这不是一个语法问题,而是取决于我们使用者的需要,我们来讨论一下这两种情况。
- void:代表方法执行后不需要指定返回值,也就是不需要使用return关键字,只需要完成方法的逻辑,输出某些信息,或者通过引用修改对象的某些属性。
- 其他类型
- 返回值类型只能指定一种,但可以是数组类型
- 如果声明了返回值类型,那么必须配合return关键字一同使用
- return在一般情况下只能出现在方法的最后一行,作为方法的结束
- 在选择结构中,也可能不会出现在最后一行的位置,可以根据需要提前结束某一个方法,但是必须保证选择结构对应的所有情况都有相应的返回值
- return后只能跟一个变量的名称或表达式,变量或表达式结果的类型必须和返回值类型相同
- 如果需要同时返回多个变量的值,可以使用数组
- 如果需要同时返回多种类型的变量,可以将返回值类型声明为:Object[]
public class Test{ public static void main(String[] args){ // 需要实现如下逻辑:计算两个数的加和,并将得到的结果变为10倍后输出 int a = 1,b = 2; // 在进行方法调用后,我们必须想办法先得到两个数计算加和的结果,再继续下一步 int c = plus(a,b); // 使用对应类型的变量(c)接收返回结果,然后继续下一步操作 int result = c * 10; System.out.println(result); } public static int plus(int a,int b){ return a + b; } }
5. 方法内容的确定
能够根据需要熟练并快速的写出方法体中的内容这是一个长期训练和锻炼的过程,有的时候我们并不是不知道如何使用方法这种结构,而是给出的问题根本没有任何的思路。在这里笔者将给大家一些建议,因为举再多的例子也无法在短时间内对大家有实质性的帮助。
其实程序本身只是我们一种逻辑思维表达,而且计算机真的很笨,所有的步骤都需要你一步一步去告诉他,比如你想写一个判断素数的程序,不要指望你定义一个变量i,然后使用选择结构在判断条件中写上:if(i == 素数){}计算机就能明白,你首先要让计算机明白什么是素数,或者符合什么样的条件的数是素数。基本上所有的问题都可以转换为一个数学问题,或者是具有步骤的逻辑问题,特别是我们要让计算机帮助我们去完成一项操作或功能的时候,你必须告诉它明确的步骤,以及遇到各种情况要如何处理,毕竟大佬是这么说的:
很多同学看到这句话的第一反应可能是:我信你个鬼!你个xx头子坏得很。但是仔细想想其实很有道理,特别是对于初学者,我们在学习编程时一定要尝试去理解计算机是如何工作的,如何教会它来帮助我们解决问题。
那么笔者的建议可以概括为以下几点:
- 不要着急开始一个方法的编写
- 首先理清问题的解决步骤
- 如果可能,对每一个步骤进行细化,分析可能出现的情况,给出解决的办法
- 结合所学的语法知识,将每一个步骤翻译为相应的结构或代码
- 如果没有解决问题,重复以上步骤
- 经历几次之后你就可以完全在大脑中完成这几个步骤,顺畅的写出方法的内容
三、方法的调用
当一个方法被定义以后,只有被调用了才会被执行,否则也是没有意义的。
1. 方法调用的格式
根据上面的例子,我想对于方法的调用方式大家已经掌握了。没错,很简单:方法名称 + 传入参数。有关于参数的写法上需要作出一点说明,在进行方法定义时,我们需要声明参数的类型,而在调用方法,传入参数时,我们需要做的仅仅是匹配,不要再次声明参数的类型,而只需要保证传入的参数与定义的类型相匹配就好,可以传入一个具体的值,也可以是声明赋值后的变量,还是那句话:类型匹配就好。
2. 方法的执行过程
方法的执行过程其实比较简单,具体的包含嵌套调用的结构我们将在后面的文章中说明。方法的执行过程其实用到的了一个最基本的结构:顺序结构。如果一段代码在执行的过程中遇到了方法调用,那么一定会进入到方法中,将方法中的代码全部执行完毕,再返回到方法的调用处,继续执行后面的代码。
那么这里也给大家解释一下初学者的问题:你说方法中定义的return是返回的意思,那到底返回到哪去了?什么时候返回的?
解释这个问题可以用一句话概括:返回到了调用该方法的位置。首先,只有一个方法被调用以后,才会执行其中的代码,才会轮到return语句的执行,那么return之后去哪了呢?自然是返回到调用这个方法的位置继续执行,这个时候,整个方法的调用语句就代表了这个方法的返回值,我们直接使用对应类型的变量接收就可以了。
public class Test{ public static void main(String[] args){ // 需要实现如下逻辑:计算两个数的加和,并将得到的结果变为10倍后输出 int a = 1,b = 2;// 代码执行步骤:1 // 代码执行步骤:2,进行方法的调用 int c = plus(a,b);// 代码执行步骤:4,进行返回值的赋值 int result = c * 10;// 代码执行步骤:5 System.out.println(result);// 代码执行步骤:6 } public static int plus(int a,int b){ return a + b;// 代码执行步骤:3 } }
3. 调用的注意事项
- static修饰符
static修饰符有很多作用,我们这里只讨论它用在方法上时,对方法的调用产生怎样的影响。由于main方法是程序的入口,那么它必须使用static声明,即:不需要实例化对象即可直接执行。那么由于main方法是static修饰的,那么它直接调用的方法必须也是由静态(static)修饰的。
- 接收返回值
具有返回值的方法在调用后,是不是一定要对返回值进行接收呢?当然不是必须的,如果不接收,方法的值也会正常返回,只不过随即被丢弃了而已。接收时将方法调用语句看成一个整体,直接用对应类型的变量赋值接收即可。
四、方法的重载
1. 重载的概念
重载指的是在一个类中,可以定义多个同名的方法,区别在于参数列表不同。对于重载的概念还是很好理解的,无非是描述了一种现象,在一个类中存在了很多名字相同的方法,大家需要掌握的就是如何定义才符合重载的规则,以及重载有什么用?
- 方法名称相同,参数列表不同
不要看这个概念简单,还是有很多同学在此翻车。方法名称相同很好理解,完全一致的才叫做相同,这里对大小写敏感。另外一个概念是:参数列表不同,大家一定要注意,参数列表相同与否,是靠参数类型以及排列顺序来决定的,与参数名称无关。因为参数列表中声明的参数名称只是传入参数的一个代表,并不具备什么具体的区分意义。
public class Test{ // 求两个整数和的方法:plus public int plus(int a,int b){ return a + b; } // 参数列表相同,不构成重载,不能在类中同时存在 public int plus(int c,int d){ return c + d; } // 参数列表不同,构成重载 public double plus(double a,double b){ return a + b; } // 参数列表不同,构成重载,但是不定参数容易构成调用的歧义,不推荐 public int plus(int... a){ return 0; } // 参数列表相同,方法名称不同,不构成重载,可以在类中同时存在 public int Plus(int a,int b){ return a + b; } }
- 方法重载有什么用?
在很多时候,我们使用方法完成一个功能或逻辑,存在很多种情况,有些情况来自于代码逻辑处理的过程中,也有些情况是要对不同的参数类型做出不同的操作。这个时候我们就可以利用重载的特点,用相同的方法名代表我们要处理的逻辑是类似的,然后在参数列表中声明不同的参数类型,这样就可以避免我们在方法中再繁杂的写各种参数个数的判断,参数类型的判断,更加利于维护。同时,使用相同的方法类型,也使得使用者在调用时变得十分方便,不需要在同一功能上记忆各种不同的方法名称,同时又能很好的解决问题。
2. 重载方法的调用
对于重载方法的调用,由于方法名称相同,jvm主要就是根据传入的参数类型来进行区分,效果如下:
public class Test{ public static void main(String[] args){ int a = 1,b = 2; int c = plus(a,b);// 调用plus(int a,int b) double m = 1.0,n = 2.0; double d = plus(m,n);// 调用plus(double a,double b) } // 求两个整数和的方法:plus public static int plus(int a,int b){ return a + b; } // 方法名相同,参数列表不同,构成重载 public static double plus(double a,double b){ return a + b; } }
从上面的例子我们可以看到,在执行方法调用时主要是通过参数类型来进行区分的。但是当方法中出现不定参数时要尤为注意:
public class Test{ public static void main(String[] args){ int a = 1,b = 2,c = 3; int d = plus(a);// 编译失败,与plus(int... a)和plus(int a,int... b)都匹配 int e = plus(a,b);// 编译成功,调用plus(int a,int b) int f = plus(a,b,c);// 编译失败,与plus(int... a)和plus(int a,int... b)都匹配 int g = plus(new int[]{a,b});// 编译成功,调用plus(int... a) int h = plus(a,new int[]{b,c});// 编译成功,调用plus(int a,int... b) } // 求两个整数和的方法:plus public static int plus(int a,int b){ return a + b; } // 方法名称相同,参数列表不同,构成重载,但是不定参数容易构成调用的歧义,不推荐 public int plus(int... a){ return 0; } // 方法名称相同,参数列表不同,构成重载,但是不定参数容易构成调用的歧义,不推荐 public int plus(int a,int... b){ return 0; } }
从以上的例子我们可以看到,如果重载方法中出现了不定参数,那么在调用时很可能出现歧义,依然要通过手动构建数组的方式来解决,所以在进行方法重载时应该尽量避免出现不定参数,当不定参数是Object类型时,歧义问题就会更加严重。