JAVA 面向对象之方法详解

版权声明:本文为博主原创文章,未经博主允许可以转载,但请保留原文链接。 https://blog.csdn.net/tmdlife/article/details/51935325

本页面更新日期: 2016年07月18日

前言

方法是类或对象 的行为特征的抽象.
方法是类或对象最重要的组成部分.
所有的方法都必需定义在类里.
方法不能独立存在.
方法在逻辑上要么属于类, 要么属于对象.

方法的所属性

一旦将一个方法定义在某个类的类体内.
如果这个方法使用了static修饰符,则这个方法属于这个类.
否则这个方法属于这个类的实例.

Java语言是静态的.
一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,永远都不会改变.

执行方法时必需使用类或对象来作为调用者.
即所有方法都必需使用 类.方法 或 对象.方法 的形式来调用.

这里可能有个问题: 同一个类里不同方法之间相互调用时,
不就可以直接调用嘛?

这里需要指出:
同一个类的一个方法调用另一个方法时,如果被调用方法是普通方法,则默认使用 this 作为调用者;
如果被调方法是静态方法,则默认使用类作为调用者.
也就是说,表面上看起来某些方法可以被独立执行, 但实际上还是使用 this 或者 类 来作为调用者.

永远不要把方法当成独立存在的实体.
正如现实世界由类和对象组成,而方法只能作为类和对象的附属.
Java语言里方法的所属性主要体现在如下几个方面:

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

使用 static 修饰符的方法属于这个类本身.
使用 static 修饰付的方法 既可以使用类作为调用者来调用.
也可以使用对象作为调用者来调用.
但需要指出的是, 因为使用 static 修饰的方法还是属于这个类的.
因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果.
这是由于底层依然是使用这些实例所属的类作为调用者.

虽然Java允许你写的时候,拿该类的实例来调用 static 修饰的类方法.
但是你要明白其中的运行机制.
所以建议你自己写程序的时候, 尽量不要混着用.
用实例来调用实例的方法, 用类来调用 static 修饰的类方法.

没有 static 修饰符的方法则属于该类的对象,不属于这个类本身.
因此没有 static 修饰符的方法只能使用对象作为调用者来调用.
不能使用类作为调用者来调用.
使用不同对象作为调用者来调用同一个普通方法可能会得到不同的结果.

看到这里, 你应该明白, 使用 static 和 不使用 static 修饰的方法是有区别的.
以后我们写程序的时候, 你慢慢就能体会到其中的奥妙.

方法的参数传递机制

如果声明方法时包含了形参声明.
则调用方法时必需给这些形参指定参数值.
调用方法时实际传给形参的参数值也被称为实参.

那么,Java的实参值是如何传入方法的呢?
这是由Java方法的参数传递机制来控制的.
Java里方法参数传递方式只有一种 – 值传递.
所谓值传递,就是将实际参数值的副本(复制品)传入方法内.
而参数本身不会受到任何影响.

小插曲:
Java里参数传递类似于<西游记>里的孙悟空.
孙悟空复制了一个假孙悟空,这个假孙悟空具有和孙悟空相同的能力.
可除妖或被砍头.
但不管这个假孙悟空遇到什么事,真孙悟空不会受到任何影响.
与此类似,传入方法的是实际参数的复制品.
不管方法中对这个复制品如何操作,实际参数值本身不会受到任何影响.

写个程序演示一下方法参数传递的效果:

public class PrimitiveTransferTest
{
  public static void swap(int a, int b)
  {
    //下面三行代码实现 a, b变量的值交换
    //定义一个临时变量来保存 a 变量的值
    int tmp = a;
    //把 b 的值赋给 a
    a = b;
    //把临时变量 tmp 的值赋给 a
    b = tmp;
    System.out.println("swap方法里, a 的值是:" + a + "; b 的值是:" + b);
  }

  public static void main(String[] args)
  {
    int a = 6;
    int b = 9;
    PrimitiveTransferTest.swap(a, b);
    System.out.println("交换结束后, 变量 a 的值是:" + a + "; 变量 b 的值是:" + b);
  }
}

运行之后, 你可以结果中得知.
swap() 方法里的 a 的值是 9 ; b 的值是 6
交换结束后, 变量 a 的值依然是 6; 变量 b 的值依然是 9
可以得出以下结论:
main() 方法里的变量 a 和 b
并不是 swap() 方法里的 a 和 b
正如前面所说, swap() 方法里的 a 和 b 只是 main() 方法里的变量 a 和 b 的复制品.
下面我画个图演示一下其执行过程.

Java程序总是从 main() 方法开始执行, main()方法开始定义了 a / b 两个局部变量.
两个变量在内存中的存储示意图如下图所示.

这里写图片描述

当程序执行 swap() 方法时, 系统进入 swap()方法, 并将 main() 方法中的 a / b 变量作为参数值传入 swap() 方法.
传入 swap() 方法的只是 a / b 的副本, 而不是 a / b 本身.
进入 swap() 方法后系统产生了 4 个变量, 这 4 个变量在内存中的存储示意图如下:

这里写图片描述

系统分别为 main() 方法 和 swap() 方法分配了两块栈区.
用于保存 main() 方法 和 swap() 方法的局部变量.
main() 方法中的 a / b 变量作为参数值传入 swap() 方法.
实际上是在 swap() 方法的栈区中 重新产生了两个变量 a / b.
并将 main() 方法的栈区中 a / b 变量的值分别赋给 swap() 方法的栈区中的 a / b 参数.
此时, 系统存在两个 a 变量 / 两个 b 变量.
只是存放在不同方法的不同栈区中.

程序在 swap() 方法中交换 a / b 两个变量的值.
交换结束后, 我们看到 a 的值是 9 / b 的值是 6
此时内存中的存储示意图如下图所示:

这里写图片描述

我们可以看到, 在所有示意图中, main() 方法栈区中的 a / b 的值并未有任何改变.
程序改变的只是 swap() 方法栈区中的 a 和 b.
这就是值传递的实质:
当系统开始执行方法时, 系统为形参执行初始化.
就是把实参变量的值赋给方法的形参变量.
方法里操作的并不是实际的实参变量.

上面你看到的是 基本类型的参数传递.
Java对于引用类型的参数传递, 一样采用的是值传递方式.
但许多小学生可能对引用类型的参数传递产生误会.
下面程序示范了引用类型的参数传递效果.

class DataWrap
{
  int a;
  int b;
}

public class ReferenceTransferTest
{
  public static void swap(DataWrap dw)
  {
    //下面三行代码实现 dw 的 a / b 两个成员变量的值交换
    //定义一个临时变量来保存 dw 对象的 a 成员变量的值
    int 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)
  {
    DataWrap dw = new DataWrap();
    dw.a = 6;
    dw.b = 9;
    swap(dw);
    System.out.println("交换结束后, a 成员变量的值是:" + dw.a + "; b 成员变量的值是:" + dw.b);
  }
}

从运行结果来看, 在 swap() 方法里, a / b 两个成员变量的值被交换成功.
不仅如此, 当 swap() 方法执行结束后, main() 方法里 a / b 两个成员变量的值也被交换了.
这很容易造成一种错觉:
调用 swap() 方法时, 传入 swap() 方法的就是 dw 对象本身, 而不是它的复制品.
但!!! 这仅仅是一种错觉.
下面还是结合示意图来说明程序的执行过程.

程序从 main() 方法开始执行, main() 方法开始创建了一个 DataWrap 对象,
并定义了一个 dw 引用变量来指向 DataWrap 对象, 这是一个与基本类型不同的地方.
创建一个对象时, 系统内存中由两个东西:
堆内存中保存了对象本身.
栈内存中保存了引用该对象的引用变量.
接着程序通过引用来操作 DataWrap 对象.
把该对象的 a / b 两个成员变量分别赋值为 6 和 9
此时系统内存中的存储示意图如下:

这里写图片描述

接下来, main() 方法中开始调用 swap() 方法.
系统会分别为 main() 和 swap() 开辟出两个栈区.
用于存放 main() 和 swap() 方法的局部变量.
调用 swap() 方法时, dw 变量作为实参传入 swap() 方法.
同样采用的是值传递方式:
把 main() 方法里的 dw 变量的值 赋给 swap() 方法里的 dw 形参.
从而完成 swap() 方法的 dw 形参的初始化.
值得注意的是, main() 方法中的 dw 是一个引用.
它保存了 DataWrap 对象的地址.
当把 dw 的值赋给 swap() 方法的 dw 形参后.
即让 swap() 方法的 dw 形参也保存这个地址值.
即也会引用到堆内存中的DataWrap对象.
下图演示 dw 传入 swap() 方法后的存储示意图:

这里写图片描述

可以由图看出, 这种参数传递方式 是不折不扣 的值传递方式.
系统一样复制了 dw 的副本传入 swap() 方法.
但关键在于 dw 只是一个引用变量, 所以系统复制了 dw 变量, 但并未复制 DataWrap 对象.

当程序在 swap() 方法中操作 dw 形参时, 由于 dw 只是一个引用变量.
所以实际操作的还是堆内存中的DataWrap对象.
此时, 不管是操作 main() 方法里的 dw() 变量, 还是操作 swap() 方法里的 dw() 参数.
其实都是操作它们共同引用的 DataWrap 对象.
因为它们操作的都是同一个对象
所以在 swap() 方法中交换 dw 参数所引用的 DataWrap 对象的 a / b 两个成员变量的值后.
可以看到 main() 方法中 dw 变量所引用的 DataWrap 对象的 a / b 两个成员变量的值也被交换了.

为了更好的证明 main() 方法中的 dw 和 swap() 方法中的 dw 是两个变量.
在 swap() 方法的最后一行增加如下代码:

//把 dw 赋值为 null , 让它不再指向任何有效地址
dw = null;

执行上面代码的结果是 swap() 方法中的 dw 变量不再指向任何有效内存.
程序其它地方不做任何修改.
main() 方法调用了 swap() 方法后, 再次访问 dw 变量的 a / b 两个成员变量, 依然可以输出 9 和 6.
可见 main() 方法中的 dw 变量没有受到任何影响.
实际上, 当 swap() 方法中增加 dw=null 代码后, 内存中的存储示意图如下:

这里写图片描述

从上图可以看到, 把 swap() 方法中的 dw 赋值为 null 后.
swap() 方法中 失去了 DataWrap 的引用, 不可以再访问堆内存中的 DataWrap 对象.
但 main() 方法中的 dw 变量不受任何影响, 依然引用着 DataWrap 对象.
所以依然可以输出 DataWrap 对象的 a / b 成员变量的值.

形参个数可变的方法

Java允许定义形参个数可变的参数.
从而允许为方法指定数量不确定的形参.
如果在定义方法时, 在最后一个形参的类型后增加三个点 …
则表明该形参可以接受多个参数值.
多个参数值将被当成数组传入.
下面我定义一个形参个数可变的方法:


public class Varargs
{
  //定义了形参个数可变的方法
  public static void test(int a, String... books)
  {
    //boos 被当成数组处理
    for(String tmp : books)
    {
      System.out.println(tmp);
    }
    //输出整数变量 a 的值
    System.out.println(a);
  }
  public static void main(String[] args)
  {
    //调用 test 方法
    test(5, "我是谁?", "我是比克大魔王", "哈哈哈!", "你真逗比!");
  }
}

从上面程序的运行结果可以看出.
调用 test() 方法时, books 参数可以传入多个字符串作为参数值.
从 test() 的方法体代码来看, 形参个数可变的参数本质就是一个数组参数.
小提示: 下面两个写法效果是一样的.

//以可变个数形参来定义方法
public static void test(int a, String... books);
//采用数组形参来定义方法
public static void test(int a, String[] books);

这两种形式都包含了一个名为 books 的数组形参.
不过在使用它们时有区别, 对于以可变形参的形式定义的方法.
调用时写法更加简洁.如下面代码所示:

test(5, "我是谁?", "我是比克大魔王", "哈哈哈!", "你真逗比!");

但如果采用数组参数来定义方法的话, 就要这样写:

test(5, new String[]{"我是谁?", "我是比克大魔王", "哈哈哈!", "你真逗比!"});

对比之后, 明显第一种形式更加简洁.
实际上, 即使你采用 形参个数可变的形式来定义方法, 调用该方法时也一样可以为其传入一个数组.

最后需要指出的是:
数组形式的形参可以处于形参列表的任意位置.
个数可变的形参只能只能处于形参列表的最后.
也就是说, 一个方法中最多只能有一个长度可变的形参.
知道真相的你是不是眼泪掉下来, 看来不是所有情景都适合使用更简洁的 长度可变的形参.

递归方法

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

例如有如下数学题.
已知有一个数列: (数列是什么鬼?)
f(0) = 1, f(1) = 4, f(n+2) = 2 * f (n+1) + f(n)
其中 n 是大于 0 的整数, 求 f(10) 的值.
这个问题可以用 递归 来求得.
下面程序将定义一个 fn 方法, 用于计算 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));
  }
}

对于 fn(10) 而言.
即等于 2 * fn(9) + fn(8), 其中fn(9)又等于 2 * fn(8) + fn(7) …… 以此类推.
最终会计算到 fn(2) 等于 2 * fn(1) + fn(0) 即 fn(2) 是可计算的.
然后一路反算回去, 就可以最终得到 fn(10) 的值.

仔细看上面递归的过程, 当一个方法不断地调用它本身时, 必需在某个时刻方法的返回值是确定的.
即不再调用它本身, 否则这种递归就变成了无穷递归, 类似与死循环.
因此定义递归方法时有一条最重要的规则: 递归一定要向已知方向递归.

递归是非常有用的.
例如希望遍历某个路径下的所有文件.
但这个路径下文件夹的深度是未知的.
那么就可以使用递归来实现这个需求.
系统可以定义一个方法, 该方法接受一个文件路径作为参数.
该方法可遍历当前路径下的所有文件和文件路径.
该方法中再次调用该方法本身来处理该路径下的所有文件路径.

总之, 只要一个方法的方法体实现中再次调用了方法本身, 就是递归方法.
递归一定要向已知方向递归.

方法重载

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

在 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)
  {
    Overload ol = new Overload();
    //调用 test() 时没有传入参数,因此系统调用上面没有参数的 test() 方法
    ol.test();
    //调用 test() 时传入了一个字符串参数
    //因此系统调用上面那个待字符串参数的 test() 方法
    ol.test("hello");
  }
}

你看,系统是不是很聪明.
根据结果我们可知, 虽然两个 test() 方法的方法名相同, 但因为它们的形参列表不同.
所以系统可以正常区分出这两个方法.

猜你喜欢

转载自blog.csdn.net/tmdlife/article/details/51935325