Java零基础(第七章)---》方法(方法重载和方法递归)
🏀方法初了解
❤️对于一个java程序来说,如果没有“方***存在什么问题?
⭐️代码无法得到复用。怎么提高复用性?可以定义方法,然后需要使用该功能的时候,直接调用一下方法即可。这样代码就得到复用了。⭐️方法(英语单词:method)是可以完成某个特定功能的并且可以被重复利用的代码片段。在C语言中,方法被称为“函数”。在java中不叫函数,叫做方法。
❤️注意:
⭐️程序开始执行的时候是先执行main方法;因为main方法是一个入口。⭐️在java语言中所有的方法体中的代码都必须遵循自上而下的顺序依次逐行执行。
⭐️main方法不需要程序员手动调用,是由JVM调用的。但是除了main方法之外其他的方法,都需要程序员手动调用,方法只有调用的时候才会执行,方法不调用是不会执行的。
⭐️方法定义在类体当中,方法定义的先后顺序没有关系;定义在main方法前面或者main方法后面都是可以的!
⭐️只能在类体中定义方法,不能在main方法体中定义方法!
❤️例1
public class Test01{ //下面是类体 //3.定义的求商方法,定义在main方法前面 public static void mulInt(int m,int n){ int q = m*n; System.out.println(m+"*"+n+ "=" +q); } //1.主方法main程序的入口 public static void main(String[] args){ sumInt(100,200); sumInt(666,888); mulInt(2,3); } //2.定义的求和方法,定义在main方法后面 // x y z在以下的sumInt方法中都属于局部变量 // 局部变量有一个特点:方法结束之后,局部变量占用的内存会自动释放。 public static void sumInt(int x,int y){ int z = x+y; System.out.println(x+"+"+y+ "=" +z); } }
🏀方法语句
🥅方法的定义
❤️方法怎么定义,语法机制是什么
[修饰符列表] 返回值类型 方法名(形式参数列表){ 方法体; }![]()
❤️注意:
⭐️[] 符号叫做中括号,以上中括号[]里面的内容表示不是必须的,是可选的。
⭐️方法体由Java语句构成。
⭐️方法定义之后需要去调用,不调用是不会执行的。
❤️1.关于修饰符列表:
⭐️修饰符列表不是必选项,是可选的。现在统一写成:public static
❤️2.关于返回值类型:
⭐️什么是返回值?
返回值一般指的是一个方法执行结束之后的结果。结果通常是一个数据,所以被称为“值”,而且还叫“返回值”。
方法就是为了完成某个特定的功能,方法结束之后大部分情况下都是有一个结果的,而体现结果的一般都是数据。数据得有类型。这就是返回值类型。⭐️返回值类型可以是任何类型,只要是java中合法的数据类型就行,数据类型包括基本数据类型和引用数据类型,也就是说返回值类型可以是:byte short int long float double boolean char String......
main{ // 调用a方法 a();..如果a方法执行结束之后有返回值,这个返回值返回给main了。 } a(){} 方法执行结束之后的返回值实际上是给调用者了。谁调用就返回给谁。![]()
⭐️当一个方法执行结束不返回任何值的时候,返回值类型也不能空白,必须写上void关键字。所以void表示该方法执行结束后不返回任何结果。⭐️如果返回值类型“不是void”,那么你在方法体执行结束的时候必须使用"return 值;"这样的语句来完成“值”的返回,如果没有“return 值;”这样的语句那么编译器会报错。return 值; 这样的语句作用是什么?作用是“返回值”,返回方法的执行结果。
⭐️只要有“return”关键字的语句执行,当前方法必然结束。return只要执行,当前所在的方法结束,记住:不是整个程序结束。⭐️如果返回值类型是void,那么在方法体当中不能有“return 值;”这样的语句。但是可以有“return;”语句。这个语句“return;”的作用就是用来终止当前方法的。除了void之外,剩下的都必须有“return 值;”这样的语句。
❤️3.方法名:
方法名要见名知意。(驼峰命名方式)方法名在标识符命名规范当中,要求首字母小写,后面每个单词首字母大写。只要是合法的标识符就行。
#java#❤️4.形式参数列表
⭐️注意:形式参数列表中的每一个参数都是“局部变量”,方法结束之后内存释放。形参的个数是:0~N个。
⭐️例如:public static void sumInt(){} public static void sumInt(int x){} public static void sumInt(int x, int y){} public static void sum(int a, int b, double d, String s){}![]()
❤️5.方法体:
⭐️由Java语句构成。java语句以“;”结尾。
⭐️方法体当中编写的是业务逻辑代码,完成某个特定功能;在方法体中的代码遵循自上而下的顺序依次逐行执行;在方法体中处理业务逻辑代码的时候需要数据,数据来源就是这些形参。🥅方法的调用
❤️方法定义之后怎么调用呢?
⭐️方法必须调用才能执行;调用语法是:类名.方法名(实际参数列表);
⭐️实参和形参的类型必须一一对应,另外个数也要一一对应public class Test02{ public static void main(String[] args){ //1.没有返回值的调用 Test02.divInt(3,2); //2.有返回值的调用 int ret = Test02.mulInt(3,2); System.out.println(ret); //3.double 接收int的返回值 //double是大容量。int是小容量。自动类型转换。 double sum = Test02.sumInt(3,2); System.out.println(sum); //4.有返回值,不接受也能编译通过,可以直接打印 Test02.subInt(2,3); System.out.println(Test02.subInt(2,3));//可以这样直接打印 } //1.没有返回值的调用 public static void divInt(int x,int y){ System.out.println(x/y); } //2.有返回值的调用 public static int mulInt(int x,int y){ return x*y; } //3.传过去int值,用double接收也可以 public static int sumInt(int x,int y){ return x+y; } //4.有返回值但是可以选择不接收 public static int subInt(int x ,int y){ return x-y; } //5.对于没有返回值的方法,只能定义为void,且不能有return “值”;但可以有return //对于有返回值的方法,我们根据类型来定义,返回值返回后,我们接收不接收都可以,编译不报错 }![]()
❤️类名的省略
⭐️在代用方法时,一般是可以直接省略类名.的,直接调用方法也可以,但是有特殊情况,我们必须要写上类名!
⭐️在方法调用的时候,什么时候“类名.”是可以省略的。什么时候不能省略?
答:a()方法调用b()方法的时候,a和b方法都在同一个类中,“类名.”可以省略。如果不在同一个类中“类名.”不能省略。
//1.类名省略也能执行 public class Test03{ public static void main(String[] args){ Test03.sumInt(3,2); sumInt(8,3);//同一个类下,不同方法名之间的调用类名省略也是可以的 //跨类代用 Myclass.subInt(10,5);//不同类下,不同方法名之间的调用类名必须写上 } public static void sumInt(int x,int y){ System.out.println(x/y); } } //2.跨类调用时就要加上类名;默认的是调用本类的方法名! class Myclass{ public static void subInt(int x,int y){ System.out.println(x*y); } }![]()
⭐️例2:
(1)不只是main方法可以调用其他方法;所有的方法之间都是可以相互调用的!并且我们可以验证一下它们的调用顺序;
(2)实际上就是压栈出栈的过程;先进栈的,mian方法最后执行结束;最后进栈的m3方法最后执行结束;
public class Test04{ public static void main(String[] args){ System.out.println("main begin"); //1.main方法调用m1方法 m1(); System.out.println("main over"); } //2.m1方法调用m2方法 public static void m1(){ System.out.println("m1 begin"); m2(); System.out.println("m1 over"); } //3.m2方法调用m3方法 public static void m2(){ System.out.println("m2 begin"); T.m3();//跨类调用要带上类名 System.out.println("m2 over"); } } //4.m3方法调用T方法 class T{ public static void m3(){ System.out.println("m3 begin"); System.out.println("T's m3 method execute!"); System.out.println("m3 over"); } } //执行顺序是 /* main begin m1 begin m2 begin m3 begin T's m3 method execute! m3 over m2 over m1 over main over */![]()
🥅break和return的区别
❤️break;语句和return;不是一个级别。
⭐️ break;用来终止switch语句和离它最近的循环。(break用来终止循环)
⭐️return;用来终止离它最近的一个方法。(return用来终止方法)public class MethodTest06{ public static void main(String[] args){ for(int i = 0; i < 10; i++){ if(i == 5){ break; // 终止for循环,打印1234和Hello World return; // 终止当前的方法,只打印1234 } System.out.println("i = " + i); } System.out.println("Hello World!"); } }![]()
❤️例1:经典错误
方法在声明的时候指定了返回值类型为 int 类型,java 语法则要求方法必须能够“百分百的保证”在结束的时候返回int 类型的数据,不然就会报错!
//1.错误: 缺少返回语句 public static int m(){ boolean flag = true; if(flag){ return 1;//有判断,并不是百分之百有返回值,但是定义的却是有返回值int } } //2.解释:编译器不负责运行程序,觉得以上代码的return有可能执行,也有可能不执行 //对于编译器来说,编译器只知道flag变量是boolean类型 // 编译器会认为flag有可能是false,有可能是true //3.修改 //第一种方案:带有else分支的可以保证一定会有一个分支执行。 public static int m(){ boolean flag = true; if(flag){ return 1; }else{ return 0; } } //第二种方案:该方案实际上是方案1的变形。 public static int m(){ boolean flag = true; if(flag){ return 1; } return 0; } //第三种方案:三目运算符 public static int m(){ boolean flag = true; return flag ? 1 : 0; }![]()
❤️例2:方法的调用
// 大家分析以下代码,编译器会报错吗? public class MethodTest07{ public static void main(String[] args){ //1. 调用方法 int result = m(); System.out.println(result); // 1 //2. 调用x方法 int result1 = x(true); System.out.println("result1 = " + result1); //3. 再次调用x方法 int result2 = x(false); System.out.println("result2 = " + result2); } //无参方法 public static int m(){ boolean flag = true; return flag ? 1 : 0; } //带有一个参数的方法。 public static int x(boolean flag){ return flag ? 1 : 0; } }![]()
🥅JVM内存结构
❤️JVM主要的三块内存空间:栈、堆、方法区;除了这三块,还有其他的!
⭐️方法区:类加载器classloader,将硬盘的.class字节码文件装载到JVM的时候,会将字节码文件存放到方法区当中;也就是说方法区中存储的是代码片段;因为类需要加载,所以方法区中最先有数据!
⭐️栈(stack)内存:方法执行需要的空间,这里会存储该方法的局部变量;在方法被调用的时候,该方法需要的内存空间在栈中分配
⭐️堆:以后会讲
🥅方法执行时内存的结构
⭐️局部变量:只在方法体中有效,方法结束之后,局部变量的内存就释放了。
⭐️JVM三块主要的内存:栈内存、堆内存、方法区内存。
⭐️方法区:最先有数据;方法区中放代码片段。存放xxxx.class字节码。
⭐️堆内存:后面讲。
⭐️栈内存:方法调用的时候,该方法需要的内存空间在栈中分配以及局部变量。
⭐️方法只定义不调用,是不会在栈中分配空间的;方法只有在调用的时候才会在栈中分配空间,并且调用时就是压栈。
⭐️方法执行结束之后,该方法所需要的空间就会释放,此时发生弹栈动作。
方法调用叫做:压栈。分配空间方法结束叫做:弹栈;释放空间
//* int a = 100; int b = a; 这个赋值原理是:将a变量中保存的100这个数字复制一份传给b变量。 所以a和b是两个不同的内存空间,是两个局部变量。 *// public class MethodTest08{ //主方法,入口 public static void main(String[] args){ System.out.println("main begin"); int x = 100; m1(x);//把x的值复制一份传给m1方法 System.out.println("main over"); } public static void m1(int i){ // i是局部变量 System.out.println("m1 begin"); m2(i);//把i的值复制一份传给m2方法 System.out.println("m1 over"); } public static void m2(int i){ System.out.println("m2 begin"); System.out.println(i);//打印i的值 System.out.println("m2 over"); } }![]()
打印的结果如下,我们来看看执行的内存变化:
main begin //进栈 m1 begin //进栈 m2 begin //进栈 100 //打印结果 m2 over //出栈 m1 over //出栈 main over //出栈![]()
编辑
❤️例1:编写一个方法求一个整数的阶乘
public class Test01{ public static void main(String[] args){ java.util.Scanner s = new java.util.Scanner(System.in); int n = s.nextInt(); System.out.println(n+ "的阶乘" + "=" +fac(n)); } public static int fac(int n){ int sum = 1; for(int i=1;i<=n;i++){ sum *= i; } return sum; } }![]()
❤️例2:输出大于某个正整数n的最小的质数,例如:5输出7(注意思想)
⭐️这里注意一点,num++只能放在if的前面,因为让求的是大于本身数的质数,不能是当前数;如果放到后面,当前数是质数就直接跳出循环了!
public class Test07{ public static void main(String[] args){ java.util.Scanner s = new java.util.Scanner(System.in); int num = s.nextInt(); while(true){ num++;//num++放前面;因为取的是大于当前的数,不包括当前数 if(x(num)){ System.out.println(num); break; } //----------上面while可以代码优化为 while(!x(++num)){ } System.out.println(num); } //这个方法判断返回的是不是质数 public static boolean x(int n){ for(int i=2;i<n;i++){ if(n%i == 0){ return false; } } return true; } }![]()
🏀方法重载
🥅方法重载的使用
❤️例1:不使用方法重载
⭐️方法功能不同,但是相似,分别起了不同的名字,有什么缺点?
⭐️缺点包括两个:
第一个:代码不美观(不好看、不整齐)。【这是次要的】
第二个:程序员需要记忆更多的方法名称,程序员比较累。public class Test08{ public static void main(String[] args){ System.out.println(sumLong(10L,20L)); System.out.println(sumDouble(10.0,20.0)); } public static long sumLong(long x,long y){ return x+y; } public static double sumDouble(double x,double y){ return x+y; } }![]()
❤️例2:使用方法重载
⭐️使用方法重载机制。解决之前的两个缺点。
优点1:代码整齐美观。
优点2:“功能相似”的,可以让“方法名相同”,更易于以后的代码编写。⭐️在java语言中,是怎么进行方法区分的呢?
(1)首先java编译器会通过方法名进行区分。
(2)在java语言中允许方法名相同的情况出现。如果方法名相同的情况下,编译器会通过方法的参数类型进行方法的区分。//方法名相同,通过不同的参数进行调用匹配 public class Test08{ public static void main(String[] args){ System.out.println(sum(10L,20L)); System.out.println(sum(10.0,20.0)); } public static long sum(long x,long y){ return x+y; } public static double sum(double x,double y){ return x+y; } }![]()
🥅什么时候使用方法重载
❤️什么时候需要考虑使用方法重载?
⭐️在同一个类当中,如果“功能1”和“功能2”它们的功能是相似的,那么可以考虑将它们的方法名一致,这样代码既美观,又便于后期的代码编写(容易记忆,方便使用)。
⭐️注意:方法重载overload不能随便使用,如果两个功能压根不相干,不相似,根本没关系,此时两个方法使用重载机制的话,会导致编码更麻烦。无法进行方法功能的区分。❤️什么时候代码会发生方法重载?
⭐️条件1:在同一个类当中
⭐️条件2:方法名相同
⭐️条件3:参数列表不同
(1)参数的个数不同算不同
(2)参数的类型不同算不同
(3)参数的顺序不同算不同❤️注意:不管代码怎么写,最终一定能让java编译器很好的区分开这两个方法。
⭐️方法重载和方法的“返回值类型”无关。
⭐️方法重载和方法的“修饰符列表”无关。❤️例1:方法重载
public class OverloadTest03{ public static void main(String[] args){ //1.参数个数不同 m1(); m1(100); //2.顺序不同 m2(10, 3.14); m2(3.14, 10); //类型不同 m3(100); m3(3.14); } //1.----------------------------- public static void m1(){ System.out.println("m1无参数的执行!"); } // 这个方法的参数个数和上面的方法的参数个数不同。 public static void m1(int a){ System.out.println("m1有一个int参数执行!"); } //2.----------------------------- public static void m2(int x, double y){ System.out.println("m2(int x, double y)"); } // 参数的顺序不同,也算不同。 public static void m2(double y, int x){ System.out.println("m2(double y, int x)"); } //3.----------------------------- public static void m3(int x){ System.out.println("m3(int x)"); } // 参数的类型不同。 public static void m3(double d){ System.out.println("m3(double d)"); } //4.不在同一个类当中,不能叫做方法重载。 class MyClass{ public static void m1(int x, int y){ } }![]()
❤️例2:方法重复
public class Test08{ public static void main(String[] args){ //1.和形参的类型有关,和命名无关 public static void m4(int a, int b){ } public static void m4(int x, int y){ } //2.和返回值类型无关 public static int m5(){ return 1; } public static double m5(){ return 1.0; } //3.和修饰符列表无关 void m6(){ } public static void m6(){ } }![]()
❤️例3:println的重载
⭐️我们在使用System.out.println()时,println()可以打印各种各样的数据,其实就是使用了函数重载。
⭐️println后面有()也是一个方法名。println参数类型可以随便传。这说明println()方法重载了。//println()各种各样的数据类型都可以传进去,进行打印 public class OverloadTest04{ public static void main(String[] args){ System.out.println(10); System.out.println(3.14); System.out.println(true); System.out.println('a'); System.out.println("abc"); } }![]()
🥅简单的封装
我们每次打印时都要输入System.out.println()是不是觉得太长了,怎么利用方法的重载、调用、封装实现一个简单的打印机制?
❤️第一步:利用重载封装一个简单的S.class
⭐️编译S.java文件生成S.class文件
// 目前我们正在学习的一个内容是:方法重载机制(overload) // 以下所有的p()方法构成了方法的重载。 public class S{ // 换行的方法 public static void p(){ System.out.println(); } // 输出byte public static void p(byte b){ System.out.println(b); } // 输出short public static void p(short s){ System.out.println(s); } // 输出int public static void p(int i){ System.out.println(i); } // 输出long public static void p(long l){ System.out.println(l); } // 输出float public static void p(float f){ System.out.println(f); } // 输出double public static void p(double d){ System.out.println(d); } // 输出boolean public static void p(boolean b){ System.out.println(b); } // 输出char public static void p(char c){ System.out.println(c); } // 输出String public static void p(String s){ System.out.println(s); } }![]()
❤️第二步:写程序HelloWorld.java调用S.class
⭐️我们在当前S.class文件夹下在创建一个HelloWorld.java,打印输出HelloWorld;此时输出就不要用System.out.println()了,直接调用类S下的p方法就可以
public class HelloWorld{ public static void main(String[] args){ S.p(100); S.p(20.0); S.p("Hello World"); S.p(true); S.p(100+200); } }![]()
🥅方法的递归
❤️什么是方法递归?
⭐️方法自己调用自己,这就是方法递归。❤️当递归时程序没有结束条件,一定会发生:
⭐️栈内存溢出错误:StackOverflowError,会一直压栈而不弹栈;所以:递归必须要有结束条件。JVM发生错误之后只有一个结果,就是退出JVM。❤️递归假设是有结束条件的,就一定不会发生栈内存溢出错误吗?
⭐️假设这个结束条件是对的,是合法的,递归有的时候也会出现栈内存溢出错误。因为有可能递归的太深,栈内存不够了。因为一直在压栈。
❤️在实际的开发中,不建议轻易的选择递归,能用for循环while循环代替的,尽量使用循环来做。因为循环的效率高,耗费的内存少。递归耗费的内存比较大,另外递归的使用不当,会导致JVM死掉。(但在极少数的情况下,不用递归,这个程序没法实现。)❤️在实际的开发中,遇到了:StackOverflowError该怎么办办?
⭐️首先第一步:
先检查递归的结束条件对不对。如果递归结束条件不对,必须对条件进一步修改,直到正确为止。
⭐️第二步:假设递归条件没问题,怎么办?
这个时候需要手动的调整JVM的栈内存初始化大小。可以将栈内存的空间调大点。(可以调整大一些。)
⭐️第三步:调整了大小,如果运行时还是出现这个错误,只能继续扩大栈的内存大小。 (java -X)这个可以查看调整堆栈大小的参数,例如:java -Xss xxxGB xx.java❤️对于递归一定要有两个条件
⭐️存在限制条件,当满足这个限制条件的时候,递归便不再继续;例如:(n>10)
⭐️每次递归调用之后越来越接近这个限制条件;例如:(n/10)⭐️递归其实就是压栈出栈的过程!
❤️例1
public class RecursionTest01{ // 入口 public static void main(String[] args){ doSome(); } public static void doSome(){ System.out.println("doSome begin"); doSome(); //没有递归结束条件,实际上是死递归,下面这行代码永远执行不到。 System.out.println("doSome over"); } }![]()
❤️例2:计算1~n的和
/* 计算0~n的和,采用递归和非递归方式 */ //方法1:采用循环方式 public class Test02{ public static void main(String[] args){ java.util.Scanner s = new java.util.Scanner(System.in); int num = s.nextInt(); System.out.println(sumInt(num)); } public static int sumInt(int n){ int sum = 0; for(int i=1;i<=n;i++){ sum += i; } return sum; } } //方法2:采用递归方式 //对于递归的思想就是大事化小!一定要有递归结束条件! public class Test02{ public static void main(String[] args){ java.util.Scanner s = new java.util.Scanner(System.in); int num = s.nextInt(); int result = sumInt(num); System.out.println(result); } public static int sumInt(int n){ if(n == 1){ return 1; } return n+sumInt(n-1); } }![]()
🥅牛刀小试
❤️递归求n的阶乘
public class Test03{ public static void main(String[] args){ java.util.Scanner s = new java.util.Scanner(System.in); int num = s.nextInt(); int ret = fac(num); System.out.println(ret); } //1.方法1 public static int fac(int n){ if(n==0){ return 1; } return n*fac(n-1); } //2.方法2 public static int fac(int n){ if(n > 0){ return n*fac(n-1); } return 1; } }![]()