Java Review (七、面向对象----方法深入)

@


方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分, 但从功能上来看,方法完 全类似于传统结构化程序设计里的函数 值得指出的是, Java里的方法不能独立存在 ,所有的方法都必须定义在类里 ,方法在逻辑上要么属于类,要么属于对象。

方法的所属性

不论是从定义方法的语法来看,还是从方法的功能来看都不难发现方法和函数之间的相似性,实际上,方法确实是由传统的函数发展而来的,方法与传统的函数有着显著不同:

  • 在结构化编程语言里,函数是一等公民,整个软件由 一个个的函数组成
  • 在面向对象编程语言里,类才是一等公民,整个系统由一个个的类组成

在 Java 方法不能独立存在,方法必须属于类或对象,如果需要定义方法,则只能在类体内定义,不能独立定义个方法。

一旦将一个方法定义在某个类的类体内:

  • 如果这个方法使用了 static修饰 ,则这个方法属于这个类,
  • 如果没有使用static修饰则这个方法属于这个类的实例。

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

方法不能独立存在,所以方法也不能像函数那样被独立执行,执行方法时必须使用类或对象来作为调用者 ,即所有方法都必须使用 类.方法对象.方法 的形式来调用 。

这里可能产生 个问题: 同一个类里不同方法之间相互调用时,不就可以直接调用吗?这里需要指出:同 一个类的一个方法调用另外 个方时,如果被调方法是普通方法, 默认使用this 作为调用者;如果被调方法是静态方法,则默认使用类作为调用者。也就是说,表面上看起来某些方法可以被独立执行,但实际上还是使用 this 或者类来作为调用者。

方法属性总结:

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

使用 static 修饰的方法属于这个类本身,使用 static 修饰的方法既可以使用类作为调用者来调用,可以使用对象作为调用者来调用。但是因为使用 static 修饰方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,这是由底层依然是使用这些实例所属的类作为调用者。没有 static 修饰的方法属于该类的对象,不属于这个类本身。因此没有 static 饰的方法只能使用对象作为调用者来调用,不能使用类作为调用者来调用 。使用不同对象作为调用者来调用同 一个普通方法,可能得到不同的结果。


方法的参数传递机制

前面己经介绍了 Java 里的方法是不能独立存在的,调用方法也必须使用类或对象作为主调者果声明方法时包含了形参声明,则调用方法时必须给这些形参指定参数值,调用方法时实际传给形参的参数值也被称为实参。

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

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


基本类型的参数传递


方法参数传递效果实例
public class PrimitiveTransferTest {

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

	}

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

	}

}

结果:

swap方法里,a的值是9; b的值是6
交换结束后,a变量 的值是6; b变量 的值是 9

从上面运行结果来看, swap()方法里 a和b的值是9、6 ,交换结束后,变量a、b的值依然是6、9 。

从这个运行结果可以看出 main()方法里的变量a和b并不是 wap()方法里的变量a和b。 swap()方法里的a和b只是main()方法里变量a和b的复制品。

实例程序的执行过程:

  • Java 程序总是从 main()方法开始执行, main()方法开始定义了a、b两个局部变量

在内存中的存储示意图如图一所示

图一:main()方法中定义了a、b变量存储示意图

在这里插入图片描述

  • 当程序执行 swap()方法时,系统进入 swap()方法,并将 main()方法中的变量a和b作为参数值传入swap()方法,传入swap()方法的只是a和b的副本,而不是a和b本身,进入 swap()方法后系统中产生了4个变量

在内存中的存储示意图如图二所示。

图二:main()方法中的变量作为参数值传入 swap()方法存储示意图

在这里插入图片描述

  • 在main()方法中调用swap()方法时,main()方法还未结束。因此,系统分别为main()方法和swap()方法分配两块栈区,用于保存main()方法和swap()方法的局部变量。main()方法中的a、b变量作为参数值传入swap()方法,实际上是在swap()方法栈区中重新产生了两个变量a、b,并将main()方法栈区中a、 b变量的值分别赋给swap()方法栈区中的a、b参数(就是对swap()方法的a、b形参进行了初始化)。 此时,系统存在两个a变量、两个b变量,只是存在于不同的方法栈区中而已。程序在swap()方法栈区中交换a、b两个变量的值, 实际上是对图三中灰色覆盖区域的a、b变量进行 交换,交换结束后swap()方法中输出a、b变量的值,看到a的值为9, b的值为6,此时内存中的存储示意图如图三所示:

图三:swap()方法中a、b交换之后的存储示意图

在这里插入图片描述
对比图三与图一两个示意图中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成员变量的值是9; b成员变量的值是6
交换结束后,a成员变量的值是9; b成员变量的值是6

从上面运行结果来看,在swap()方法里,a、b两个成员变量的值被交换成功。不仅如此,当swap() 方法执行结束后,main()方法里a、b两个成员变量的值也被交换了。这很容易造成一种错觉:调用swap() 方法时,传入swap()方法的就是dw对象本身,而不是它的复制品。

程序的执行过程:

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

此时系统内存中的存储示意图如图四所示。

图四:main()方法中创建了 DataWrap对象后存储示意图

在这里插入图片描述

  • main()方法中开始调用swap()方法,main()方法并未结束,系统会分别为main()和swap() 开辟岀两个栈区,用于存放main()和swap()方法的局部变量。调用swap()方法时,dw变量作为实参传入swap()方法,同样釆用值传递方式:把main()方法里dw变量的值赋给swap()方法里的dw形参,从 而完成swap()方法的dw形参的初始化。值得指出的是,main()方法中的dw是一个引用(也就是一个 指针),它保存了 DataWrap对象的地址值,当把dw的值赋给swap()方法的dw形参后,即让swap()方 法的dw形参也保存这个地址值,即也会引用到堆内存中的DataWrap对象。

图五显示了 dw传入swap()后的存储示意图。

图五:main()方法中的 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()方法中的dw赋值为null后,swap()方法中失去了DataWrap的引用,不可再访问堆内存中的DataWraper对象。但main()方法中的dw变量不受任何影响,依然引用DataWrap对象,所以依然可以输岀DataWrap对象的a、b成员变量的值。


可变参数

JDK 1.5 开始,Java支持传递同类型的可变参数给一个方法。

方法的可变参数的声明如下所示:


typeName... parameterName

在方法声明中,在指定参数类型后加一个省略号(...) 。

一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。


可变参数实例

public class VarargsDemo {
    public static void main(String args[]) {
        // 调用可变参数的方法
        printMax(34, 3, 3, 2, 56.5);
        printMax(new double[]{1, 2, 3});
    }
    
    //定义了形参可变的方法
    public static void printMax( double... numbers) {
        if (numbers.length == 0) {
            System.out.println("No argument passed");
            return;
        }
       //numbers被当成数组处理
        double result = numbers[0];
 
        for (int i = 1; i <  numbers.length; i++){
            if (numbers[i] >  result) {
                result = numbers[i];
            }
        }
        System.out.println("The max value is " + result);
    }
}

结果:

The max value is 56.5
The max value is 3.0

递归方法

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

例如有如下数学题 己知有 个数列:只0) = ,只 1) ,只 + 2) = 2*j n+l) + j(时
其中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));

	}
}

方法的重载

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

Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。







参考:

【1】:《疯狂Java讲义》
【2】:《Java核心技术 卷一》
【3】:https://www.runoob.com/java/java-methods.html

posted @ 2020-06-05 22:02  三分恶  阅读(230)  评论(0编辑  收藏  举报