重走Java基础:面向对象-多态及其常见问题

一、多态概述

多态是继封装、继承之后,面向对象的第三大特性。

现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。

Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。

Java中多态的代码体现在一个子类对象(实现类对象)既可以给这个子类(实现类对象)引用变量赋值,又可以给这个子类(实现类对象)的父类(接口)变量赋值。

如Student类可以为Person类的子类。那么一个Student对象既可以赋值给一个Student类型的引用,也可以赋值给一个Person类型的引用。

最终多态体现为父类引用变量可以指向子类对象。

注意:

1. 多态的前提是必须有子父类关系或者类实现接口关系,否则无法完成多态。
2. 在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法。

二、多态的三种定义和使用格式

通用格式:

父类类型  变量名 = new 子类类型();  		//定义格式
变量名.方法名();  						//使用格式

1. 普通类多态定义的格式

父类 变量名 = new 子类();
如:	class Fu {}
	class Zi extends Fu {}
	//类的多态使用
Fu f = new Zi();

2. 抽象类多态定义的格式

抽象类 变量名 = new 抽象类子类();  
如:	abstract class Fu {
         public abstract void method();
	     }
class Zi extends Fu {
public void method(){
		      System.out.println(“重写父类抽象方法”);
}
}
//类的多态使用
Fu fu= new Zi();

3. 接口多态定义的格式

接口 变量名 = new 接口实现类();
如: interface Fu {
		     public abstract void method();
}
class Zi implements Fu {
		     public void method(){
              System.out.println(“重写接口抽象方法”);
}
}
//接口的多态使用
Fu fu = new Zi();

注意事项

同一个父类的方法会被不同的子类重写。在调用方法时,调用的为各个子类重写后的方法。

如 Person p1 = new Student();
   Person p2 = new Teacher();
   p1.work(); //p1会调用Student类中重写的work方法
   p2.work(); //p2会调用Teacher类中重写的work方法

因此,当变量名指向不同的子类对象时,由于每个子类重写父类方法的内容不同,所以会调用不同的方法。

三、多态——成员特征(方法和变量调用特征)

1. 成员变量的调用特征

当进行多态的定义后,那么多态出现后类的成员有啥变化呢?我们知道继承时,子父类之间成员变量有了自己的特定变化,那么当多态出现后,成员变量在使用上有没有变化呢?

首先我们看成员变量的变化:

看如下代码:思考下输出结果为什么?

class Fu {
	int num = 4;
}
class Zi extends Fu {
	int num = 5;
}
class Demo {
	public static void main(String[] args) 	{
		Fu f = new Zi();
		System.out.println(f.num);
		Zi z = new Zi();
		System.out.println(z.num);
	}
}

输出结果为:

4    //调用的是父类中的成员变量
5    //这里不属于多态调用,因此输出该类的成员变量

我们在看以下代码,注意代码变化:思考下代码输出结果是什么?

class Fu {
	//int num = 4;    -----------我把这行代码注释掉
}
class Zi extends Fu {
	int num = 5;
}
class Demo {
	public static void main(String[] args) 	{
		Fu f = new Zi();
		System.out.println(f.num);   // ---------f.num无法通过编译
		Zi z = new Zi();
		System.out.println(z.num);
	}
}

其实以上代码无法编译通过,大家可以试试。我这就不截图了。

重点:

因此,我们可以总结以下规律:

多态调用成员变量时:

  • 编译时期:参考的是引用类型变量所属的类中是否有该被调用的成员变量。有,则通过编译,进入运行时期;没有,编译失败。
  • 运行时期:直接调用引用类型变量所属的类中的成员变量。

简单记:编译和运行都参考等号的左边。编译运行看左边。即看父类的脸色行事,父类有该变量,直接调用父类成员变量,父类无,编译无法通过。多态的调用跟子类有无该成员无关。

2. 成员方法的调用特征

多态调用时,同调用成员变量相似,多态出现后会导致子父类中的成员方法也会有微弱的变化。

看如下代码:思考输出结果?

class Fu {                                   //定义一个父类
	int num = 4;
	void show()	{                  //定义一个父类成员方法
		System.out.println("Fu show num");
	}
}
class Zi extends Fu {              //定义一个子类,子类继承父类
	int num = 5;
	void show()	{             //子类重写父类的show方法
		System.out.println("Zi show num");
	}
}
class Demo {                       //测试类
	public static void main(String[] args) 	{
		Fu f = new Zi();
		f.show();            //思考输出结果?
	}
}

输出结果为:

Zi show num                   //结果执行的是子类的重写方法

继续看如下代码:注意代码的变化!

class Fu {
	int num = 4;
	/*
	void show()	{
		System.out.println("Fu show num");
	}
	*/
}
class Zi extends Fu {
	int num = 5;
	void show()	{
		System.out.println("Zi show num");
	}
}
class Demo {
	public static void main(String[] args) 	{
		Fu f = new Zi();
		f.show();     //这里会编译失败:会显示Fu类没有show方法,编译无法通过
	}
}

**输出结果是什么呢?**同样,编译失败!大家自己试试。

重点:

因此,我们可以总结以下规律:

多态调用成员方法时:

  • 编译时期:参考引用变量所属的类,如果没有类中没有调用的方法,编译失败。如果有则进入运行时期。
  • 运行时期:对于非静态方法,则参考引用变量所指的对象所属的类,并运行对象所属类中的成员方法,如果对象所属类中没有重写父类方法,则运行父类中的被调用方法但是如果调用的是静态方法,则运行引用变量所属类(父类)中的静态方法,跟子类无关。

简单记:编译看左边,运行看右边。即如果父类中有该非静态成员方法,并且

1. 子类重写了父类该成员方法,则直接调用子类中的成员方法;
2. 子类没有重写父类的该成员方法,则直接调用父类中的成员方法。

;如果父类中没有该非静态成员方法,即使子类中含有该成员方法,编译一样无法通过!如果调用的是静态方法,那么直接调用父类中的今天方法,跟子类无关。

多态调用的成员特征总结

  1. 成员变量:
  • 编译的时候, 参考父类中有没有这个变量,如果有,编译成功,没有编译失败
  • 运行的时候, 运行的是父类中的变量值
  • 编译运行全看父类
  1. 成员方法:
  • 编译的时候, 参考父类中有没有这个方法,如果有,编译成功,没有编译失败
  • 运行的时候, 运行时看方法类型。如果是非静态方法,运行的是子类的重写方法;如果是静态方法,运行的是父类中的静态方法,跟子类无关。
  • 编译看父类,运行看调用的方法类型

四、多态的类型辨别问题( instanceof关键字)

多态产生的问题:引用类型的实际对象类型的疑惑问题

看如下代码:思考!

public abstract class Person {      //这里定义一个抽象Person父类
	public abstract void sleep();
}
public class Student extends Person {   //这里定义一个Student的子类
	public void sleep(){
		System.out.println("学生在休息睡觉");
	}
	public void study(){
	}
}
public class Teacher extends Person{    //这里定义第二个子类Teacher子类
	public void sleep(){
		System.out.println("老师在休息");
	}
}

接下来我们看这样的一个测试类:

public class Test {
	public static void main(String[] args) {
		Person p1 = new Student();
		Person p2 = new Teacher();
	}
}

思考一下:Person p1 = new Student();Person p2 = new Teacher(); 由于多态的产生,我们如何分辨这两个引用变量的所引用的实际对象类型,因此Java中利用instanceof关键字来判断引用类型指向的对象的实际类型。
那么我们可以利用关键字来判断对象的实际类型,然后调用它们的对应的方法

public class Test {
	public static void main(String[] args) {
		Person p1 = new Student();
		Person p = new Teacher();
		
		if(boolean b = p instanceof Student){
			System.out.println(b);
			p.sleep();
		}
		if(boolean b = p instanceof Teacher){
			System.out.println(b);
			p.sleep();
		}
	}
}

五、多态——转型问题

实际上多态定义过程中,涉及了一个数据转型的问题。我们知道多态的定义格式为一个父类变量的引用指向子类的对象来完成,既父类类型 变量名 = new 子类类型();,这里就同基础数据类型一样,涉及了数据转型问题。

多态的转型分为向上转型和向下转型两种

1. 向上转型:当有子类对象赋值给一个父类引用时,便是向上转型,多态本身就是向上转型的过程。

  • 使用格式:
父类类型  变量名 = new 子类类型();
如:Person p = new Student();

向上转型我们很容易理解,接下来我们讲另一种转型。

2. 向下转型

背景:为什么需要向下转型?认真看!

我们前面说过多态调用成员方法和变量。当调用成员方法时,如果父类中含有该成员方法,且该方法被子类重写后,那么调用的是子类重写的方法,而如果父类中没有该方法时,则无法编译。对于调用成员变量,无论编译还是运行都是根据父类的成员变量来编译和运行,跟子类无关。

我们考虑这种情况:如果我们需要调用子类的自身的方法或成员变量那该怎么办呢,调用的方法或成员变量由子类独有?我们应该怎么实现呢?
如以下代码:

public abstract class Person {      //这里定义一个抽象Person父类
	int i =10 ;
	public abstract void sleep();
}
public class Student extends Person {   //这里定义一个Student的子类
	String name = "凝练于心";
	public void sleep(){
		System.out.println("学生在休息睡觉");
	}
	public void study(){	
		System.out.println("学生在学习java");
	}
}

我们如何利用多态调用子类study()方法呢?
我们接下来看以下代码:

public class Test {
	public static void main(String[] args) {
		Person p = new Student();
		p.sleep();  //这里调用的是子类Student的sleep()方法
		//p.study();  注意这里如果这样调用是无法通过编译的!
		//这里就需要利用向下转型了
		Student s = (Student) p;
		p.study(); //这里就可以调用了
		p.name;
	}
}

定义:一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用转为子类引用,这个过程是向下转型。如果是直接创建父类对象,是无法向下转型的!

  • 使用格式:
    子类类型 变量名 = (子类类型) 父类类型的变量;
    如:Student stu = (Student) p;  //变量p 实际上指向Student对象

总结 :向下转型可以用于调用子类自有的成员变量和方法!

多态转型的通俗理解(孔子装爹),好好理解下!

我们定义两个类:一个是孔子爹这个类;一个为孔子类

public class  孔子爹 {
	int 年龄 =  80岁;
	public void 杀猪(){
	System.out.println("孔子爹是个杀猪的,会杀猪")
	}
} 
public class  孔子{
	int 年龄 = 48岁;
	public void 教论语()
	System.out.println("孔子是个老师,会教论语")
	}
} 

假设有一天孔子的邻居请孔子爹去杀猪,但孔子爹出差去了不在家,由于是之前答应好的,古人讲究诚信,因此孔子就假装他爹去帮他邻居杀猪,但是他怕他邻居认出来,他就化了个装,去帮他邻居杀猪。杀猪完后他就卸妆去学院给他的学生讲课。接下来我们就用多态的过程来实现孔子的一天的工作!

public class 孔子的一天{
	//孔子要去杀猪了
	//开始化妆 :
	 孔子爹  fakeDade = new 孔子();  //这里利用向上转型,来装爹
	//成爹的工作: 
	 fakeDade.杀猪();//帮邻居杀猪
	//孔子要去教学了
	//卸妆    :
	  trueSon = (孔子)fakeDade; //这里利用向下转型,恢复模样
	//成自己工作
	 trueSon.教论语(); //给学生上课,调用子类孔子自己的方法

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