anonymous的Java学习笔记(6)——Java面向对象之方法详解

方法

方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。

Java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

Java里方法的所属性

  • 方法不能独立定义,方法只能在类里边定义
  • 从逻辑意义上来讲,方法要么属于该类本身,要么属于该类的一个对象
  • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

方法调用

  1. 使用static修饰的方法,可以使用类对象来调用。
  2. 不使用static修饰的方法,只能使用类对象来调用。

方法参数传递机制

Java里方法参数传递方式只有一种:值传递,所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

基本类型的值传递代码示例

package com.abc.part_four;

/**
 * @Auther: ABC
 * @Date: 2020/5/3 12:27
 * @Description: 验证值传递就是将`实际参数值的副本(复制品)`传入方法内,而参数本身不会受到任何影响。
 */
public class PrimitiveTransferTest {
    public static void swap(int a, int b) {
        int tmp = a;
        a = b;
        b = tmp;
        System.out.println("a和b交换后的值为:" + "a = " + a + ", b = " + b);
    }

    public static void main(String[] args) {
        /**
         * 当程序执行swap()方法时,系统进入swap()方法,并将main()法中的a,b变量作为参数值传入swap()方法,
         * 传入swap()方法的只是a,b的副本,而不是a,b本身。
         */
        int a = 3;
        int b = 4;
        swap(3, 4);
        System.out.println("交换结束后的a, b值为:" + "a = " + a + ", b = " + b);
    }
    /**
     * 输出:
     * a和b交换后的值为:a = 4, b = 3
     * 交换结束后的a, b值为:a = 3, b = 4
     */
}

引用类型的值传递代码示例

package com.abc.part_four;

/**
 * @Auther: ABC
 * @Date: 2020/5/3 12:56
 * @Description: 引用类型的值传递代码示例:验证值传递就是将`实际参数值的副本(复制品)`传入方法内,而参数本身不会受到任何影响。
 */
public class ReferenceTransferTest {
    public static void swap(String a, String b) {
        String tmp = a;
        a = b;
        b = tmp;
        System.out.println("a和b交换后的值为:" + "a = " + a + ", b = " + b);
    }

    public static void main(String[] args) {
        String a = "小花花";
        String b = "小亮亮";
        swap(a, b);
        System.out.println("交换结束后的a, b值为:" + "a = " + a + ", b = " + b);
        /**
         * 输出:
         * a和b交换后的值为:a = 小亮亮, b = 小花花
         * 交换结束后的a, b值为:a = 小花花, b = 小亮亮
         */

    }
}

值传递的实质

当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法里操作的并不是实际的实参变量。

形参个数可变的方法

如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

代码示例

package com.abc.part_four;

import java.util.Arrays;

/**
 * @Auther: ABC
 * @Date: 2020/5/3 21:48
 * @Description:
 */
public class Varargs {
    public static void test(int a, String... books) {
        System.out.println("一共" + a + "本书, " + Arrays.toString(books));
        System.out.println("分别是以下这些书:");
        for (String book : books){
            System.out.println(book);
        }
    }

    public static void main(String[] args) {
        test(3, "疯狂Java讲义", "疯狂Python讲义", "Java核心技术卷");
        /**
         * 输出:
         * 一共3本书, [疯狂Java讲义, 疯狂Python讲义, Java核心技术卷]
         * 分别是以下这些书:
         * 疯狂Java讲义
         * 疯狂Python讲义
         * Java核心技术卷
         */
    }
}

需要注意的点

  • 数组形式的形参可以处于形参列表的任意位置,但个数可变的形参(如String... books)只能处于形参列表的最后。
  • 一个方法中最多只能有一个个数可变的形参
  • 个数可变的形参本质就是一个数组类型的形参

递归方法

一个方法体内调用它自身,被称为方法递归。方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

递归方法实现计算数字阶乘斐波拉切数列,代码示例

package com.abc.part_four;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @Auther: ABC
 * @Date: 2020/5/3 22:29
 * @Description:
 */
public class Recursive {
    /**
     * 计算一个整数的阶乘,如果用字母n来代表一个整数,阶乘代表着所有`小于或等于n`且大于0的整数的乘积。
     * 阶乘通常简写成 n!
     * 例如: 5! = 1 * 2 * 3 * 4 * 5 = 120
     *
     * @param n
     * @return 计算结果
     */
    public static int fn(int n) {
        if (n == 1) {
            return 1;
        } else {
            //方法中调用它自身,就是方法递归
            return n * fn(n - 1);
        }
    }

    /**
     * 递归函数实现斐波那契数列
     * 斐波那契数列:[1,1,2,3,5,8,13,21,34....],第一个数是1,后面的数等于前面两个数相加的结果
     * 打印1-50内的斐波拉切数列
     *
     * @param m
     * @return 计算结果
     */
    public static int fn1(int m) {
        if (m == 1 || m == 2) {
            return 1;
        } else {
            return fn1(m - 1) + fn1(m - 2);
        }
    }


    public static void main(String[] args) {
        int result = fn(5);
        System.out.println("5! = " + result);//5! = 120

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入需要打印斐波拉契数列中的数字个数:");
        int numCount = scanner.nextInt();
        List<Integer> list=new ArrayList<Integer>();
        for (int i = 1; i <= numCount; i++){
            list.add(fn1(i));
        }
        System.out.print("斐波拉契数列中前" + numCount + "个数字为:" + list);
        /**
         * 输出:
         * 5! = 120
         * 请输入需要打印斐波拉契数列中的数字个数:
         * 10
         * 斐波拉契数列中前10个数字为:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
         */
    }
}

方法重载

Java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。

确定一个方法的三个要素

  • 调用者,也就是方法的所有者,可以是类,也可以是对象。
  • 方法名。
  • 形参列表,当调用方法时,系统会根据传入的实参列表进行匹配。

方法重载的要求:二同一不同

  • 同一个类中方法名相同,参数列表不同。方法返回值类型、修饰符等,与方法重载没有任何关系。
package com.abc.part_four;

public class OverLoad {
    /*
    方法重载,同一个类中,方法名相同,形参列表不同。
     */
    public void test(String name, int age) {
        System.out.println("我的名字是:" + name + ", 我的年龄是:" + age);
    }

    public void test(String name, char gender) {
        System.out.println("我的名字是:" + name + ", 我的性别是:" + gender);
    }

    public static void main(String[] args) {
        OverLoad overLoad = new OverLoad();
        overLoad.test("小花花", 21);
        overLoad.test("小花花", '女');
        /**
         * 输出:
         * 我的名字是:小花花, 我的年龄是:21
         * 我的名字是:小花花, 我的性别是:女
         */
    }
}

成员变量和局部变量

在Java语言中,根据定义变量位置的不同,可以将变量分成两大类:成员变量和局部变量。

  • 成员变量
    • 成员变量指的是在类里定义的变量。
  • 局部变量
    • 局部变量指的是在方法里定义的变量。

变量分类图
变量分类图

成员变量

成员变量被分为类变量和实例变量两种,定义成员变量时没有static修饰的就是实例变量,有static修饰的就是类变量。

  • 类变量从该类的准备阶段起开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同。
  • 实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。

正是基于以上的原因,可以把类变量和实例变量统称为成员变量。

  • 类变量可以理解为类成员变量,它作为类本身的一个成员,与类本身共存亡;
  • 实例变量则可理解为实例成员变量,它作为实例的一个成员,与实例共存亡。

只要类存在,程序就可以访问该类的类变量。在程序中访问类变量通过如下语法:

类.类变量

只要实例存在,程序就可以访问该实例的实例变量。在程序中访问实例变量通过如下语法:

实例.实例变量

当然,类变量也可以让该类的实例来访问。通过实例来访问类变量的语法如下:

实例.类变量

代码示例

Person1.java

package com.abc.part_four;

public class Person1 {
    public String name;
    public static int eyeNum;
}

Person1Test.java

package com.abc.part_four;

public class Person1Test {
    public static void main(String[] args) {
        Person1 person1 = new Person1();
        Person1 person2 = new Person1();
        System.out.println("1)我的名字是:" + person1.name + ", 我有" + Person1.eyeNum + "只眼睛。");
        //通过Person1类访问类变量
        Person1.eyeNum = 1;
        System.out.println("2)我的名字是:" + person1.name + ", 我有" + Person1.eyeNum + "只眼睛。");
        //通过Person1类示例访问类变量
        person1.eyeNum = 2;
        System.out.println("3)我的名字是:" + person1.name + ", 我有" + Person1.eyeNum + "只眼睛。");
        //通过Person1类示例person1访问实例变量
        person1.name = "小花花";
        System.out.println("4)我的名字是:" + person1.name + ", 我有" + Person1.eyeNum + "只眼睛。");
        ////通过Person1类示例person2访问类变量eyeNum,访问到的是修改后的eyeNum
        System.out.println("5)我的名字是:" + person2.name + ", 我有" + Person1.eyeNum + "只眼睛。");
        /**
         * 程序输出:
         * 1)我的名字是:null, 我有0只眼睛。
         * 2)我的名字是:null, 我有1只眼睛。
         * 3)我的名字是:null, 我有2只眼睛。
         * 4)我的名字是:小花花, 我有2只眼睛。
         * 5)我的名字是:null, 我有2只眼睛。
         */
    }
}

从上面程序运行结果不难发现,类变量的作用域比实例变量的作用域更大。实例变量随实例的存在而存在,而类变量则随类的存在而存在。实例也可访问类变量,同一个类的所有实例访问类变量时,实际上访问的是该类本身的同一个变量,也就是说,访问了同一片内存区。

如前面提到的,Java允许通过实例来访问static修饰的成员变量本身就是一个错误, 因此以后看到通过实例来访问成员变量的情形,都可以将它替换成通过类本身来访问static成员变量的情形,这样程序的可读性、明确性都会大大提高。

局部变量

局部变量根据定义形式的不同,又可以被分为如下三种

  • 形参:在定义方法时定义的变量,形参的作用域在整个方法内有效。
    • 形参的作用域是整个方法体内有效,而且形参也无须**显式初始化**,形参的初始化在调用该方法时由系统完成,形参的值由方法的调用者负责指定。
    • 当通过类或对象调用某个方法时,系统会在该方法枝区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,这就完成了形参的初始化。
    package com.abc.part_four;
    
    public class VariableOverrideTest {
        //创建两个成员变量
        private String name = "小花花";//实例变量
        private static int age = 21;//;类变量
    
        public void info(char gender) {
            String name = "大花花";
            System.out.println("我叫" + name + ", 性别是:" + gender);//我叫大花花, 性别是:女
            //使用this来调用实例变量
            System.out.println(this.name);//小花花
    
        }
    
        public static void main(String[] args) {
            //创建一个局部变量
            int age = 22;
            System.out.println("我的年龄是" + age);//我的年龄是22
            System.out.println("小花花的年龄是" + VariableOverrideTest.age);//小花花的年龄是21
            new VariableOverrideTest().info('女');
        }
    }
    

    从上面代码可以清楚地看出局部变量覆盖成员变量时,依然可以在方法中显式指定类名和使用this(只能用来调用实例变量,不能调用类变量)作为调用者来访问被覆盖的成员变量。这使得编程更加自由,不过大部分时候还是应该尽量避免这种局部变量和成员变量同名的情形。

  • 方法局部变量:在方法体内定义的局部变量,作用域是从定义该变量的地方开始生效,到该方法结束时失效。
    package com.abc.part_four;
    
    public class MethodLocalVariableTest {
        public static void main(String[] args) {
            int b;//定义一个方法局部变量b
            //System.out.println("方法局部变量的值为:" + b);//会报错,因为方法局部变量还为进行初始化。
            b = 7;//为方法局部变量进行赋值,也就是进行初始化。
            System.out.println("方法局部变量的值为:" + b);//方法局部变量的值为:7
    
    
        }
    }
    
  • 代码块局部变量:在代码块中定义的局部变量,作用域是从定义该变量的地方开始生效,到该代码块结束时失效。
    package com.abc.part_four;
    
    public class BlockTest {
        public static void main(String[] args) {
            {
                int a;//定义一个代码块局部变量a
                //System.out.println("代码块局部变量a的值为:" + a);//会报错,因为未对代码块局部变量a做初始化,
                a = 5;
                System.out.println("代码块局部变量a的值为:" + a);//初始化局部变量a后才可以正常进行编译和输出:代码块局部变量a的值为:5
            }
            //System.out.println(a);//会报错,代码块外是访问不到局部变量a的。
        }
    
    }
    

    从上边程序代码可以看出来,程序执行完代码块后,代码块局部变量a立即被销毁。代码块局部变量a只能在代码块中可以访问得到,在代码块是访问不到的。

成员变量与局部变量比较

ScopeTest1.java

package com.abc.part_four;

public class ScopeTest1 {
    //定义一个类成员变量作为循环变量
    static int i;

    public static void main(String[] args) {
        for (i = 1; i <= 10; i++) {
            System.out.println(i);
        }
    }
}

ScopeTest2.java

package com.abc.part_four;

public class ScopeTest2 {
    public static void main(String[] args) {
        //定义一个方法局部变量作为循环变量
        int i;
        for (i = 1; i <= 10; i++) {
            System.out.println(i);
        }
    }
}

ScopeTest3.java

package com.abc.part_four;

public class ScopeTest3 {
    public static void main(String[] args) {
        //定义一个代码块局部变量作为循环变量
        for (int i = 1; i <= 10; i++) {
            System.out.println(i);
        }
    }
}

以上三个程序执行结果完全相同,但是第三种最符合软件开发规范。对于一个循环变量而言,只需要它在循环体内有效即可,因此这个变量需要在代码块中进行定义,从而保证这个变量的作用域只在该代码块中。该局部变量占用的内存更小,程序性能更健壮。