方法

  • 方法:实现了代码复用
[修饰符列表] + 返回值 + 方法名 + (形式参数列表){
	方法体;
}

形式参数列表中每一个参数都是一个局部变量,方法结束之后内存释放;

形参的数据类型起决定性作用,形参对应的变量名是随意的

方法调用语句: 类名.方法名

public class MethodTest01 {
    public static void main(String[] args) {
        MethodTest01.pr1();
        MethodTest01.pr2();//若方法都在同一个类中,类名.是可以省略的
    }
    public static void pr1(){}
    public static void pr2(){}
}
class My{
    //若在一个类中调用另一个类的方法,类名.是不能省略的
}

方法调用时参数传递

public static void main(String[] args){
    Person p = new Person();
    p.age = 10;
    add(p);
    System.out.println(p.age); //11
}

public static void add(Person p){
    p.age++;
    System.out.println(p.age); //11
}

以上的两个p,在栈中属于两个不同的方法栈帧,但是指向了堆内存中的同一个对象,所以访问到了同一个实例变量

练习题

生成五位验证码:

StringBuilder builder = new StringBuilder();
char[] chs = new char[52];
for (int i = 0; i < 52; i++) {
	chs[i] = i < 26 ? (char)('A' + i) : (char)('a' + (i - 26)); //注意减26
}
Random random = new Random();
for (int i = 0; i < 4; i++) {
	builder.append(chs[random.nextInt(chs.length)]);
}
builder.append(random.nextInt(10));

内存结构

栈内存 堆内存 方法区
方法执行所需的空间及该方法的局部变量 对象及对象的实例变量 类加载器将字节码文件装在到JVM的时候,会将字节码文件存入方法区,存储的是代码片段
局部变量 对象 静态变量
  • 类先加载:方法区先有数据
  • new 实际上是在堆内存中开辟了一块空间
  • 栈:方法被调用时,该方法需要的内存空间在栈中分配

局部变量:只在方法体中有效,方法结束后局部变量的内存空间就被释放了

注意:

  1. 方法区最先有数据,方法区中存放代码片段,class字节码
  2. 栈内存:方法调用时,该方法所需的内存在栈中分配,方法不调用时不会在栈中分配内存的

栈内存 stack

存储:局部变量。

  1. 基本数据类型的局部变量,在栈内存中存储的是“数据值”。
  2. 引用数据类型的局部变量,在栈内存中存储的是“地址值”。

栈内存特点:

  1. 栈内存具备“先进后出”或“后进先出”的特点。
  2. 栈内存是一块连续的内存空间,由系统自动分配,效率高!
  3. 虚拟机会为每个线程创建一个栈内存,用于存放该线程执行方法的信息。

栈内存的组成:

堆内存heap

存储:数组或对象

  1. 在堆内存中创建数组或对象成功后,还可以在栈内存中定义一个局部变量,让该局部变量来存储创建在堆内存中数组或对象的首地址,这样该变量就引用了堆内存中的数组或对象。
  2. 数组和对象在没有变量指向(引用)它的时候,则数组或对象本身占用的堆内存空间就变成垃圾,然后就被Java虚拟机的自动垃圾回收器(GC)释放了该存储空间,这样该数组或对象就不能被使用了,否则就会抛出空指针异常(NullPointerException)。

堆内存特点:

  1. 虚拟机中只有一个堆内存,被所有的线程共享。
  2. 堆内存是一个不连续的内存空间,分配灵活,效率低!

方法重载

方法重载机制:功能相似的,可以让其方法名相同

编译器在区分的时候,会首先根据方法名进行区分;如果方法名相同的,再用参数类型进行区分 (也就是说返回值类型是无法区分的)

重载时通过参数的静态类型而不是实际类型进行区分;静态类型在编译期可知,在编译阶段,javac编译器就根据静态类型决定来调用哪个版本的方法。

静态分派发生在编译阶段,确定静态分派的动作实际上不是由虚拟机执行的。

满足三个条件:

  1. 在同一个类当中
  2. 方法名不同
  3. 参数列表不同
  • 方法重载与返回值类型修饰符列表无关

对于println方法来说,参数类型是随意的,说明println()是方法重载

System.out.println();

也可以手动对println方法进行封装:

class S{
    public static void p(){
        System.out.println();
    }
    public static void p(byte b){
        System.out.println(b);
    }
    public static void p(int b){
        System.out.println(b);
    }
    public static void p(long b){
        System.out.println(b);
    }
    public static void p(char b){
        System.out.println(b);
    }
    public static void p(String b){
        System.out.println(b);
    }
    public static void p(boolean b){
        System.out.println(b);
    }
    public static void p(float b){
        System.out.println(b);
    }
    public static void p(double b){
        System.out.println(b);
    }
}

调用的时候就可以:

public class OverLoadTest01 {
    public static void main(String[] args) {
        S.p(10);
        S.p(10.0);
        S.p(true);
    }
}

注意:源码当中一个方法以;结尾并且用native关键字修饰 表示调用了底层C++写的dll程序

构造方法重载

构造方法默认支持方法重载,因为方法名必须和类名保持一致

但是注意,方法重载要求参数列表不同,也就是说:

public Person(String name){
    
}
public Person(String age){
    
}

这两个构造方法是不能重载的,因为参数名称不同并不是参数列表不同。

构造方法虽然在没有返回值类型,但是执行结束之后会返回会该对象的内存地址,也就是用定义好的 引用 来接受

char类型参数的方法重载

public static void main(String[] args) {  
	sayHi('a');  
}  
  
public static void sayHi(int i){  
  
}  
  
public static void sayHi(char c){  
  
}

这时调用的是char类型参数的sayHi方法,如果注释调这个方法,只保留int类型的sayHi,编译运行都是没问题的,因为 'a' 除了代表一个字符之外,还可以代表数字97

以下程序是没有问题的:

public static void main(String[] args) {  
	sayHi('a');   //char自动转换为int
}  
public static void sayHi(int i){  
	System.out.println(i);  
}  
  
// public static void sayHi(char c) {  
//  
// }

但是如果注释int类型参数的方法,在main方法中就不能将int类型赋值给char类型:

public static void main(String[] args) {  
	sayHi(97);  //java: 不兼容的类型: 从int转换到char可能会有损失 
}  
/* public static void sayHi(int i){  
System.out.println(i);  
}*/  
  
public static void sayHi(char c) {  
  
}

编译器只知道传递的参数是int类型的,不知道具体的值

byte、short类型参数的重载

public class Overload {  
	public static void sayHello(byte arg){  
		System.out.println("hello byte");  
	}  
	  
	public static void sayHello(short arg){  
		System.out.println("hello short");  
	}  
	  
	public static void main(String[] args) {  
		sayHello(1);  
	}  
}

编译报错:

java: 对于sayHello(int), 找不到合适的方法
    方法 Overload.sayHello(byte)不适用
      (参数不匹配; 从int转换到byte可能会有损失)
    方法 Overload.sayHello(short)不适用
      (参数不匹配; 从int转换到short可能会有损失)

因为字面量默认被当作int类型处理,int类型不能自动转型到byte或short,要解决这个问题,就要指明参数的静态类型:

sayHello((byte) 1);  
sayHello((short) 1);

可变长参数

例如:

public static void test(int...num){
        
    }

表示test方法可以接受0 – n个int类型的参数,也就是可以不传参数,也可以传多个参数

public static void main(String[] args) {
    test();
    test(1);
    test(1,2,3,4);
}
public static void test(int...num){
    System.out.println("test method execute");
}

注意:可变长参数列表只能出现1个,并且只能出现在参数列表的最后一个位置上

另外,可变长参数可以当作数组处理:

public static void test(int...num){
    for (int i = 0; i < num.length; i++) {
        System.out.print(num[i]);
    }
    System.out.println();
}

可变长参数可以是任何类型的值

class Group {
    private String[] names;

    public void setNames(String... names) {
        this.names = names;
    }
}

上面的setNames()就定义了一个可变参数。调用时,可以这么写:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String

完全可以把可变参数改写为String[]类型:

class Group {
    private String[] names;

    public void setNames(String[] names) {
        this.names = names;
    }
}

但是,调用方需要自己先构造String[],比较麻烦。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

另一个问题是,调用方可以传入null

Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null

方法递归

归化递归

方法递归三要素:

  • 递归公式
  • 递归终结点
  • 递归方向必须走向终结点

计算 n!

归化递归都可以根据以上三要素计算出递归过程

递归公式:f(n) = f(n - 1) * n

递归终结点:f(1) = 1

递归方向必须走向终结点:从 f(10) 递向f(1),所以递归公式是f(n) = f(n - 1) * n

public static int factorial(int n){  
    if (n == 1)  
        return 1;  
    return factorial(n - 1) * n;  
}

猴子摘桃

猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个 第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少。

递归公式:f(n + 1) = f(n) / 2 - 1

递归终结点:f(10) = 1

递归方向:f(1) 递归到 f(10) ,所以要改变递归公式为 f(n) = 2 * f(n + 1) + 2

public static int monkey (int day){  
    if(day == 10)  
        return 1;  
    return 2 * monkey(day + 1) + 2;  
}

上台阶

小明一次可以爬一阶或两阶楼梯,20层楼有几种走法?

由以上内容可知:

$$
F(20) = F(19) + F(18)
$$
走到20层楼的走法 = 走到19楼再走向20楼的走法 + 走到18楼再走向20楼的走法是一致的。

  • 走到19楼再走向20楼的走法:上一阶
  • 走到18楼再走向20楼的走法:上两阶,因为从18楼上一阶的走法和19楼再走向20楼的上一阶走法重复了

递归公式为:f(n) = f(n - 1) + f(n - 2)

递归终结点为:f(1) = 1 到1楼有一层走法 和 f(2) = 2 到2楼有两层走法

递归方向:f(20)f(1) f(2)

非归化递归

见[[011-IO#统计每种文件个数并打印|统计每种文件个数并打印 BFS]]

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。