一、多态的基本概念
多态(Polymorphism)是面向对象编程的一个重要特性。它允许一个父类(或接口)引用指向子类对象,从而让不同的子类对象在同一接口下表现出各自的行为。多态提高了代码的扩展性和可维护性。
多态的表现形式主要有以下几种:
- 方法重写(Override):子类继承父类时,可以重写父类的方法,使子类对象在调用该方法时表现出与父类不同的行为。
- 接口实现:类通过实现接口来提供接口所定义的方法,从而使不同的实现类对象在同一接口下表现出各自的行为。
- 抽象类和抽象方法:抽象类可以包含抽象方法,子类需要实现这些抽象方法,从而实现多态。
多态的前提条件:
- 必须存在继承(或实现)关系。
- 子类(或实现类)必须重写(或实现)父类(或接口)中的方法。
多态的使用场景:
- 当存在多个具有相似功能的子类时,可以通过多态来简化代码和提高扩展性。
- 当需要对一组具有相同接口的对象执行相似操作时,可以使用多态来简化代码和减少重复。
- 在设计模式中,多态被广泛应用,如策略模式、工厂模式等。
总之,多态的主要目的是提高代码的灵活性和可扩展性。通过多态,我们可以编写出通用的代码,这些代码可以处理多种类型的对象,而不需要针对每个具体类型进行编程。多态的实现依赖于继承和接口,通过方法重写和接口实现来实现不同类型对象的行为多样性。多态有助于降低代码之间的耦合度,使得代码更易于修改和扩展。
二、成员变量和成员方法的特点
在 Java 中,多态是指允许一个对象拥有多种形式。这主要通过继承和接口实现,实现多态的关键是方法重写(覆盖)和接口实现。当我们在编程时,多态的主要特点表现在对成员变量和成员方法的调用上。
-
多态调用成员变量的特点:
当子类和父类具有相同的成员变量时,多态调用成员变量时,访问的是父类的成员变量。这是因为多态中,成员变量的访问遵循编译时类型,也就是父类类型。要想访问子类的成员变量,可以通过子类类型的引用来访问。 -
多态调用成员方法的特点:
当子类重写了父类的方法时,多态调用成员方法时,会调用子类的实现。这是因为多态中,成员方法的调用遵循运行时类型,也就是子类类型。多态允许我们在运行时根据对象的实际类型来选择调用哪个方法实现,这提高了代码的灵活性。
举个例子:
class Animal {
String sound = "animal sound";
void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
String sound = "bark";
void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog();
System.out.println(myAnimal.sound); // 输出 "animal sound"
myAnimal.makeSound(); // 输出 "The dog barks"
}
}
在这个例子中,Dog 类继承了 Animal 类。虽然 Dog 类有自己的 sound 成员变量和 makeSound() 方法实现,但在多态调用时,访问的是父类的成员变量和子类的成员方法。
总结:
多态调用成员变量时,访问的是编译时类型(父类)的成员变量。
多态调用成员方法时,访问的是运行时类型(子类)的成员方法。
从内存角度来理解多态调用成员变量和方法时,我们需要关注对象在内存中的存储和分配,以及方法调用的过程。
-
多态调用成员变量:
在 Java 中,对象的成员变量存储在堆内存中。当子类继承父类时,子类对象会包含父类的成员变量。这意味着子类对象在内存中会有一个连续的空间存储父类和子类的成员变量。当我们使用多态调用成员变量时,实际上是根据编译时类型(通常是父类)来访问成员变量。编译器根据编译时类型确定访问哪个成员变量,并在内存中找到相应的存储位置。因此,多态调用成员变量时,访问的是父类的成员变量。
-
多态调用成员方法:
成员方法的调用与成员变量有所不同。在 Java 中,对象的方法不是直接存储在对象内存中的,而是存储在方法区。方法区包含了一个对象的所有方法实现。为了支持多态,Java 使用了一种叫做虚方法表(Virtual Method Table,简称 VMT)的技术。虚方法表是一个在类加载时创建的、包含了类中所有实例方法的表。当子类重写父类的方法时,子类的虚方法表会将对应的父类方法替换为子类方法的实现。
当我们使用多态调用成员方法时,Java 虚拟机会根据运行时类型(子类类型)在虚方法表中查找对应的方法实现并执行。这就是为什么多态调用成员方法时,执行的是子类的实现。
总结:
多态调用成员变量时,根据编译时类型在内存中找到相应的成员变量存储位置,访问的是父类的成员变量。
多态调用成员方法时,Java 虚拟机会根据运行时类型在虚方法表中查找并执行子类的方法实现。
三、instanceof关键字
多态的优势:
-
代码可重用:多态允许我们编写一段代码,以处理父类和其所有子类的实例,从而减少重复代码。
-
扩展性:通过多态,我们可以轻松地添加新的子类,而不必修改已有的代码。这降低了代码的耦合度,并提高了扩展性。
-
灵活性:多态允许我们在运行时根据对象的实际类型选择合适的方法执行,使得代码更加灵活。
-
抽象:多态将关注点从具体的实现转向了接口,这使得我们可以更关注问题的抽象层面,而不是具体实现细节。
多态的弊端:
-
性能开销:虚方法调用需要额外的时间进行动态分派。虽然现代JVM优化了这方面的性能,但在某些性能关键的场景中,这可能是一个潜在问题。
-
可读性:多态可能使代码的可读性降低,因为要跟踪父类和子类之间的关系以及方法的重写关系。
如何解决弊端:
-
优化性能:在非关键性能场景下,多态性能开销通常可以忽略不计。在关键性能场景中,可以考虑避免使用虚方法调用,将相关代码放在非虚方法中,或使用其他编程技巧以减小性能开销。
-
提高可读性:合理地组织代码、使用清晰的命名规范、添加文档注释等方式可以提高代码的可读性。同时,保持类和方法的粒度适中,避免过度继承和过度重写也有助于提高代码的可读性。
为什么多态无法使用子类的特有方法
实际上,这是多态的一种设计特点。多态是为了实现程序的抽象性和扩展性,它鼓励我们使用父类类型作为变量类型,这样我们可以处理各种子类对象,而不用关心具体的实现细节。
当你需要使用子类的特有方法时,可以通过类型检查和类型转换来实现。例如,你可以使用instanceof关键字检查对象是否属于某个子类,然后使用类型转换(type casting)将父类引用转换为子类引用,从而访问子类的特有方法。
这种设计确实可能导致额外的类型检查和类型转换,但它符合多态的设计目标,即将关注点从具体的实现转向接口。同时,它也鼓励程序员更加关注抽象层面的设计,如何优雅地处理不同子类的行为,而不是将注意力集中在特定子类的特有方法上。
instanceof是Java中的一个关键字,用于检查某个对象是否属于特定的类或接口。instanceof的主要作用是判断一个对象是否是某个类的实例,或者是否实现了某个接口。这在处理多态的场景中非常有用,尤其是当我们需要确定对象具体属于哪个子类时。
使用场景:
- 在多态的情况下,当需要根据对象的实际类型执行不同的操作时,可以使用instanceof来判断对象的类型。
- 当处理一组对象,它们可能属于不同的子类,但是实现了相同的接口时,可以使用instanceof检查它们是否实现了某个特定的接口,以便执行相应的操作。
使用示例:
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
public void fetch() {
System.out.println("Fetching...");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog();
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal;
myDog.fetch();
} else if (myAnimal instanceof Cat) {
// Do something specific for cats
} else {
// Do something for other animals
}
}
}
在上述示例中,myAnimal是一个Animal类型的引用,指向一个Dog对象。使用instanceof关键字检查myAnimal是否是Dog类的实例。如果是,我们将myAnimal引用转换为Dog类型,并调用fetch()方法。这样,我们可以在处理多态对象时,根据它们的具体类型执行特定的操作。
四、耦合
耦合是指程序中不同部分之间的相互依赖关系。在软件开发中,通常希望降低耦合度,以提高代码的可维护性、可重用性和灵活性。耦合度越低,一个模块对其他模块的影响就越小,这意味着在修改或扩展程序时,需要改动的部分会更少。
多态有助于降低代码的耦合度,原因如下:
-
抽象:多态允许我们使用基类或接口引用来操作不同类型的对象。这样,我们可以在不知道对象具体类型的情况下编写代码,从而将代码与特定实现解耦。这使得代码更容易修改和扩展,因为我们只需关注基类或接口的行为,而不是特定子类的实现细节。
-
可扩展性:当我们需要添加新的子类时,多态使得我们无需修改已有的代码,只需确保新的子类实现了相应的基类或接口。这降低了代码间的耦合度,因为不同模块之间的依赖关系减少了。
-
代码重用:由于多态允许我们使用基类或接口引用来操作不同类型的对象,因此我们可以在不修改原有代码的情况下重用和共享代码。这降低了代码的重复性,提高了代码的可维护性。
例如,假设我们有一个处理Animal类的方法,该方法可以接受Animal及其任何子类的对象(如Dog、Cat等)。这意味着,如果我们需要添加一个新的动物类型,只需创建一个新的子类(例如Fish),无需修改原有的处理Animal的方法。这降低了代码之间的耦合度,使得程序更容易扩展和维护。
五、动态分派和虚方法表
动态分派(Dynamic Dispatch)是一种在运行时根据对象的实际类型来决定调用哪个方法的机制。在面向对象的编程语言中,如 Java,动态分派通常与多态和继承一起使用,以实现灵活的代码设计和更好的代码重用。
在 Java 中,动态分派是通过虚方法表(Virtual Method Table)实现的。虚方法表是一个在类加载时创建的数据结构,其中包含了该类对象的所有非私有方法的地址。当一个对象调用一个实例方法时,Java 虚拟机会根据对象的实际类型查找其对应的虚方法表,并根据方法名和签名找到正确的方法地址,然后执行该方法。
动态分派的一个典型例子是方法重写(Override)。假设我们有一个父类 Animal 和两个子类 Dog 和 Cat,它们都有一个叫做 makeSound() 的方法。在代码中,我们可以使用 Animal 类型的引用来指向 Dog 或 Cat 类型的对象。当我们调用 makeSound() 方法时,Java 虚拟机会根据对象的实际类型(Dog 或 Cat)在运行时动态地调用正确的 makeSound() 方法。
Animal myAnimal = new Dog();
myAnimal.makeSound(); // 调用 Dog 类的 makeSound() 方法
myAnimal = new Cat();
myAnimal.makeSound(); // 调用 Cat 类的 makeSound() 方法
动态分派是一种在运行时根据对象的实际类型动态决定调用哪个方法的机制,它使得代码更加灵活和可扩展。
虚方法表(Virtual Method Table,简称 VMT 或 vtable)是面向对象编程语言中实现动态分派(Dynamic Dispatch)的一种技术。在具有继承和多态特性的面向对象编程语言中,子类可能会覆盖(Override)父类的方法。在运行时,编译器并不总是能确定一个对象具体属于哪个类,因此需要一种机制来确定应该调用哪个类的方法。
Java、C++ 等编程语言使用虚方法表来实现这一机制。虚方法表是一个包含方法指针的表,每个类都有一个虚方法表。当一个类的方法被子类覆盖时,子类的虚方法表中对应的方法指针将指向子类的实现。当需要调用一个对象的方法时,运行时系统会根据对象的类查找虚方法表,然后调用表中对应的方法。
通过这种方式,虚方法表使得在运行时能够正确地调用对象实际类型的方法实现,从而实现动态分派和多态。需要注意的是,虚方法表在内存中占用额外的空间,对于每一个类,其虚方法表在方法区中只存在一份。
但在 Java 中,并非每个类的所有方法都会出现在虚方法表中。只有实例方法才会出现在虚方法表里,静态方法、构造方法和私有方法不会出现在虚方法表中,因为它们不涉及到动态分派。
继承关系中的虚方法表如何体现:
-
当一个类继承自另一个类时,子类会继承父类的所有实例方法(除了被声明为 final 的方法,因为 final 方法不能被覆盖)。子类的虚方法表中将包含父类的实例方法。
-
如果子类覆盖了父类的某个方法,那么子类的虚方法表中将替换原有的方法指针,指向子类的实现。
-
如果子类定义了新的实例方法,那么这些方法会被添加到子类的虚方法表中。
举个例子:
class Parent {
void foo() { System.out.println("Parent foo"); }
void bar() { System.out.println("Parent bar"); }
}
class Child extends Parent {
void foo() { System.out.println("Child foo"); }
void baz() { System.out.println("Child baz"); }
}
在这个例子中,Child 类继承了 Parent 类。Child 类覆盖了 Parent 类的 foo() 方法,并定义了一个新的实例方法 baz()。在这种情况下,虚方法表如下:
-
Parent 类的虚方法表:
foo() -> Parent.foo()
bar() -> Parent.bar() -
Child 类的虚方法表:
foo() -> Child.foo() (覆盖了父类的实现)
bar() -> Parent.bar() (继承自父类)
baz() -> Child.baz() (子类定义的新方法)
通过这种方式,虚方法表在继承关系中保持了方法的正确调用。
六、关于继承的一个问题
如果我有三个类,A类,B类,C类,三个类中都有成员变量name,然后B类继承A类,C类继承B类,在C类中能通过super关键字访问A类中的name吗?
在 C 类中,你不能直接使用 super 关键字访问 A 类中的 name 成员变量,因为 super 只能访问直接父类(即 B 类)的成员。但是,你可以通过 B 类的 super 关键字来访问 A 类中的 name 成员变量。
要实现这个功能,你需要在 B 类中创建一个方法来访问 A 类中的 name 成员变量,并在 C 类中调用该方法。下面是一个简单的例子来说明这个过程:
public class A {
protected String name = "A's name";
}
public class B extends A {
public String getNameInA() {
return super.name;
}
}
public class C extends B {
public void printNameInA() {
System.out.println("Name in A: " + super.getNameInA());
}
public static void main(String[] args) {
C c = new C();
c.printNameInA();
}
}
在这个例子中,我们为 B 类创建了一个 getNameInA() 方法,它使用 super 关键字访问 A 类中的 name 成员变量。然后,在 C 类中,我们通过调用 super.getNameInA() 来间接访问 A 类中的 name 成员变量。