欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java的多态性

程序员文章站 2022-04-21 23:49:29
...

多态性概述

多态是同一个行为具有多个不同表现形式或形态的能力。是同一个接口,使用不同的实例而执行不同操作,如图所示:

Java的多态性

 

多态的优点:

1)消除类型之间的耦合关系      2)可替换性                      3)可扩充性

4)接口性                                   5)灵活性                          6)简化性

多态的缺点:

当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类中具备的方法,不可以访问子类中特有的方法。(前期不能使用后期产生的功能,即访问的局限性)

 

多态存在的三个必要条件:

1)必须要有关系(比如继承、实现)       2)重写(覆盖操作)     

3)向上转型(父类引用指向子类对象 // 例如Animal a = new Cat();

 

多态性,是面向对象中最重要的概念,在java中有两种体现(表现形式):

1)方法的重载(overload)和重写(overwrite)。

2)对象的多态性——可以直接应用在抽象类和接口上。

 

多态的分类

多态一般分为两种:重写式多态和重载式多态。

重载式多态,也叫编译时多态。也就是说这种多态再编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。

但是这里是有歧义的,有的人觉得不应该把重载也算作多态。因为很多人对多态的理解是:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,这种情况叫做多态。 这个定义中描述的就是我们的第二种多态—重写式多态。并且,重载式多态并不是面向对象编程特有的,而多态却是面向对象三大特性之一。我觉得大家也没有必要在定义上去深究这些,同一个行为具有多个不同表现形式或形态的能力就是多态,所以重载也是一种多态。

重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。 这种多态通过函数的重写以及向上转型来实现。我们接下来讲的所有多态都是重写式多态,因为它才是面向对象编程中真正的多态。

 

对象的多态

Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。若编译时类型和运行时类型不一致,就出现多态(Polymorphism)。

在Java中,子类的对象可以替代父类的对象使用:

一个变量只能有一种确定的数据类型。

一个引用类型变量可能指向(引用)多种不同类型的对象。

子类可看做是特殊的父类,所以父类类型的引用可以指向子类的对象:向上转型(upcasting)

Person p = new Student();
Object o = new Person(); // Object类型的变量o,指向Person类型的对象
o = new Student(); // Object类型的变量o,指向Student类型的对象

一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误

Student s = new Student();
s.school = “zwj”; // 合法,Student类有school成员变量
Person p = new Student();
p.school = “zwj”; // 非法,Person类没有school成员变量

 

向上转型

子类引用的对象转换为父类类型称为向上转型。通俗地说就是是将子类对象转为父类对象。此处父类对象可以是接口。

// 以下类所在包 com.zwj.animal;
public class Animal {
    public void eat() {
        System.out.println("animal eatting...");
    }
}
public class Cat extends Animal {
    public void eat() { 
        System.out.println("我吃鱼");
    }
} 
public class Dog extends Animal { 
    public void eat() { 
        System.out.println("我吃骨头");
    } 
    public void run() {
        System.out.println("我会跑");
    }
} 
public class Main { 
    public static void main(String[] args) { 
        Animal animal = new Cat(); // 向上转型,将子类对象 Cat 转化为父类对象 Animal
        animal.eat();  // 这个时候 animal 这个引用调用的方法是子类Cat的eat()方法。
        animal = new Dog(); 
        animal.eat();
    } 
}

向上转型过程中需要注意的问题:

1)向上转型时,子类单独定义的方法会丢失。比如上面Dog类中定义的run方法,当animal引用指向Dog类实例时是访问不到run方法的,animal.run()会报错。

2)子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。

向上转型的好处

1)减少重复代码,使代码变得简洁。

2)提高系统扩展性。

 

向下转型

与向上转型相对应的就是向下转型了。向下转型是把父类对象转为子类对象。请注意!这里是有坑的。我们来看看下面这个例子就知道了:

//还是上面的animal和cat dog
Animal a = new Cat();
Cat c = ((Cat) a); 
c.eat(); //输出  我吃鱼
Dog d = ((Dog) a); 
d.eat(); 
// 报错:java.lang.ClassCastException:com.zwj.animal.Cat cannot be cast to com.zwj.animal.Dog
Animal a1 = new Animal();
Cat c1 = ((Cat) a1);
c1.eat();
// 报错:java.lang.ClassCastException:com.zwj.animal.Animal cannot be cast to com.zwj.animal.Cat

为什么第一段代码不报错呢?因为 a 本身就是 Cat 对象,所以它理所当然的可以向下转型为 Cat,也理所当然的不能转为 Dog,估计也没人见过一条狗突然就变成一只猫这种现象吧?

而 a1 为 Animal 对象,它也不能被向下转型为任何子类对象。比如我们去考古,发现了一个新生物,知道它是一种动物,但是我们不能直接说它是猫,或者说它是狗。

向下转型注意事项:

1)向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)

2)向下转型只能转型为本类对象(猫是不能变成狗的)。

 

虚拟方法的调用(Virtual Method Invocation)

正常的方法调用

Person e = new Person();
e.getInfo();
Student e = new Student();
e.getInfo();

虚拟方法调用(多态情况下)

当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。要想调用父类中被重写的方法,则必须使用关键字super。  

Person e = new Student();
// 编译时e为Person类型,而方法的调用是在运行时确定的,
// 所以调用的是Student类的getInfo()方法。——动态绑定
e.getInfo(); 

 

多态在子父类中的成员上的体现的特点:

若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。

1)成员变量:在多态中,子父类成员变量同名。

编译时期:参考的是引用型变量所属的类中是否有调用的成员。(编译时不产生对象,只检查语法错误)

运行时期:也是参考引用型变量所属的类中是否有调用的成员。

简单一句话:无论编译和运行,成员变量参考的都是引用变量所属的类中的成员变量

再说的更容易记忆一些:成员变量 --- 编译运行都看 = 左边。

2)成员方法。

编译时期:参考引用型变量所属的类中是否有调用的方法。

运行时期:参考的是对象所属的类中是否有调用的方法。调用实际对象所属的类中的重写方法。

为什么是这样的呢?因为在子父类中,对于一模一样的成员函数,有一个特性:覆盖。

简单一句:成员方法,编译看引用型变量所属的类,运行看对象所属的类。

更简单:成员方法 --- 编译看 = 左边,运行看 = 右边。

3)静态方法。

编译时期:参考的是引用型变量所属的类中是否有调用的成员。

运行时期:也是参考引用型变量所属的类中是否有调用的成员。

为什么是这样的呢?因为静态方法,其实不所属于对象,而是所属于该方法所在的类。

调用静态的方法引用是哪个类的引用调用的就是哪个类中的静态方法。

简单说:静态方法 --- 编译运行都看 = 左边。

 

instanceof操作符

如果想用子类对象的特有方法,如何判断对象是哪个具体的子类类型呢?可以通过一个关键字 instanceof ; /判断对象是否实现了指定的接口或继承了指定的类

格式:<对象 instanceof 类型> ,判断一个对象是否所属于指定的类型。

Student instanceof Person = true; // student继承了person类

x instanceof A:检验x是否为类A的对象,返回值为boolean型。

要求x所属的类与类A必须是子类和父类的关系,否则编译错误。

如果x属于类A的子类B,x instanceof  A值也为true。

 

对象类型转换

基本数据类型的Casting:

自动类型转换:小的数据类型可以自动转换成大的数据类型

          如long g=20;           double d=12.0f

强制类型转换:可以把大的数据类型强制转换(casting)成小的数据类型

          如 float f=(float)12.0;   int a=(int)1200L

注意:

1)对Java对象的强制类型转换称为造型。从父类到子类的类型转换必须通过造型(强制类型转换)实现。

2)从子类到父类的类型转换可以自动进行。

3)无继承关系的引用类型间的转换是非法的。

4)在造型前可以使用instanceof操作符测试一个对象的类型。