多态
1.多态的概念
- 多态是方法或对象具有多种形态,是面向对象的第三大特征。
- 多态的前提是两个对象(类)存在继承关系,多态是建立在封装和继承基础之上的。
2.为什么要使用多态
-
代码重用和扩展性:
多态性使得我们可以编写通用的代码,可以适用于各种不同类型的对象。通过抽象类和接口,我们可以定义通用的方法和属性,而具体的实现可以由子类来完成。这种灵活性使得代码更容易被重用和扩展,同时也降低了代码的耦合度。 -
简化代码和逻辑:
多态性使得我们可以通过统一的接口来处理不同类型的对象,从而简化了代码和逻辑。我们不需要为每种具体类型都编写一套独立的逻辑,而是可以通过一个统一的方法来处理所有类型的对象。这种简化的代码结构更易于理解和维护。 -
更易于测试和调试:
由于多态性使得代码更加模块化和可复用,因此更容易进行单元测试和调试。我们可以针对接口或抽象类编写通用的测试用例,而不需要为每个具体的实现编写独立的测试代码。这样可以提高测试效率,降低测试成本。 -
扩展性和灵活性:
多态性使得程序具有更强的扩展性和灵活性。当需要添加新的功能或修改现有功能时,我们可以通过添加新的子类或修改现有子类来实现,而不需要修改现有的代码。这种低耦合度的设计使得程序更容易适应变化,并且更容易进行维护和升级。 -
可替换性:
多态性允许我们将父类的引用指向子类的对象,从而实现了对象的替换。这种特性使得程序更具灵活性,可以在运行时动态地替换对象的实现,而不需要修改代码。这种可替换性使得程序更容易适应不同的需求和环境。
3.多态的分类
1.1 编译时多态(静态绑定)
编译时多态指的是方法重载(Method Overloading),即同一个类中可以有多个同名的方法,但它们的参数列表不同。编译器会在编译时根据方法的参数列表决定调用哪个方法。
public class OverloadExample {
public void display(int a) {
System.out.println("Argument: " + a);
}
public void display(String a) {
System.out.println("Argument: " + a);
}
public static void main(String[] args) {
OverloadExample obj = new OverloadExample();
obj.display(10); // 输出:Argument: 10
obj.display("Hello"); // 输出:Argument: Hello
}
}
1.2 运行时多态(动态绑定)
运行时多态指的是方法重写(Method Overriding),即子类可以重写父类的方法。在运行时,JVM根据对象的实际类型调用相应的方法。
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog();
Animal myCat = new Cat();
myAnimal.sound(); // 输出:Animal makes a sound
myDog.sound(); // 输出:Dog barks
myCat.sound(); // 输出:Cat meows
}
}
4.多态的机制原理
RTTI
多态实现的技术基础是 RTTI,即 Run-Time Type Identification(运行时类型判定),它的作用是在我们不知道某个对象的确切的类型信息时(即某个对象是哪个类的实例),可以通过 RTTI 相关的机制帮助我们在编译时获取对象的类型信息。
而 RTTI 的功能主要是通过 Class 类文件实现的,更精确一点来说,是通过 Class 类文件的方法表实现的。
这里提到的 Class 类可以理解为是 “类的类”(class of classes)。如果说类是对象的抽象的话,那么 Class 类就是对类的抽象。而每个类都有一个 Class 对象,每当编写并且编译成功一个新的类,就会生成一个对应的 Class 对象,被保存在一个与类同名的 .class 文件中。
详细一点来说,就是 Java 源码编译器将 Java 文件编译成 .class 文件,然后通过类装载器将 .class 文件装载到 JVM 中,并在内部建立该类的类型信息(.class 文件在 JVM 中存储的一种数据结构),最后通过执行引擎来执行。
方法表是实现多态的关键所在,里面保存的是实例方法的引用,且是直接引用。Java 虚拟机在执行程序时,就是通过方法表来确定运行哪一个多态方法的。
多态方法调用
在调用方法时,首先需要完成实例方法的符号引用解析,也就是将符号引用解析为方法表的偏移量。
虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法;
此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中根据偏移量获取该方法名对应的指针,进而就能指向实际类的方法了。
上面我们讨论的仅是利用继承实现多态的内部机制,多态的另外一种实现方式:接口实现相比而言会更加复杂。原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。
所以在 JVM 中,多态的实例方法调用实际上有两种指令:
invokevirtual 指令:用于调用声明为类的方法;
invokeinterface 指令:用于调用声明为接口的方法。
当使用 invokeinterface 指令调用方法时,就不能采用固定偏移量的办法了。实际上,Java 虚拟机对于接口方法的调用是采用搜索方法表的方式来实现的,比如,要在 Father 接口的方法表中找到 dealHouse() 方法,必须搜索 Father 的整个方法表。所以我们可以得出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的。
以上就是多态的原理,总结起来说就是两点:
方法表起了决定性作用,如果子类改写了父类的方法,那么子类和父类的同名方法共享一个方法表项,都被认作是父类的方法,因此可以写成父类引用指向子类对象的形式。
类和接口的多态实现不一样,类的方法表可以使用固定偏移,但接口需要进行搜索,原因是接口的实现不是确定唯一的,所以相对来说性能差一些。