多态
多态的概念
1.多态概念:一个事物具有多种表现形态。
2.在Java程序中,多态表现为定义一个方法,在不同环境下呈现不同的业务逻辑。
多态的具体表现
一、方法的多态
方法的重载和重写均体现多态
在Java中,方法的重载(Overloading)和重写(Overriding)确实是多态的两种体现方式,但它们各自适用的场景和实现机制有所不同。下面我将分别通过例子来说明这两种多态的表现形式。
方法的重载(Overloading)
方法重载是在一个类中定义多个同名的方法,但这些方法的参数列表必须不同(参数的个数、类型或顺序不同)。这使得在调用这些方法时,编译器能够根据传入的参数类型和数量来决定调用哪个具体的方法。
示例代码
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public double add(double a, double b, double c) {
return a + b + c;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
int sum1 = calc.add(5, 10); // 调用 int add(int a, int b)
double sum2 = calc.add(5.5, 10.1); // 调用 double add(double a, double b)
double sum3 = calc.add(5.5, 10.1, 15.2); // 调用 double add(double a, double b, double c)
System.out.println(sum1);
System.out.println(sum2);
System.out.println(sum3);
}
}
在这个例子中,Calculator
类中定义了三个 add
方法,它们通过不同的参数列表实现了方法的重载。调用时,根据传入的参数类型和数量,编译器会选择合适的方法执行。
方法的重写(Overriding)
方法重写发生在子类继承父类,并且子类提供了与父类中具有相同方法签名(即方法名、参数列表和返回类型相同)的方法的情况下。当一个子类重写了一个父类的方法时,如果子类对象通过父类引用被调用,那么实际上执行的是子类中重写的方法。
示例代码
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof woof");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // 多态:Animal 引用指向 Dog 对象
myDog.makeSound(); // 调用的是 Dog 类中的 makeSound 方法
}
}
在这个例子中,Dog
类继承了 Animal
类,并重写了 makeSound
方法。当通过 Animal
类型的引用 myDog
调用 makeSound
方法时,实际上是调用了 Dog
类中重写的方法,这就是方法重写的多态表现。
总结来说,方法重载体现了编译时的多态性,它允许在一个类中定义多个同名方法,编译器在编译时根据不同的参数列表选择适当的方法。而方法重写体现了运行时的多态性,它允许子类替换父类的方法实现,当通过父类引用调用方法时,实际上执行的是子类的方法。这两种方式都体现了多态性,但它们实现的机制和应用场景有所不同。
回顾:方法重写和方法重载的区别
方法重写(Method Overriding)和方法重载(Method Overloading)是Java中非常重要的概念,它们在程序设计中有着不同的用途。以下是这两者的主要区别:
位置
- 方法重写:发生在继承关系中,即子类中对父类的方法进行重写。只有在子类继承父类的情况下,子类才有可能重写父类的方法。
- 方法重载:发生在同一个类中,即在同一个类内定义多个同名的方法,但这些方法的参数列表不同。
方法名
- 方法重写:子类中重写的方法必须与父类中的方法具有相同的方法名。
- 方法重载:方法名必须相同,但可以通过不同的参数列表来区分不同的方法。
参数列表
- 方法重写:子类中重写的方法必须和父类中被重写的方法具有相同的参数列表。
- 方法重载:参数列表必须不同,包括参数的数量、类型或顺序。
返回值
- 方法重写:返回类型必须与父类的方法相同或为父类方法返回类型的子类型。
- 方法重载:返回类型可以不同,但仅靠返回类型不能区分重载的方法。
访问权限
- 方法重写:子类中重写的方法使用的访问权限不能小于父类中被重写的方法的访问权限。例如,如果父类的方法是
public
的,那么子类重写的方法也必须至少是public
的。 - 方法重载:方法的访问权限可以不同,没有严格的要求。
异常
- 方法重写:子类重写的方法不能抛出新的检查型异常或比父类方法所抛出的异常更广泛的异常。
- 方法重载:重载的方法可以声明不同的异常。
二、对象的多态
1.一个对象的编译类型和运行类型可以不一致
2.编译类型在定义对象时,就确定了,不能改变
3.运行类型是可以变化的
4.编译类型看定义时=号的左边,运行类型看=号的右边
在Java中,一个对象的编译类型和运行类型是两个重要的概念,它们分别决定了对象的不同方面。了解这两个概念有助于更好地理解Java中的多态性和面向对象编程的核心思想。
编译类型
编译类型指的是在声明变量时使用的类型。也就是说,当你声明一个变量时,编译器就知道了这个变量的类型。这个类型决定了编译器在编译期允许对该变量执行哪些操作。
例如:
Father child = new Son(); // child 的编译类型是 Father
在这个例子中,child
的编译类型是 Father
类型。编译器在编译时会检查 child
变量是否调用了 Father
类中定义的方法。如果尝试调用 Father
类中不存在的方法,则会导致编译错误。
运行类型
运行类型指的是实际赋给该变量的对象的类型。也就是说,当一个对象被创建出来,并被赋予某个引用变量时,这个对象的实际类型就是该变量的运行类型。运行类型决定了在运行时,对象实际上可以执行哪些操作。
继续上面的例子:
Father child = new Son(); // child 的运行类型是 Son
在这个例子中,虽然 child
的编译类型是 Father
,但是它实际上指向的是一个 Son
类的对象。因此,child
的运行类型是 Son
类型。
总结
- 编译类型 是在编译时确定的,由变量声明时使用的类型决定。编译器根据编译类型来检查代码的合法性。
- 运行类型 是在运行时确定的,由实际创建的对象的类型决定。运行时类型决定了实际执行的操作。
多态性
当编译类型和运行类型不一致时,就会出现所谓的多态性。多态性允许我们在编译时使用父类的引用,而在运行时却可以调用子类的方法。这是因为Java中的方法调用是动态绑定的,也就是说,在运行时会根据对象的实际类型来决定应该调用哪个方法。
例如:
class Father {
public void doWork() {
System.out.println("父类doWork方法");
}
}
class Son extends Father {
@Override
public void doWork() {
System.out.println("子类doWork方法");
}
}
public class Main {
public static void main(String[] args) {
Father child = new Son(); // 多态:Father 引用指向 Son 对象
child.doWork(); // 运行时调用 Son 类中的 doWork 方法
}
}
在这个例子中,虽然 child
的编译类型是 Father
类,但是在运行时 child.doWork()
实际上调用的是 Son
类中重写的方法,这是多态性的体现。
多态的向上转型
向上转型的前提
多态的向上转型要求存在继承关系的类之间可以互相转换引用。也就是说,只有在父类和子类之间存在继承关系时,才能发生向上转型。例如,Cat
类继承自 Animal
类,那么 Cat
就是 Animal
的子类,可以进行向上转型。
向上转型的本质
向上转型的本质是父类的引用指向了子类的对象。这种转换是自动的,不需要显式地使用类型转换操作符。例如:
Animal a = new Cat(); // 向上转型
向上转型的语法
向上转型的语法遵循以下格式:
父类类型 引用名 = new 子类类型();
例如:
Person p = new Student(); // 假设 Student 继承自 Person
特点:
编译类型看左边,运行类型看右边
- 编译类型看左边:在编译时,Java编译器会根据引用变量的类型(即左侧的父类类型)来决定哪些方法和属性是可以访问的。也就是说,如果一个方法或属性在父类中不存在,则即使子类中有也不能被访问。
- 运行类型看右边:在运行时,实际的对象类型(即右侧的新建子类对象)决定了实际调用的方法版本。如果调用的方法被子类重写,则会调用子类版本的方法。
可以调用父类中的所有成员
向上转型后的引用可以调用父类中的所有成员(前提是符合访问控制权限)。这是因为编译器在编译时会检查引用类型是否包含被调用的方法或属性。
不能调用子类中特有成员
如果子类中包含了一些父类中没有的方法或属性,那么向上转型后的引用是不能访问这些子类特有的成员的。因为编译器在编译时不知道这些成员的存在。
最终运行效果看子类的具体实现
当调用一个被重写的方法时,实际执行的是子类中提供的实现版本。这意味着即使父类和子类中都有相同的方法签名,实际执行的也是子类的方法实现。
示例代码
class Animal {
public void eat() {
System.out.println("Animal eats");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("Cat eats");
}
public void meow() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Cat(); // 向上转型
a.eat(); // 输出 "Cat eats"
// 下面这一行会导致编译错误,因为Animal类型没有meow方法
// a.meow();
}
}
在这个例子中,Animal a = new Cat();
就是一个向上转型的例子。当我们调用 a.eat()
时,虽然编译时类型是 Animal
,但由于运行时类型是 Cat
,因此实际输出的是 “Cat eats”。如果我们尝试调用 a.meow()
,则会导致编译错误,因为 Animal
类型中没有 meow
方法。
多态的向下转型
向下转型是在多态中一种常见的操作,它涉及到从父类引用转换到子类引用的过程。下面是关于向下转型的一些关键点及其详细的解释:
向下转型的语法
向下转型的语法遵循以下格式:
子类类型 引用名 = (子类类型) 父类引用;
例如:
Cat cat = (Cat) a; // 假设 a 是 Animal 类型的引用
特点:
1.只能强转父类的引用,不能强转父类的对象
向下转型只能应用于父类的引用,而不能直接应用于父类的对象。这是因为向下转型实际上是将父类引用转换成子类引用,而不是改变对象本身的类型。例如:
Animal a = new Cat(); // 向上转型
Cat cat = (Cat) a; // 向下转型
在这个例子中,a
是 Animal
类型的引用,但它实际上指向的是 Cat
类型的对象。向下转型将 a
转换成 Cat
类型的引用。
2.要求父类的引用必须指向的是当前目标类型的对象
向下转型的一个关键点是,只有当父类引用实际上指向的是子类对象时,向下转型才是安全的。如果不满足这一点,将会导致 ClassCastException
异常。例如:
Animal a = new Dog(); // 向上转型
Cat cat = (Cat) a; // 错误,因为 a 实际指向的是 Dog 类型的对象
在这个例子中,由于 a
实际指向的是 Dog
类型的对象,而我们试图将其转换成 Cat
类型的引用,这会导致运行时异常。
3.向下转型后可以调用子类类型中所有的成员
一旦完成向下转型,就可以通过新的子类引用访问子类中特有的方法和属性。例如:
class Animal {
public void eat() {
System.out.println("Animal eats");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("Cat eats");
}
public void meow() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Cat(); // 向上转型
Cat cat = (Cat) a; // 向下转型
cat.meow(); // 输出 "Meow"
}
}
在这个例子中,通过向下转型后,可以调用 Cat
类特有的 meow()
方法。
总结
向下转型是多态中的一个重要概念,它允许我们恢复子类引用以便访问子类特有的方法和属性。不过,需要注意的是,向下转型必须谨慎使用,以避免 ClassCastException
异常。通过使用 instanceof
关键字可以增加向下转型的安全性。
多态的注意事项
-
属性没有重写之说,属性的值看编译类型
在Java中,确实没有“属性重写”这一说法。当子类中定义了一个与父类中相同名称的属性时,这种行为被称为属性的遮蔽(shadowing)。在运行时,如果通过子类对象或子类的引用访问该属性,那么实际上是访问的子类中的属性,而不是父类中的。但是,如果通过父类的引用访问该属性,那么访问的就是父类中的属性,即使实际对象是子类的实例。
例如,如果我们有两个类,一个基类
Base
和一个派生类Sub
,它们都有一个整型属性count
,那么当通过Base
类型的引用访问count
属性时,会得到Base
类中的count
值,即使引用实际上指向的是Sub
类的实例。这是因为属性的访问取决于引用的编译类型,而不是实际的对象类型。方法的访问取决于引用的运行类型class Base { public int count = 10; } class Sub extends Base { public int count = 20; } public class Main { public static void main(String[] args) { Base base = new Sub(); // 向上转型 System.out.println(base.count); // 输出 10,因为base的编译类型是Base } }
这个例子展示了属性值取决于引用的编译类型,而非运行类型。如果想要访问子类中的
count
值,可以首先判断引用的运行类型是否为子类,然后进行向下转型。 -
instanceof比较操作符,用于判断对象的运行类型是否为XX类型或XX类型的子类型
instanceof
是 Java 中的一个二元操作符,用于测试一个对象是否是一个特定类的实例或者是这个类的子类的实例。如果对象是该类或其子类的实例,则返回true
;否则返回false
。下面是
instanceof
的一些用法示例:-
基础用法:
Animal animal = new Dog(); if (animal instanceof Dog) { System.out.println("animal is a Dog"); }
-
检查是否为子类的实例:
Animal animal = new Dog(); if (animal instanceof Animal) { System.out.println("animal is an Animal or its subclass"); }
-
检查是否为接口的实现:
class Bird implements Flyable { public void fly() { System.out.println("Bird is flying"); } } Animal animal = new Bird(); if (animal instanceof Flyable) { System.out.println("animal can fly"); }
-
多态性检查:
Animal animal = new Cat(); if (animal instanceof Dog) { System.out.println("animal is a Dog"); } else if (animal instanceof Cat) { System.out.println("animal is a Cat"); }
示例代码
下面是一个完整的示例代码,展示
instanceof
的用法:// 定义Animal类 class Animal { public void eat() { System.out.println("The animal is eating."); } } // 定义Dog类,继承自Animal class Dog extends Animal { @Override public void eat() { System.out.println("The dog is eating."); } } // 定义Cat类,继承自Animal class Cat extends Animal { @Override public void eat() { System.out.println("The cat is eating."); } } // 定义Bird类,实现Flyable接口 interface Flyable { void fly(); } class Bird implements Flyable { public void fly() { System.out.println("Bird is flying"); } } public class Main { public static void main(String[] args) { Animal animal = new Dog(); System.out.println("Is animal a Dog? " + (animal instanceof Dog)); // 输出:Is animal a Dog? true System.out.println("Is animal an Animal? " + (animal instanceof Animal)); // 输出:Is animal an Animal? true Animal bird = new Bird(); System.out.println("Is bird a Flyable? " + (bird instanceof Flyable)); // 输出:Is bird a Flyable? true Animal cat = new Cat(); if (cat instanceof Dog) { System.out.println("cat is a Dog"); } else if (cat instanceof Cat) { System.out.println("cat is a Cat"); // 输出:cat is a Cat } } }
解释
-
基础用法:
Animal animal = new Dog();
animal instanceof Dog
检查animal
是否是Dog
类的实例,返回true
。
-
检查是否为子类的实例:
animal instanceof Animal
检查animal
是否是Animal
类的实例或其子类的实例,返回true
。
-
检查是否为接口的实现:
bird instanceof Flyable
检查bird
是否实现了Flyable
接口,返回true
。
-
多态性检查:
cat instanceof Dog
检查cat
是否是Dog
类的实例,返回false
。cat instanceof Cat
检查cat
是否是Cat
类的实例,返回true
。
通过这些示例,你应该能够理解
instanceof
的基本用法和它在 Java 编程中的应用。 -
为何要使用多态
多态的使用极大地提高了面向对象编程的灵活性和可扩展性。以下是使用多态的几个主要原因:
- 提高代码的重用性:通过多态,我们可以将与具体实现无关的代码抽象出来,形成通用的接口或基类。这样,不同的子类可以实现同一个接口,从而使得代码更加灵活、可重用。
- 增强程序扩展性:多态使得程序在面对新的需求时,可以方便地添加新的子类来实现新的功能,而不需要修改已有的代码。这有助于提高程序的扩展性和可维护性。
- 提高代码可读性:使用多态可以让代码更加简洁易懂,因为我们可以将相似的操作归为同一个接口或基类,减少代码的冗余和复杂性。
多态的优点
- 可替换性:多态对已存在代码具有可替换性。例如,多态对圆(Circle)类工作,对其他任何圆形几何体,如圆环,也同样工作。
- 可扩充性:多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。
- 接口性:多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
- 灵活性:多态在应用中体现了灵活多样的操作,提高了使用效率。
- 简化性:多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
多态的分类
多态可以分为以下几类:
- 变量多态:基类型的变量(对于C++是引用或指针)可以被赋值基类型对象,也可以被赋值派生类型的对象。
- 函数多态:相同的函数调用界面(函数名与实参表),传送给一个对象变量,可以有不同的行为,这视该对象变量所指向的对象类型而定。
- 动态多态:通过类继承机制和虚函数机制生效于运行期。可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口。也被称为子类型多态(Subtype polymorphism)或包含多态(inclusion polymorphism)。
- 静态多态:模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。可以用来实现类型安全、运行高效的同质对象集合操作。
多态的机制原理
多态是面向对象编程中的一个重要特性,它允许一个接口在不同的上下文中表现出不同的行为。在C++和Java等面向对象语言中,多态的实现机制主要依赖于虚函数、虚函数表(vtable)、虚函数指针(vptr)等概念。下面是关于多态机制原理的详细介绍:
C++中的多态
在C++中,多态主要是通过虚函数(virtual function)来实现的。虚函数是一个类的成员函数,它在基类中被声明为虚拟的,并且可以在派生类中被重写。当通过一个基类的指针或引用调用一个虚函数时,C++会在运行时决定调用哪个函数实现,这个过程被称为动态绑定(dynamic binding)。
虚函数表(Virtual Function Table, VTable)
- 定义:虚函数表是C++编译器自动生成的一个数据结构,用于存储一个类的所有虚函数的地址。当一个类包含至少一个虚函数时,就会有一个与之关联的虚函数表。
- 作用:虚函数表允许程序在运行时根据对象的实际类型来调用正确的虚函数实现。
虚函数指针(vptr)
- 定义:每一个包含虚函数的对象实例都会有一个隐藏的虚函数指针(vptr),它指向该对象所属类的虚函数表。
- 作用:当通过基类的指针或引用调用虚函数时,程序会根据vptr找到正确的虚函数表,并根据虚函数在表中的位置调用相应的函数。
Java中的多态
Java中的多态机制类似于C++,但实现细节有所不同。Java中没有显式的虚函数关键字,所有的非final方法都可以被子类重写,因此默认实现了多态。
动态绑定
在Java中,方法的调用在运行时通过动态绑定来实现。这意味着,当一个方法通过父类引用被调用时,实际调用的是该引用所指向的对象所属类的方法实现。这种机制允许Java在运行时根据对象的实际类型来决定调用哪个方法。
方法重写
Java中的方法重写(override)是指在子类中提供一个与父类中同名、同参数列表和同返回类型的方法实现。这使得子类可以为继承的方法提供一个具体的实现,当通过父类引用调用该方法时,实际上执行的是子类提供的实现。
实现细节
不论是C++还是Java,多态的实现都涉及到了以下几个关键点:
- 继承:多态的基础是继承关系,只有在继承的基础上,才能实现基类引用指向子类对象的多态性。
- 方法重写:在派生类中重写基类的虚函数或方法,这是实现多态的关键步骤。
- 动态绑定:在运行时根据对象的实际类型来决定调用哪个方法的实现。
示例
以下是C++中的一个简单示例,展示多态是如何工作的:
class Base {
public:
virtual void Print() { std::cout << "Base::Print" << std::endl; }
};
class Derived : public Base {
public:
void Print() override { std::cout << "Derived::Print" << std::endl; }
};
void callPrint(Base* ptr) {
ptr->Print(); // 调用的是ptr指向的对象的Print方法
}
int main() {
Base b;
Derived d;
Base* basePtr = &d; // 基类指针指向派生类对象
callPrint(&b); // 输出: Base::Print
callPrint(basePtr); // 输出: Derived::Print
}
在这个例子中,callPrint
函数接受一个 Base
类型的指针,但由于动态绑定,实际调用的是指针指向的对象所属类的 Print
方法。
通过这些机制,多态为面向对象编程带来了极大的灵活性,使得程序设计更加模块化,易于扩展和维护。