Java面向对象系列[v1.0.0][方法详解]

方法是类和对象的行为特征的抽象,Java中的方法与传统的函数非常相似又有着显著的不同,在结构化语言中函数是最小单位,整个软件由一个一个函数组成;在面向对象语言中,类是最小单位,整个系统由一个一个类组成,在java中方法不能独立存在,必须属于类或者对象。

方法必须属于类或者对象

  • 如果要定义方法,则只能在类体内定义,不能独立定义方法,如果类中的方法由static修饰,那么这个方法属于这个类,否则该方法属于这个类的实例
  • Java语言是静态的,一个类定义完成后,只要不再重新编译这个类文件,该类和类的对象所拥有的方法是固定的,永远都不会改变
  • 方法不能独立定义,也不能独立执行,它的调用方式必须是【类.方法】【对象.方法】
  • 同一个类中的一个方法调用另一个方法时,如果被调用的是普通方法,则默认使用this作为调用者,如果被调用的方法是静态方法,则默认使用类作为调用者,表面上看是独立执行,但实际上只是隐藏了主调
  • 需要注意的是static修饰的方法既可以使用类作为调用者,也可以使用对象作为调用者来调用,但static修饰的方法属于类,虽然使用类的对象调用static修饰的方法能够得到相同的执行结果,但那是因为底层依然是使用这些实例所属的类作为调用者,执行虽然不会报异常,但并不支持这么写程序

Java方法的参数传递机制

如果声明方法的时候指定了形参,那么调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值也被称为实参,在java中,参数的传递方式只有一种,就是值传递,就是将实际参数值的副本(复制一份)传入方法内,而参数本身不会受到任何影响

基本类型传参

public class PrimitiveTransferTest
{
    public static void swap(int a, int b)
    {
        // 下面三行代码实现a、b变量的值交换。
        // 定义一个临时变量来保存a变量的值
        var tmp = a;
        // 把b的值赋给a
        a = b;
        // 把临时变量tmp的值赋给b
        b = tmp;
        System.out.println("swap方法里,a的值是"
            + a + ";b的值是" + b);
    }
    public static void main(String[] args)
    {
        var a = 6;
        var b = 9;
        swap(a, b);
        System.out.println("交换结束后,变量a的值是"
            + a + ";变量b的值是" + b);
    }
}

Java面向对象系列[v1.0.0][方法详解]_第1张图片
Java面向对象系列[v1.0.0][方法详解]_第2张图片
实际上mian栈区中的a和b并没有改变

引用类型传参

引用类型的参数传递,仍旧是值传递方式

class DataWrap
{
    int a;
    int b;
}
public class ReferenceTransferTest
{
    public static void swap(DataWrap dw)
    {
        // 下面三行代码实现dw的a、b两个成员变量的值交换。
        // 定义一个临时变量来保存dw对象的a成员变量的值
        var tmp = dw.a;
        // 把dw对象的b成员变量值赋给a成员变量
        dw.a = dw.b;
        // 把临时变量tmp的值赋给dw对象的b成员变量
        dw.b = tmp;
        System.out.println("swap方法里,a成员变量的值是"
            + dw.a + ";b成员变量的值是" + dw.b);
    }
    public static void main(String[] args)
    {
        var dw = new DataWrap();
        dw.a = 6;
        dw.b = 9;
        swap(dw);
        System.out.println("交换结束后,a成员变量的值是"
            + dw.a + ";b成员变量的值是" + dw.b);
    }
}

Java面向对象系列[v1.0.0][方法详解]_第3张图片
Java面向对象系列[v1.0.0][方法详解]_第4张图片
为了更好的证明main()方法中的dw和s’wswap()方法中的dw是两个变量,在swap()方法的最后一行添加代码

class DataWrap
{
    int a;
    int b;
}
public class ReferenceTransferTest
{
    public static void swap(DataWrap dw)
    {
        // 下面三行代码实现dw的a、b两个成员变量的值交换。
        // 定义一个临时变量来保存dw对象的a成员变量的值
        var tmp = dw.a;
        // 把dw对象的b成员变量值赋给a成员变量
        dw.a = dw.b;
        // 把临时变量tmp的值赋给dw对象的b成员变量
        dw.b = tmp;
        System.out.println("swap方法里,a成员变量的值是"
            + dw.a + ";b成员变量的值是" + dw.b);
        // 把dw直接赋为null,让它不再指向任何有效地址。
        dw = null;
    }
    public static void main(String[] args)
    {
        var dw = new DataWrap();
        dw.a = 6;
        dw.b = 9;
        swap(dw);
        System.out.println("交换结束后,a成员变量的值是"
            + dw.a + ";b成员变量的值是" + dw.b);
    }
}

Java面向对象系列[v1.0.0][方法详解]_第5张图片

  • 把swap()方法中的dw赋值给null后,swap()方法中失去了DataWrap的引用,不可再访问堆内存中的DataWrap对象
  • main()方法中的dw变量不受任何影响,依然引用DataWrap对象,所以依然可以输出DataWrap对象的a、b成员变量值

形参个数可变的方法

在JDK1.5之后,java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参,只需要在定义方法时,在最后一个形参的类型后增加三个点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入

public class Varargs
{
    // 定义了形参个数可变的方法
    public static void test(int a, String... books)
    {
        // books被当成数组处理
        for (var tmp : books)
        {
            System.out.println(tmp);
        }
        // 输出整数变量a的值
        System.out.println(a);
    }
    public static void main(String[] args)
    {
        // 调用test方法
        test(5, "疯狂Java讲义", "轻量级Java EE企业应用实战");
    }
}
// 可变个数的形参定义方法
public static void test(int a, String... books);
// 数组形参定义方法
public static void test(int a, String[] books);
// 调用包含可变个数形参的函数
test(5, "davieyang""alexyang", "Ethanyang");
// 调用包含数组形参的函数
test(5, new String[]{"davieyang", "alexyang", "Ethanyang"})
  • 即使是采用形参个数可变的形式来定义方法,调用该方法时也一样可以为个数可变的形参传入一个数组,本质上还是就是一个数组类型的形参
  • 数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能放在形参列表最后,且只能有一个个数可变的形参

递归方法

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

// f(0)=1, f(1)=4, f(n+2)=2*f(n+1)+f(n), 其中n是大于0的整数,求f(10)
public class Recursive
{
    public static int fn(int n)
    {
        if (n == 0)
        {
            return 1;
        }
        else if (n == 1)
        {
            return 4;
        }
        else
        {
            // 方法中调用它自身,就是方法递归
            return 2 * fn(n - 1) + fn(n - 2);
        }
    }
    public static void main(String[] args)
    {
        // 输出fn(10)的结果
        System.out.println(fn(10));
    }
}
  • 拆解:实际上f(10)=2f(9)+f(8), 而f(9)=2f(8)+f(7)…以此类推能够推到f(2)=2*fn(1)+f(0), 而fn(0)=1并且fn(1)=4,也就是说fn(2)就可以计算出来了,然后再反推回去就可以得到fn(10)的值
  • 回顾递归过程:当一个方法不断调用它本身,必须在某个时候方法的返回值是确定的,也就是我们推到fn(2)的时候是有结果的,不需要再调用它本身(else分支),否则递归就成了无穷递归,类似于死循环
  • 递归方法规定:递归一定要向已知的方向递归
// f(20)=1,f(21)=4,f(n+2)=2*f(n+1)+f(n),其中n是大于0的整数,求f(10)
public class Recursive
{
    public static int fn(int n)
    {
        if (n == 20)
        {
            return 1;
        }
        else if (n == 21)
        {
            return 4;
        }
        else
        {
            // 方法中调用它自身,就是方法递归
            return fn(n + 2) - 2 * fn(n + 1);
        }
    }
    public static void main(String[] args)
    {
        // 输出fn(10)的结果
        System.out.println(fn(10));
    }
}

fn(10)=fn(12)-2fn(11), 而fn(11)=fn(13)-2fn(12)…以此类推知道fn(19)=fn(21)-2*fn(20),也就是说fn(19)我们是能得到确切结果的

递归方法使用场景

例如希望遍历某个文件路径,而文件路径的深度是未知的,我们可以定义一个方法将文件路径作为参数,该方法可以遍历该路径下所有的文件和文件路径,然后再调用自身来处理下层路径

方法的重载

Java允许同一个类里定义多个同名方法,只要形参列表不同即可,在同一个类中包含了了两个或者两个以上的同名方法,但形参不同,则称为方法重载
方法的重载聚焦的是方法名,俩同一不同,及同一个类里相同的方法名,不同的形参列表,至于方法的其他部分并不关心

public class Overload
{
    // 下面定义了两个test()方法,但方法的形参列表不同
    // 系统可以区分这两个方法,这种被称为方法重载
    public void test()
    {
        System.out.println("无参数");
    }
    public void test(String msg)
    {
        System.out.println("重载的test方法 " + msg);
    }
    public static void main(String[] args)
    {
        var ol = new Overload();
        // 调用test()时没有传入参数,因此系统调用上面没有参数的test()方法。
        ol.test();
        // 调用test()时传入了一个字符串参数,
        // 因此系统调用上面带一个字符串参数的test()方法。
        ol.test("hello");
    }
}

Java并不能使用方法返回值类型作为区分方法重载的依据。

形参个数可变的方法重载

public class OverloadVarargs
{
    public void test(String msg)
    {
        System.out.println("只有一个字符串参数的test方法 ");
    }
    // 因为前面已经有了一个test()方法,test()方法里有一个字符串参数。
    // 此处的个数可变形参里不包含一个字符串参数的形式
    public void test(String... books)
    {
        System.out.println("****形参个数可变的test方法****");
    }
    public static void main(String[] args)
    {
        var olv = new OverloadVarargs();
        // 下面两次调用将执行第二个test()方法
        olv.test();
        olv.test("aa", "bb");
        // 下面将执行第一个test()方法
        olv.test("aa");
        // 下面调用将执行第二个test()方法
        olv.test(new String[] {"aa"});
    }
}
  • 定义了两个方法test(String msg)和test(String…books),然而当我们传参的时候只想穿一个字符串“aa”,又想让程序重载test(String…books)这样的传参方式是做不到,系统会自动重载test(String msg)
  • 如果想实现只穿一个字符串又让系统能够重载test(String… books),必须使用数组参数olv.test(new String[] {“aa”});
  • 没多大意义_

成员变量和局部变量

在Java中根据变量定义的位置不同,可以将变量分为成员变量和局部变量,而成员变量和局部变量的运行机制存在较大的差异。
成员变量:是指在类里定义的变量;局部变量:是指在方法里定义的变量;而不论是什么变量都应该遵循相同的命名规则,且具备可读性,应该是多个有意义的单词连缀而成,其中第一个单词的首字母小写,后面每个单词的首字母大写。

成员变量

Java面向对象系列[v1.0.0][方法详解]_第6张图片

  • 类变量:从该类的准备阶段就开始存在,直到系统完全销毁这个类为止,类变量的作用域与这个类的生存范围相同
  • 实例变量:从该类的实例被创建时起开始存在,直到系统完全销毁这个实例,其作用域与对应实例的生存范围相同
  • 类变量与类共存亡,实例变量与实例共存亡,只要类存在,程序就可以访问该类的类变量类.类变量;
  • 只要类的实例存在,程序就可以访问该实例的实例变量实例.实例变量;
  • 类变量也可以让该类的实例访问实例.类变量
class Person
{
    // 定义一个实例变量
    public String name;
    // 定义一个类变量
    public static int eyeNum;
}
public class PersonTest
{
    public static void main(String[] args)
    {
        // 第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0
        System.out.println("Person的eyeNum类变量值:"
            + Person.eyeNum);
        // 创建Person对象
        var p = new Person();
        // 通过Person对象的引用p来访问Person对象name实例变量
        // 并通过实例访问eyeNum类变量
        System.out.println("p变量的name变量值是:" + p.name
            + " p对象的eyeNum变量值是:" + p.eyeNum);
        // 直接为name实例变量赋值
        p.name = "孙悟空";
        // 通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量
        p.eyeNum = 2;
        // 再次通过Person对象来访问name实例变量和eyeNum类变量
        System.out.println("p变量的name变量值是:" + p.name
            + " p对象的eyeNum变量值是:" + p.eyeNum);
        // 前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
        System.out.println("Person的eyeNum类变量值:" + Person.eyeNum);
        var p2 = new Person();
        // p2访问的eyeNum类变量依然引用Person类的,因此依然输出2
        System.out.println("p2对象的eyeNum类变量值:" + p2.eyeNum);
    }
}

注意:Java允许通过实例来访问static修饰的成员变量,这本身就有待商榷,当我们看到这种情形的时候都可以将其替换成类本身访问static成员变量,这样程序的可读性、明确性都大大提高

局部变量

  • 形参:在定义方法的时候,同于给方法传递参数的一种形式上的参数,形参的作用域在方法内有效
  • 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失
  • 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效
  • 与成员变量不同,局部变量中除形参外,都需要显示的初始化,否则无法访问
public class BlockTest
{
    public static void main(String[] args)
    {
        {
            // 定义一个代码块局部变量a
            int a;
            // 下面代码将出现错误,因为a变量还未初始化
            // System.out.println("代码块局部变量a的值:" + a);
            // 为a变量赋初始值,也就是进行初始化
            a = 5;
            System.out.println("代码块局部变量a的值:" + a);
        }
        // 下面试图访问的a变量并不存在
//         System.out.println(a);
    }
}
public class MethodLocalVariableTest
{
    public static void main(String[] args)
    {
        // 定义一个方法局部变量a
        int a;
        // 下面代码将出现错误,因为a变量还未初始化
        // System.out.println("方法局部变量a的值:" + a);
        // 为a变量赋初始值,也就是进行初始化
        a = 5;
        System.out.println("方法局部变量a的值:" + a);
    }
}

当通过类或对象调用方法时,系统会在该方法栈区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,由系统完成了形参的初始化

public class VariableOverrideTest
{
    // 定义一个name实例变量
    private String name = "李刚";
    // 定义一个price类变量
    private static double price = 78.0;
    // 主方法,程序的入口
    public static void main(String[] args)
    {
        // 方法里的局部变量,局部变量覆盖成员变量
        var price = 65;
        // 直接访问price变量,将输出price局部变量的值:65
        System.out.println(price);
        // 使用类名作为price变量的限定,
        // 将输出price类变量的值:78.0
        System.out.println(VariableOverrideTest.price);
        // 运行info方法
        new VariableOverrideTest().info();
    }
    public void info()
    {
        // 方法里的局部变量,局部变量覆盖成员变量
        var name = "孙悟空";
        // 直接访问name变量,将输出name局部变量的值:"孙悟空"
        System.out.println(name);
        // 使用this来作为name变量的限定,
        // 将输出name实例变量的值:"李刚"
        System.out.println(this.name);
    }
}

为了避免异常或者变量被覆盖的情形出现,减少代码调试成本,应避免变量重名,即便java提供了重名情况下的访问方法,但仍旧没必要增加调试成本

成员变量初始化与其在内存中的运行机制

当系统加载类或者创建类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值

// 创建第一个类
Persion p1 = new Persion();

Java面向对象系列[v1.0.0][方法详解]_第7张图片
Java面向对象系列[v1.0.0][方法详解]_第8张图片

// 创建第二个类
Persion p2 = new Persion();

Java面向对象系列[v1.0.0][方法详解]_第9张图片
Java面向对象系列[v1.0.0][方法详解]_第10张图片

局部变量初始化与其在内存中的运行机制

  • 局部变量定义后,必须显示的进行初始化,在初始化之前,系统不会为它分配内存,直到初始化后系统为其分配内存并将初始值保存在这块内存中
  • 与成员边个两不同,局部变量不属于任何类或者实例,它总是保存在其所在方法的栈内存中,如果是基本类型,那么这直接把变量值保存在该变量对应的内存中,如果是引用类型则变量里存储的是内存地址
  • 栈内存中的变量无需系统垃圾回收,往往随着方法或者代码块的运行结束而结束

变量的使用规则

如果仅从程序的运行结果来看,大部分时候都可以直接使用类变量或者实例变量来解决问题,无需使用局部变量,然而如果考虑程序运行的性能那就不能这么简单处理,当我们定义一个成员变量时,成员变量将被放置在堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在的范围,而这无疑增大了变量的生存时间,导致更大的内存开销,同时扩大了变量的作用域,不利于提高程序的内聚性

public class ScopeTest1
{
    // 定义一个类成员变量作为循环变量
    static int i;
    public static void main(String[] args)
    {
        for (i = 0; i < 10; i++)
        {
            System.out.println("Hello");
        }
    }
}
public class ScopeTest2
{
    public static void main(String[] args)
    {
        // 定义一个方法局部变量作为循环变量
        int i;
        for (i = 0; i < 10; i++)
        {
            System.out.println("Hello");
        }
    }
}
public class ScopeTest3
{
    public static void main(String[] args)
    {
        // 定义一个代码块局部变量作为循环变量
        for (var i = 0; i < 10; i++)
        {
            System.out.println("Hello");
        }
    }
}

如果有如下几个情形,则应考虑成员变量:

  • 如果需要定义的变量适用于描述某个类或者某个对象的固有信息的
  • 如果在某个类中需要以一个变量来保存该类或者实例运行时的状态的
  • 如果某个信息是需要在某个类的多个方法之间共享的

即时在程序中使用局部变量,也应该尽可能的缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中驻留的时间越短,程序运行性能就越好。

你可能感兴趣的:(Java基础即高端)