一、多态
1、什么是多态
多态(Polymorphism)是面向对象编程的三大核心特性之一(另外两个是封装和继承)。多态性允许一个接口或基类的不同实现或子类以统一的方式处理。
二、方法多态
方法的多态性主要通过方法重载(Overload)和方法重写(Override)来实现。
1、方法重载实现方法多态
package com.pack1;
public class Test {
public static void main(String[] args) {
Cal cal = new Cal();
System.out.println(cal.sum(1, 2));
System.out.println(cal.sum(1.1, 2.2));
}
}
class Cal {
public int sum(int n1, int n2) {
return n1 + n2;
}
public double sum(double n1, double n2) {
return n1 + n2;
}
}
运行结果:
这个例子中我们使用了方法重载来实现 sum 这个方法的多态,对方法 sum 传入不同的参数,可以看调用不同的 sum 函数,这就是多态的一种体现。
2、方法重写实现方法多态
package com.pack1;
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
animal.makeSound();
Dog dog = new Dog();
dog.makeSound();
}
}
class Animal {
public void makeSound() {
System.out.println("Animal make sound...");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("woof...");
}
}
运行结果:
这一个例子就是通过方法重写实现多态,当我们对不同对象使用 makeSound 方法时,调用的 makeSound 方法也是不同的,这里方法重写也能体现多态。
三、对象多态
1、编译时类型和运行时类型
对象的多态主要体现在是编译时类型和运行时类型。编译时类型是声明对象引用变量时使用的类型,也就是引用变量的类型,运行时类型是实际创建的对象的类型,也就是实际指向的对象的类型。
例如下面的一段代码:
package com.pack1;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
}
}
class Animal {
}
class Dog extends Animal {
}
这段代码中 Dog 为 Animal 类的子类,然后我们声明的一个 Animal 类的对象引用变量 animal ,然后创建一个 Dog 类的对象,将这个 Dog 类的对象的引用赋值给这个 Animal 类的对象引用变量 animal 。
这里的 animal 的编译时类型就是 Animal ,运行时类型就是 Dog 。
从这个例子中,我们可以总结出一些结论:
- 编译时类型在声明对象引用变量时就确定了,不能改变;而运行类型是可以改变的。
- 编译时类型看声明时的 “=” 左边,运行时类型看 “=” 右边。
2、向上转型(Upcasting)
上面的例子中我们可以看到我们使用一个 Animal 类的对象引用变量存储了一个 Dog 类的对象的引用,也就是使用一个父类引用变量指向了一个子类对象,实际上这个就是向上转型的本质。
所以说,向上转型(Upcasting)是指将子类对象的引用转换为父类类型的引用。在Java中,这是一个隐式的类型转换,因为子类对象本身就具备父类的所有特性和行为。因此,我们可以直接将子类对象赋值给父类引用,而不需要显式的类型转换。
向上转型的语法:
Parent parentRefe = new Child();
在这个例子中,Child
是 Parent
的子类。通过向上转型,Child
对象被赋值给 Parent
类型的引用 parent
。
向上转型的特点:
- 编译类型(或者说引用变量的类型)为父类:向上转型后的编译类型是父类,因此只能访问父类中定义的方法和属性,不能访问子类中的特有的方法和属性。这是因为在编译阶段能调用哪些成员和属性是由编译类型决定的,对于向上转型的编译类型一般是父类类型,父类中没有子类特有的属性和方法,所以父类引用变量无法访问子类特有的属性和方法,如果使用这个向上转型的引用变量访问了子类的特有方法,则在编译阶段就会报错。
- 运行类型(或者说指向的对象的类型)为子类:虽然编译类型是父类,但运行类型是子类。这意味着,如果子类重写了父类的方法,调用的将是子类的方法。这是因为子类重写父类的方法,父类中也有同签名的方法,所以使用这个向上转型的引用变量在编译阶段不会报错。在运行阶段,调用的方法是哪个方法,实际上是由运行时类型决定的,向上转型的运行时类型一般是子类类型,所以这时调用方法时,是从子类中依次向上找的(先从子类中找是否有这个方法,如果没有,在向父类找,一次向上找,直到 Object 类,也就是之前我们提到的方法调用的规则),所以子类中如果有重写父类的方法,会调用子类的方法。
向上转型补充:
在向上转型后,如果子类中有与父类相同的属性,如果使用向上转型后的引用变量访问这个属性,访问的实际上是编译类型(或者说引用变量的类型)对应的那个属性,也就是父类对应的属性。
package com.pack1;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();// 向上转型
System.out.println(animal.num);
}
}
class Animal {
public int num = 100;
}
class Dog extends Animal {
public int num = 200;
}
运行结果:
所以说属性是不存在重写这一说法的,这里与子类中重写父类的方法然后调用这个方法的情况不一样。这个问题我们将在下面的动态绑定和静态绑定里讲解。
3、向下转型(Downcasting)
上面我们提到向上转型后不能调用子类的特有属性和方法,那如果我们需要访问子类中特有的方法和属性呢,就必须使用向下转型(Downcasting)。
向下转型的语法:
ChildClass childRefe = (ChildClass)parentRefe;
向下转型的细节:
向下转型是将父类引用转换为子类类型的引用。向下转型需要显式转换(也就是强制类型转换),并且需要确保实际对象类型是目标子类类型,否则会抛出 ClassCastException
异常。
也就是说必须保证这里的 parentRefe 引用变量指向的是 ChildClass 类的对象(这里就表明了前面进行了向上转型,所以一般情况下必须先向上转型然后才能向下转型),不然就会抛出异常。而且这里强制转换的只是这个引用变量的类型,没有转换对象的类型,对象一直都是子类类型。在向下转型之后就可以完成我们上面提到的目的了——访问子类特有的属性和方法。
实际上,这里的向下转型使引用变量的类型变成了子类类型,或者说把编译类型变成了子类类型,所以就可以调用子类的特有方法了。这时的运行类型(或者说是指向的对象的类型)依旧是子类类型,这个子类类型与引用变量的子类类型是一致的。
例子:
package com.pack1;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();// 向上转型
animal.cry();
// 这里的animal引用变量的编译类型是父类Animal类,
// 所以不能调用子类Dog类中特有的shakeTail方法,
// 但是可以调用父类Animal中也有的cry方法,
// 因为这里的运行类型是子类Dog类,也就是这个animal引用变量指向的是Dog类的对象
// 这里子类中重写了这个cry方法,所以实际调用的是子类的cry方法
Dog dog = (Dog)animal;// 向下转型
dog.shakeTail();
// 这里向下转型后就可以调用子类的特有方法了,
// 因为引用变量(或者说编译类型)的类型是子类类型
}
}
class Animal {
public void cry() {
System.out.println("动物叫~");
}
}
class Dog extends Animal {
public void cry() {
System.out.println("汪汪叫~");
}
public void shakeTail() {
System.out.println("摇尾巴~");
}
}
运行结果:
4、动态绑定和静态绑定
1)静态绑定(Static Binding)
静态绑定,也称为早期绑定(Early Binding),是在编译时决定的绑定。这意味着在编译阶段,编译器已经确定了方法调用的目标对象和要调用的方法。这种绑定通常适用于静态方法、私有方法、final修饰的方法和所有的字段(属性)访问,因为它们在编译时是确定的,不涉及对象的动态类型。
这里我们就可以对上面所说的属性不存在重写这种说法进行解释了,属性的绑定在编译时进行,称为静态绑定(Static Binding)。这意味着编译器根据引用类型确定访问哪个类的属性,而不管实际对象的类型是什么。
示例:
class Parent {
int value = 10;
}
class Child extends Parent {
int value = 20;
}
public class TestFields {
public static void main(String[] args) {
Parent parent = new Child();
System.out.println(parent.value); // 输出 10
}
}
在这个示例中,parent.value
访问的是 Parent
类的 value
属性,而不是 Child
类的 value
属性。因为在编译时,parent
的类型是 Parent
,所以编译器选择了 Parent
类的 value
属性。
2)动态绑定(Dynamic Binding)
动态绑定,也称为晚期绑定(Late Binding),是在运行时决定的绑定。这意味着在运行阶段,根据实际对象的类型来决定调用的方法。这种绑定方法适用于非静态、非私有、非final方法。动态绑定是实现多态性(Polymorphism)的关键机制。
方法调用在运行时确定,称为动态绑定(Dynamic Binding)。这意味着方法调用根据实际对象的类型来确定执行哪个类的方法。
class Parent {
void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
@Override
void display() {
System.out.println("Child display");
}
}
public class TestMethods {
public static void main(String[] args) {
Parent parent = new Child();
parent.display(); // 输出 "Child display"
}
}
在这个示例中,调用 parent.display()
时,执行的是 Child
类的 display
方法。因为在运行时,parent
实际上指向的是 Child
类型的对象,所以调用的是 Child
类的实现。
3)动态绑定机制
当调用方法(非静态、非私有、非final方法)时,该方法会与实际对象的类型绑定。也就是说在调用某个方法时,具体调用的是哪个方法是由现在调用这个方法的实际对象的类型决定的。对于属性,是没有动态绑定的,在哪里声明的就使用哪里的,也就是说属性的值是由声明它的类决定,而不是由实际对象的类型决定。
例子:
package com.pack1;
public class Test {
public static void main(String[] args) {
Base b = new Sub();// 向上转型
System.out.println(b.foo());// 输出30
// 这里的b指向的实际对象的类型是Sub类,
// 所以调用foo方法会在Sub类中找,
// 但是Sub类中没有foo方法,所以就向父类Base类中找,
// 父类中有foo方法,调用父类中的foo方法,父类的foo方法中调用了getNum方法,
// 我们发现子类中对getNum方法进行了重写,这时实际对象的类型是子类Sub类,
// 所以getNum这个方法实际上是与这时的实际对象类型即子类Sub类绑定的,这里就体现了动态绑定,
// 所以调用的是子类Sub类的getNum方法,getNum访问的是属性num,对于属性是没有动态绑定的,
// 也就是说,属性的值取决于声明该变量的类,而不是实际对象的类型,也就是说哪里声明就使用哪里的,
// 所以会使用子类中的num属性,所以getNum方法返回的值为子类的num值,即20,
// 所以这里调用的foo函数返回的值为30
System.out.println(b.bar());// 输出20
// 这里b指向的实际对象的类型为Sub类,调用bar方法会先从Sub类中寻找,
// 然后在Sub类中没找到,再向上面的父类Base类中寻找,然后再父类Base类中找到,
// 然后调用父类中的bar方法,bar方法访问了num属性,上面我们提到了,
// 属性是没有动态绑定的,在哪里声明就使用哪里的,所以直接使用父类中的num属性,
// 所以bar方法会返回20
}
}
class Base {
public int num = 10;
public int foo() {
return getNum() + 10;
}
public int getNum() {
return num;
}
public int bar() {
return num + 10;
}
}
class Sub extends Base {
public int num = 20;
public int getNum() {
return num;
}
}
运行结果:
5、补充
在之前讲解的运算符知识中,我们提到 instanceof 这个运算符,到这里我们可以说这个运算符判断的其实是运行类型(或者说实际对象的类型)是否为指定类型的同类或者子类。
我们可以通过一个例子来验证:
package com.pack1;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();// 向上转型
System.out.println(animal instanceof Animal);
System.out.println(animal instanceof Dog);
}
}
class Animal {
}
class Dog extends Animal {
}
这里的运行结果为:
这里显然判断的是实际对象的类型(运行类型),因为这里的实际对象的类型是 Dog 类,Dog 类是 Animal 的子类,所以第一个为 true,Dog 类是 Dog 类,所以第二个为 true。
我们假设判断的是引用变量的类型(编译类型),引用类型为 Animal 类,Animal 类是 Animal 类,第一个为 true,但是第二个 Animal 类是 Dog 类的父类,而不是其子类,所以第二个为 false,与实际运行结果不同,假设不成立,所以判断的不是引用变量的类型。
所以说这个运算符 instanceof 判断的是实际对象的类型是否为指定类型的同类或子类。
6、实例
1)多态数组
将不同类型的对象(Person
、Student
、Teacher
)存储在同一个数组中,可以利用动态绑定(也就是多态)来调用这些对象的相应方法。
package com.pack1;
public class Test {
public static void main(String[] args) {
Person[] arr = new Person[3];
arr[0] = new Person("Alice", 18);
arr[1] = new Student("Bob", 19, 79.5);
arr[2] = new Teacher("Kim", 28, 10000.0);
// 向上转型
for (int i = 0; i < 3; i++) {
arr[i].say();// 调用say方法时,会根据实际对象的类型来判断调用的方法是哪一个,
// 如果实际对象是父类,则调用父类的say方法,
// 如果实际对象是子类,则会调用子类重写父类的say方法
}
for (int i = 0; i < 3; i++) { // 向下转型
// 这里使用instanceof判断实际对象的类,如果实际对象是特定子类,
// 则向下转型,然后就可以调用子类特有的方法了
if(arr[i] instanceof Student) {
Student student = (Student)arr[i];
student.study();
} else if (arr[i] instanceof Teacher) {
Teacher teacher = (Teacher)arr[i];
teacher.teach();
}
}
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void say() {
System.out.println(name + "\t" + age + "\t");
}
}
class Student extends Person {
private double score;
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
@Override
public void say() {
System.out.print("student ");
System.out.print(getName() + "\t" + getAge() + "\t");
System.out.println("score " + score);
}
public void study() {
System.out.print("student ");
System.out.print(getName() + "\t" + getAge() + "\t");
System.out.println("study~");
}
}
class Teacher extends Person {
private double salary;
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
@Override
public void say() {
System.out.print("teacher ");
System.out.print(getName() + "\t" + getAge() + "\t");
System.out.println("salary " + salary);
}
public void teach() {
System.out.print("teacher ");
System.out.print(getName() + "\t" + getAge() + "\t");
System.out.println("teach~");
}
}
2)多态参数
package com.pack1;
public class Test {
public void testSound(Animal animal) {
animal.makeSound();
}
public static void main(String[] args) {
Test test = new Test();
Animal dog = new Dog();// 向上转型
Animal cat = new Cat();// 向上转型
test.testSound(dog);
test.testSound(cat);
}
}
class Animal {
public void makeSound() {
System.out.println("Animal sound~");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof~");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow~");
}
}
运行结果: