目录
1、为什么需要方法
2、什么是方法
3、方法的定义
4、方法的返回值
5、方法的调用
6、实参和形参之间的关系
7、方法执行中JVM的内存分配
8、方法的重载 (overload)
9、方法的递归调用
10、递归调用在JVM中的内存分配
PS:判断素数时,程序中i<=n/2用的是什么原理?
1、为什么需要方法
在代码的编写过程中,经常会遇到重复对某个功能进行实现的要求,这就意味着我们人需要代码能够进行重复的利用,以便于简化代码的编写和调用,这就是方法存在的必然因素。所以在设计代码的构成的时候,对于能够构成独立功能的代码,可以考虑剥离出来,形成一个单独的方法,便于日后的调用和使用。
2、什么是方法
方法其实就是一段独立的代码片段,该代码片段独立的实现某个功能,可以重复的调用,它的作用就相当于C语言中的函数.
3、方法的定义 修饰符列表 返回值类型 方法名(形式参数列表){
方法体;
}
修饰符列表 返回值类型 方法名(形式参数列表){
方法体;
}
方法体由{ }括号括起来
①修饰符列表,目前先了解public static,其他写法后续了解
②返回值类型:一般是方法执行结束后,会有一个值,该值返回到方法调用处供程序员使用。
③方法名:遵循java中对于标识符的规定就可以了
④形参列表:其实就是变量的定义,多个形参之间用逗号分隔,更重要的是数据类型,和变量的名称无关。
⑤方法体:java语句,一般用于实现该方法所需要完成的功能。
————方法定义的注意事项:
(1)方法的定义要在类体中进行定义,类中可以定义多个方法,类的定义不存在定义的先后顺序之分
(2)方法不可以嵌套定义,但是可以嵌套使用
(3)方法体的代码和普通的java代码一样,遵循从上到下的执行原理。
4、方法的返回值
返回值关键字:return + 字面值/运算或者运行结果为字面值的一个表达式
返回值用return语句实现,可以返回任意数据类型的数值。但是注意如果方法有返回值,那么方法首部中,返回值类型一定不能为空或者void,返回值的数据类型要和return语句返回的数据的数据类型相对应,如果返回值和返回值类型不能匹配、无法 进行自动类型转换或者没有人为的进行强制类型转换,会报错【error:不兼容的类型】,注意一个方法在某种条件下只能存在一个返回值。
返回值的类型可以是任意的数据类型:{
byte,short ,int,long,float,double,String,char,boolean,void,引用数据类型;
}
如果方法不需要返回值,那么返回值类型不能不写,要写明“void”,并且return语句不能有返回值,例如:
public static void 方法名(形式参数1……n){ 方法体; 【return;】 //不写返回值,或者去掉该语句,不写return语句;}
为什么没有返回值的情况下可以写“return;”呢?保留这个功能是因为,return从本质上代表着方法的中止语句,遇到该语句,它所在的方法就彻底的执行结束了。其实return语句就相当于方法的结束语句,类似于循环的break语句的作用,只不过中止的是整个方法的执行过程,
例如:
public static void pu(int n){ for(int i=0;i<=5;i++){ System.out.println(i); if(i==2)return; } }
输出结果如下:
012进程已结束,退出代码为 0
【很明显在遇到return之后,整个方法体的执行就彻底结束了。】
但是如果需要返回值,在方法结束之后一定要返回一个具体数据类型的值,否则会报错。注意这个返回值并不是一定要接收的,由调用者选择是否设置一个对应数据类型的变量接收该值,然后供后期的运算使用。但是一般会接收的,因为一般那个数据是程序员所期望的数据,一般具有一定的作用。
break和return的区别
·break用于结束循环,但是如果在该循环后还有相关的java代码,依旧可以顺序执行
·但是return不同,一旦在方法中,该语句执行,那么整个方法就结束了,在return语句之后的任意java语句都是无法再进行执行的,方法就彻底的中止了。所以return语句的执行意味着方法的结束,所以在它所在的方法作用域当中,其后不能再编写任何代码,是无法访问到的
public static void pu(int n){ return; System.out.print("end"); }
如上代码会报错,如下:
D:IdeaJAVATESTsrctest3.java:68:9java: 无法访问的语句
break也是同理:
for(int i=0;i<=5;i++){ break; System.out.print(i); }
也依旧会报错:
D:IdeaJAVATESTsrctest3.java:72:13java: 无法访问的语句
以上在编译阶段就能够发现错误,无需到运行阶段。
//关于返回值类型和return语句之间的对错,在编译阶段就能够检查到,无需等到巡行后报错。
5、方法的调用
方法只定义不调用是不会发挥方法的作用的,所以只有我们去调用写好的方法才能够发挥其作用。方法可以重复多次的进行合理调用
——调用语句:
对于静态的方法,采用【类名·方法名(实际参数)】的方式进行调用,当调用方法和非 调用方法在同一个类体中的时候,类名可以省略。如果省略了类名,默认从当前类找方法, 但是找不到,会报错。 所以如果调用的方法不是在当前类,请务必加上类名
对于非静态的方法,如果在同一个类当中,采用【方法名(实际参数)】既可以完成方 法的调用。
——调用原理:
类——汽车工厂 实际参数——原材料 形式参数——材料接收器
方法——产生不同产品的车间 返回值——产物
在车间中(类),用户拿着原材料(实际参数)放入材料接收器(形式参数),经过产生不同产品的车间(方法)的处理,取到产物(返回值)。
在方法的调用过程中,形参列表可以有0~N个,是局部变量,有自身的数据类型,形参中起决定性作用的是形参的数据类型,名字只是在该方法作用域内可以使用的一个变量,变量名是任意的。
在用户调用的时候,传递过来的原材料就是实参,注意实参的类型一定要和形参的数据类型对应相同,参数的数目也要相同。
因为方法体中代码自上而下执行的特性,当被调用方法全部执行结束或者由于遇到return结束之后,调用方法的那一行代码才相当于执行结束,之前提到方法不能嵌套定义,但是注意方法可以任意调用,也可以嵌套调用,但是注意不要调用成环,这样程序会没有出口,导致“死循环”;
6、实参和形参之间的关系
在用户调用方法并传递了一定数目的实参的时候,如果实参的数据类型均为一般数据类型,那么在实参和形参之间传递的只是数值,即变量所在内存单元存储的字面值,两个变量的内存单元是不相同的两个内存单元,占用两个不同的内存地址,二者互不影响。但是如果实参的数据类型是引用数据类型,那么在实参和形参之间传递的是实参所在的地址,即二者是指的一个内存单元地址,如下所示:
其实就相当于形参变量被赋予了实参变量所包含的值,这就相当于一般的赋值,所以在这个关系当中,类型的转换规则依旧满足。
规则:数据类型的转换,有强制类型转换和自动类型转换两种。
1、在基本数据类型中,除了boolean类型的数据之外,其他的数据类型之间是可以互相的实现数据类型的转换的。
2、从小的数据类型转换成大的数据类型的时候,是自动类型转换,计算机会自动的进行该转换
3、从大的数据类型转换成小的数据类型的时候,是强制类型转换
数据类型,范围由小到大:
boolean=byte
但是强制类型转换有可能会导致数据损失精度,尽量避免
4、sun公司给予了byte,short,char三种数据类型特权,如果赋值的整形字面值在这几种数据类型的范围内,就可以直接的进行赋值,而不损失精度,其实内部也有强制类型转换过程,只不过是由计算机完成的。
5、当byte,short,char和int类型的数据进行运算时,均转换为int类型然后在进行运算
6、当多种数据类型都在一起进行运算时,会转换成最大取值范围的数据类型进行运算
7、方法执行中JVM的内存分配
方法只有调用并为之分配内存空间后才可以真正的发挥作用,如果不调用,JVM是不会为之分配内存空间的,只有在调用的时候才会为其动态的分配内存空间。
在JVM的内存划分上,有三块主要的内存空间,分别是:方法区内存,堆内存和栈内存,当然还存在其他内存空间,但是主要的就这三种。
首先我们要了解以下栈这种数据结构,以便于更好的理解在方法调用时内存的分配。
栈(stack)————
一种数据结构(数据存储的形态和方式),可以用于数据存储,该数据结构有两种操作,压栈(push)和弹栈(pop),压栈即往在栈内分配内存单元(存入信息),弹栈即从栈内释放内存单元(取出信息),并且栈这种数据结构入口和出口是同一个,所以参考以下羽毛球桶,只能从一个口存取,并且先存入的”乒乓球“在下边,后放入的”乒乓球“在上边,如果取出时,先取出的是后放入的乒乓球,即栈的存取遵循【先入后出】的存取原则。
栈的最顶部的元素是栈顶元素,最底部的(最先存入的元素)是栈底元素,通常会有一个指针,叫做栈帧,指向当前的栈顶元素。并且栈顶元素是栈中唯一一个活跃的元素,即栈顶元素处于活跃的状态,其他元素静止不动。
了解栈的结构之后,就相对容易理解在
方法调用时的内存分配————
方法在执行时,代码片段(字节码文件)以及java中自带类库中的类对应的字节码文件在类加载时,有类加载器放入方法区当中,所以在三块主要的内存区域当中,方法区内存在代码开始执行前最先使用,字节码文件在方法区内存中只有一份,但是可以被重复的进行调用。
代码的执行过程是在栈内存中分配内存的,每次调用一个方法的时候,会在栈内存中单独的为该方法的执行分配独立的活动场所(内存空间),此时发生的是压栈操作,在当前活跃的方法执行结束或者遇到return语句之后,在栈内存中,会发生弹栈动作,释放当前栈顶元素所占用的活动空间,栈帧下移,指向当前活跃的新的栈顶元素。
因为遇到return语句之后,就会发生弹栈操作,所以return就相当于弹栈操作。
而方法中定义的局部变量,包括内部定义的变量以及形参,都是在栈为该方法分配的内存单元中储存,所以局部变量在运行阶段内存是在栈中分配的。
如下代码所示:
public class test3 { public static void main(String[] args) { pu(2); } public static void pu(int n){ int i=10; }}
其内存分配图如下所示:
当main方法开始执行,main压栈,然后当调用pu方法时,为pu方法的执行分配活跃的内存单元,因为该方法有自身的局部变量和形参,所以内部包含两个局部变量。
栈内存中如下图所示:
8、方法的重载 (overload)
对于功能相似,参数不同(参数的顺序、数据类型或者参数的数目)的几个方法,可以利用方法的重载,简化代码的处理、编写和调用,优化代码。
例如:当对多种类型的两个数进行求和时,照常写的代码如下所示:
public static void main(String[] args) { int a=1; int b=2; float c=1; float d=2; int sumInt=sumInt(a,b); float sumFloat=sumFloat(c,d); System.out.println(sumInt); System.out.println(sumFloat); } public static int sumInt(int a,int b){ return a+b; } public static float sumFloat(float a,float b){ return a+b; }
运行结果:
33.0进程已结束,退出代码为 0
但是很明显,sumInt和sumFloat的功能在本质上是相同的,但是却用了两个方法名去命名两个方法,这样在调用的时候,程序员需要记忆更多的方法相关信息,避免调用出现错误,同时增加了代码的编写复杂度。
那么有没有一种方法能够让调用者感觉只调用了一个方法,但是实现了多种功能的效果呢?这就是方法重载的最根本的意义。
如果利用方法重载,那么以上的程序只需要编写为:
public static void main(String[] args) { int a=1; int b=2; float c=1; float d=2; int sumab=sum(a,b); float sumcd=sum(c,d); System.out.println(sumab); System.out.println(sumcd); } public static int sum(int a,int b){ return a+b; } public static float sum(float a,float b){ return a+b; }
很明显在代码的理解、编写和调用上更加的美观方便,一劳永逸,用一个方法名,传入不同的参数就可以实现不同的效果。
经过观察,可以发现如果想要构成方法重载,那么在这几个重载方法之间,他们的方法名是相同的,但是参数列表不同。
参数的类型不同:可行
private static int sum(int a,int b){ return a+b; } public static float sum(float a,float b){ return a+b; }
参数的顺序不同:可行
private static int sum(int a,float b){ return 0; } public static int sum(float a,int b){ return 1; }
参数的数目不同:可行
public static float sum(int a,int b){ return a+b; } public static int sum(int a){ return 2; }
方法的修饰符列表不同:不可行
方法的返回值类型不同:不可行
所以方法的重载和方法名、参数有关,和修饰符列表以及方法的返回值无关;
即重载方法的方法名必须相同,参数必须不同,仅修改修饰符列表和返回值不能构成重载。
9、方法的递归调用
递归调用:在一个方法的内部调用自身方法,注意要合理的利用,否则可能会导致无限次的调用,使代码的运行陷入死循环,最终导致内存的溢出,代码报错,并不能达到程序员的预期,方法的功能也就不存在意义了,所以递归一定要求停止的一个条件存在,保证这个调用环的结束和中止。
所以递归操作就是一个不断的压栈,然后再出栈的过程,很消耗内存。尽可能少的利用递归,但是有时候递归很重要,能够解决一些具体的复杂问题,例如在求阶乘的时候
10、递归调用在JVM中的内存分配
public class test3 { static int sum=0; static int n=0; public static void main(String[] args) { Scanner in=new Scanner(System.in); int n=in.nextInt(); System.out.println(sum(n)); System.out.println(test(n)); } //不使用递归完成1~n的求和 public static int sum(int n){ int sum=0; for(int i=1;i<=n;i++){ sum+=i; } } //使用递归计算1~n的求和 public static int sum(int i){ if(i==1) return 1; else if(i>1){ return i+sum(i-1); }else{ return 0; } } //计算5的阶乘,不用递归 public static int test(int n){ int end=1; for(int i=n;i>=1;i--){ end=end*i; } return end; } //计算5的阶乘,用递归完成 public static int test(int n){ if (n==1) return 1; else return n*test(n-1); }
输入:515120120进程已结束,退出代码为 0
阶乘内存分配图:(仅展示栈内存)