在 Java 编程中,经常会出现使用父类类型的引用指向子类对象的情况。这种情况下,通过父类引用可以调用子类中继承或重写父类的属性和方法,具有一定的多态性和灵活性。本文将介绍父类引用指向子类实例的相关概念、用法和特点。
子类可以继承父类中的属性和方法,并且可以重写(override)父类已有的方法。当一个子类对象创建后
假设有一个父类 Animal 和一个子类 Cat,其中 Cat 是 Animal 的子类。父类 Animal 中定义了一个属性 name 以及一个方法 eat(),而子类 Cat 中覆盖了父类的 eat() 方法。现在使用父类引用指向子类对象的方式来创建对象并调用方法:
Animal animal1 = new Cat();
animal1.name = "Tom";
animal1.eat();
上面代码中使用了Animal 类型的引用 animal1 来指向一个 Cat 类型的对象。通过 animal1 可以调用 Cat 类中继承或重写了父类的属性和方法,例如设置 name 属性为 “Tom”,并调用 eat() 方法输出 “Cat is eating.”。
当父类引用指向子类对象时,父类引用指向了整个子类对象。这包括子类对象中的继承自父类的部分以及子类特有的部分。子类在调用构造函数创建对象时,会先调用父类的构造函数,以便初始化继承自父类的成员。然后再执行子类自己的构造函数,完成对子类特有成员的初始化。
这里需要注意的是,父类引用指向子类对象后,通过该引用只能访问到父类的成员和子类重写或覆盖的父类成员,而无法直接访问子类特有的成员(如果有)。因为在编译时,编译器只知道该引用的类型是父类,所以只能看到父类中定义的成员。
多态就是使得同一个行为具有多个不同表现形式或形态的能力。
举个生活中的例子:对于 打印机的“打印” 行为,使用彩色打印机 “打印” 出来的效果就是彩色的,而使用黑白打印机 “打印” 出来的效果就是黑白的。那么 “打印” 这个行为就是多态的,彩色打印效果和黑白打印效果就是 “打印” 这个行为的两个不同的表现形式。
当使用父类引用指向子类实例时,可以实现多态。
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗在汪汪叫");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("猫在喵喵叫");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // 输出: 狗在汪汪叫
animal2.makeSound(); // 输出: 猫在喵喵叫
}
}
通过将父类引用指向子类对象,可以通过统一的方式调用makeSound()方法。当animal1引用指向Dog对象时,调用makeSound()方法时会输出"狗在汪汪叫";当animal2引用指向Cat对象时,调用makeSound()方法时会输出"猫在喵喵叫"。这里就体现了多态
多态性是基于 Java 的面向对象编程特性之一,它的核心概念包括多态、继承和重写。在父类引用指向子类实现中,由于子类对象被赋值给了父类类型的引用,从而使得对这个对象的操作具有多态性。
当然多态存在也是有条件的:
1、继承关系。(没有继承就不要谈多态)
2、子类要重写父类的方法。(不重写方法,子类就没有存在的必要)
3、父类引用指向子类对象。(今天写的重点)
多态到底是如何发生的?如上面那种图所示编译器是如何知道父类 Shape 引用指向的是 Circle 而不是 Triangle 或 Square 呢?
先来理解一下静态绑定和动态绑定。什么是绑定?将一个方法调用同一个方法主体(也就是方法的具体实现)关联起来的过程称作绑定。
上图中Shape 即父类,引用类型在编译期可知,不会被改变,而 Circle 作为实例对象类型在运行期才可知,可能会发生变化。所以如果使用前期绑定,在运行之前,编译器只知道有一个 Shape 引用,它无法得知究竟会调用哪个方法。只有在运行时才根据对象的类型自动的进行绑定,所以动态绑定也称运行时绑定。
另外Java 中除了 static和 final方法(private方法属于 final方法)之外,其他所有方法都是动态绑定。这意味着通常情况下,我们不需要判断动态绑定是否会发生,它是自动发生的。动态绑定是多态的基础。下面细说一下动态绑定
在访问一个对象的方法时,如果该方法是一个普通方法,那么编译器会根据引用类型来决定要调用哪个方法,而不是根据实际对象类型。
但是,在访问一个被重写的方法时,其行为就发生了变化,此时编译器将会在运行时确定所要调用的方法。
动态绑定使得代码与具体对象解耦,允许在不修改现有代码的情况下添加新的子类(通过反射运行时决定实例化哪个子类),从而实现更好的代码复用和维护性。
因此,只有使用父类引用指向子类对象的情况下,才能够实现多态性和动态绑定的特性。
回到上面的代码,Animal 类定义了一个 makeSound() 方法,Dog 和 Cat 类继承自 Animal 并重写了该方法。在 main 方法中,通过将父类引用指向子类对象,即 Animal animal1 = new Dog(); 和 Animal animal2 = new Cat();,然后调用 makeSound() 方法。在运行时,根据对象的实际类型,会动态地调用相应的重写方法。
在使用父类引用指向子类实例时,需要注意以下几点:
变量属性和静态方法没有多态性:变量属性和静态方法不受动态绑定机制的影响,因此无法通过父类引用调用子类中重写或隐藏的变量属性和静态方法。
可以强制类型转换:如果我们需要调用子类特有的属性或方法,可以通过强制类型转换将父类引用转换成子类引用,然后再调用子类特有的属性和方法。但是,强制类型转换可能会导致类型转换异常(ClassCastException),因此需要谨慎使用。
构造函数不能被继承和重写:当创建子类对象时,必须先调用父类的构造函数进行初始化。如果使用父类引用指向子类对象,那么只能通过子类构造函数来完成对象的初始化过程,因为构造函数不能被继承或重写。
多态性:父类引用指向子类实现是多态性的一种表现。多态性能够使代码具有更高的灵活性和可扩展性,可以以统一的方式处理不同类型的对象。
统一的接口和规范:通过父类引用指向子类实现,可以定义统一的接口和规范,使得代码更加清晰和易于理解。由于父类和子类之间存在继承关系,父类引用可以作为通用的参数类型传递和操作,从而增强了代码的可读性和可维护性。
代码复用和扩展:父类引用指向子类实现可以促进代码的复用和扩展。通过定义父类,可以将共性的属性和方法提取到父类中,子类通过继承父类来获得这些共性的特征。当需要新增一种子类时,只需编写该子类独有的属性和方法,并且可以使用父类引用来操作新的子类对象,无需修改已有的代码。
适应框架设计和面向接口编程:在许多框架和设计模式中,常常使用父类引用指向子类实现的方式来实现插件式开发和面向接口编程。通过定义父类接口,不同的子类实现该接口,并且可以使用父类引用来操作具体的子类对象,从而实现了松耦合的设计和可插拔的架构。
依赖注入和反转控制:在一些框架和容器中,常常使用父类引用指向子类实现来进行依赖注入和反转控制。通过将具体的子类对象注入到父类引用中,实现了对象间的解耦,提高了代码的可测试性和可维护性。
在Java语言中,通过将父类引用指向子类对象,可以实现多态性,这种实现方式使得代码具有更好的可扩展性和维护性,同时也符合面向对象编程的设计原则。