1. 多态的基本概念
1.1 定义:
多态是指同一操作作用于不同的对象时,能够表现出不同的行为。多态通常通过以下两种方式实现:
- 方法重载(Overloading)
- 方法重写(Overriding)
1.2 示例:
假设有一个父类Animal
,它有一个sound
方法。Dog
和Cat
类继承了Animal
,并重写了sound
方法。通过多态,可以让我们用统一的方式调用sound
方法,但不同的对象会有不同的行为。
java">class Animal {void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {@Overridevoid sound() {System.out.println("Dog barks");}
}class Cat extends Animal {@Overridevoid sound() {System.out.println("Cat meows");}
}
2. 多态的两种实现方式
Java中的多态有两种主要形式:方法重载和方法重写。
2.1 方法重载(Overloading)方法重载的详细知识点可以看这篇笔记
方法重载是指在同一个类中,方法名相同,但参数列表不同(参数的类型、个数或者顺序不同)。方法的返回类型可以相同或不同。重载是静态多态,发生在编译时。
java">class Printer {void print(String message) {System.out.println(message);}void print(int number) {System.out.println(number);}
}
在上面的例子中,print
方法被重载了,可以接收String
类型或int
类型的参数。
2.2 方法重写(Overriding)方法重写的详细知识点可以看这篇笔记
方法重写是指子类重新实现父类的某个方法,方法名、参数列表和返回类型必须和父类中的方法完全相同。重写是动态多态,发生在运行时。
java">class Animal {void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {@Overridevoid sound() {System.out.println("Dog barks");}
}class Cat extends Animal {@Overridevoid sound() {System.out.println("Cat meows");}
}
在这个例子中,Dog
和Cat
类重写了Animal
类中的sound
方法。通过多态,父类引用可以指向子类对象,并且调用子类的sound
方法。
3. 多态的实现机制
3.1 Java中的多态主要通过以下方式实现:
- 父类引用指向子类对象:多态依赖于父类类型的引用指向子类对象。
- 方法的动态绑定:当调用父类引用的方法时,Java虚拟机会在运行时根据对象的实际类型来决定调用哪个方法。
3.2 示例:父类引用指向子类对象
java">public class Main {//通过一个方法实现不同的功能(Dog和Cat中有同一种sound方法,但是却能实现不同功能,这就叫多态)public static void main(String[] args) {Animal animal1 = new Dog(); // 父类引用指向子类对象animal1.sound(); // 输出:Dog barksAnimal animal2 = new Cat();animal2.sound(); // 输出:Cat meows}
}
4. 多态的好处
多态提供了许多编程上的好处:
- 代码复用:同一方法名可以用于不同的对象,减少了重复代码的编写。
- 提高灵活性和可扩展性:可以通过继承和实现接口来扩展程序,无需修改现有代码。
- 增强系统的可维护性:通过多态,父类和子类的关系更加清晰,修改父类的代码时,子类可以自动适应新的行为。
5. 运行时多态与编译时多态
- 运行时多态(动态绑定):通过方法重写实现,调用方法时,Java会根据对象的实际类型来选择方法。这是通过父类引用指向子类对象的方式来实现的。
- 编译时多态(静态绑定):通过方法重载实现,编译器会根据参数的类型来选择调用哪个方法。
5.1 运行时多态示例:(方法重写)
java">class Animal {void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {@Overridevoid sound() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) {Animal animal = new Dog(); // 父类引用指向子类对象animal.sound(); // 输出:Dog barks}
}
5.2 编译时多态示例:(方法重载)
java">class Printer {void print(String message) {System.out.println(message);}void print(int number) {System.out.println(number);}
}public class Main {public static void main(String[] args) {Printer printer = new Printer();printer.print("Hello, World!"); // 调用print(String)printer.print(100); // 调用print(int)}
}
6. 向上转型与向下转型
6.1 向上转型(Upcasting):
向上转型是指将子类对象赋给父类变量,即将一个子类对象转型为父类类型。向上转型通常是安全的,因为子类对象本身是父类对象的一种特殊形式。向上转型不会丢失任何信息,因为父类包含了子类的所有公共方法和属性,父类引用可以指向子类的对象。
示例:
java">class Animal {void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {void sound() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) {Dog dog = new Dog();Animal animal = dog; // 向上转型:Dog -> Animalanimal.sound(); // 输出:Dog barks}
}
解释:
dog
是Dog
类型的对象。- 通过
Animal animal = dog;
进行向上转型,animal
变成了Animal
类型的引用,指向了Dog
类型的对象。 - 即使
animal
的类型是Animal
,它依然会调用Dog
类的sound()
方法,这是因为 Java 支持动态方法调度(多态)。
6.2 向下转型(Downcasting)
向下转型是指将父类变量转换为子类类型。向下转型在运行时可能会引发 ClassCastException
,因为父类引用的对象可能并不是子类的实例。因此,在执行向下转型之前,通常需要进行类型检查。
示例:
java">class Animal {void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {void sound() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) {Animal animal = new Dog(); // 向上转型Dog dog = (Dog) animal; // 向下转型dog.sound(); // 输出:Dog barks}
}
解释:
animal
是Animal
类型的引用,它实际上指向一个Dog
类型的对象。Dog dog = (Dog) animal;
将animal
引用向下转型为Dog
类型,这时可以调用Dog
类的方法。- 向下转型时需要确保对象的实际类型是目标类型,否则会抛出
ClassCastException
。
类型检查
为了避免错误的向下转型,可以使用 instanceof
运算符来检查对象的类型。
java">if (animal instanceof Dog) {Dog dog = (Dog) animal; // 安全的向下转型dog.sound();
} else {System.out.println("animal is not a Dog");
}
7. 接口与多态
接口的详细知识点可以看这篇笔记
接口也支持多态。通过接口类型引用实现多态,不同的类实现同一个接口时,可以在运行时根据实际对象的类型调用不同的实现。
java">interface Animal {void sound();
}class Dog implements Animal {@Overridepublic void sound() {System.out.println("Dog barks");}
}class Cat implements Animal {@Overridepublic void sound() {System.out.println("Cat meows");}
}public class Main {public static void main(String[] args) {Animal animal1 = new Dog();animal1.sound(); // 输出:Dog barksAnimal animal2 = new Cat();animal2.sound(); // 输出:Cat meows}
}
8. 方法重载与多态的关系
方法重载和多态有区别,方法重载发生在编译时,而多态主要通过方法重写发生在运行时。方法重载是静态绑定,而多态是动态绑定。两者常常一起使用,但它们并不相同。
java">class Printer {void print(String s) {System.out.println("Printing string: " + s);}void print(int i) {System.out.println("Printing integer: " + i);}
}public class Main {public static void main(String[] args) {Printer printer = new Printer();printer.print("Hello");printer.print(123);}
}
9. 多态的注意事项
- 多态只有在继承或实现接口的情况下才会生效。
- 子类方法必须具有相同的签名(方法名、参数、返回类型)才能重写父类方法,保证多态的正确实现。
- 使用多态时,要注意类型转换,避免出现
ClassCastException
。
10. 多态与构造方法的关系
构造方法不会被继承,且构造方法不能被重写。因此,在子类中调用父类的构造方法时,即使我们有多态的存在,父类构造方法的选择是静态的,和多态无关。
10.1 构造方法 不会被继承
在 Java 中,构造方法是类的特殊方法,用于创建对象并初始化它的状态。虽然子类可以调用父类的构造方法(使用 super()
),但是构造方法本身不会被继承。也就是说,子类不能直接访问父类的构造方法,它只能通过 super()
显式地调用父类的构造方法。如果父类没有显式定义构造方法,Java 会自动提供一个默认的无参构造方法。
示例:
java">class Animal {Animal() {System.out.println("Animal constructor");}
}class Dog extends Animal {// Dog类没有继承Animal的构造方法,它必须显式调用父类的构造方法Dog() {super(); // 显式调用父类构造方法System.out.println("Dog constructor");}
}
解释:
Dog
类并没有继承Animal
类的构造方法,而是必须在自己的构造方法中显式地调用父类的构造方法(通过super()
)。- 即使
Dog
是Animal
的子类,它并没有自动继承Animal
的构造方法,因此Dog
类需要自己定义构造方法。
10.2 构造方法 不能被重写
构造方法不能被重写是指,子类无法用相同的签名来重写父类的构造方法。构造方法的目的是创建类的实例,因此它是与类的实例化过程紧密相关的。构造方法的名字必须和类的名字相同,而且每个类只能有一个构造方法。虽然子类可以定义一个构造方法,并在其中调用父类的构造方法,但它无法“重写”父类的构造方法。
示例:
java">class Animal {Animal() {System.out.println("Animal constructor");}
}class Dog extends Animal {// 这里并不是重写父类的构造方法,而是定义了自己的构造方法Dog() {super(); // 调用父类构造方法System.out.println("Dog constructor");}
}
解释:
- 在
Dog
类中,虽然定义了一个构造方法,它并没有重写父类Animal
的构造方法。Dog
的构造方法是 新的 构造方法,而不是父类构造方法的重写。 - 构造方法无法被重写的原因在于,构造方法的调用是由实例化对象时自动进行的,而不能像普通方法一样通过多态进行动态绑定。
11. 多态与泛型
泛型是Java中用于实现类型安全的一种机制,通常和多态一起使用。通过泛型,你可以实现类型参数化的多态,使得代码在不丧失类型安全的情况下能够处理多种类型。
示例:
java">class Box<T> {private T value;public void setValue(T value) {this.value = value;}public T getValue() {return value;}
}public class Main {public static void main(String[] args) {Box<Integer> intBox = new Box<>();intBox.setValue(10);System.out.println(intBox.getValue()); // 输出:10Box<String> strBox = new Box<>();strBox.setValue("Hello");System.out.println(strBox.getValue()); // 输出:Hello}
}
这里的Box<T>
类是泛型类,T代表一个类型参数。你可以通过不同的类型来创建Box
对象,并通过多态灵活地处理这些不同的类型。
12. 多态与反射
反射是Java提供的一种强大的工具,它允许在运行时动态地访问类的方法、字段、构造方法等。反射和多态结合使用时,可以让我们动态地创建对象、调用方法,而不需要事先知道对象的类型。
示例:
java">import java.lang.reflect.Method;class Animal {public void sound() {System.out.println("Animal makes a sound");}
}class Dog extends Animal {@Overridepublic void sound() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) throws Exception {Class<?> clazz = Class.forName("Dog");Animal animal = (Animal) clazz.getDeclaredConstructor().newInstance();// 使用反射动态调用方法Method method = clazz.getMethod("sound");method.invoke(animal); // 输出:Dog barks}
}
这里,Class.forName("Dog")
动态加载了Dog
类,Method.invoke()
动态调用了Dog
类的sound()
方法,这种方式与多态结合非常有用。
13. 多态与抽象类的结合
抽象类的详细知识点可以看这篇笔记
抽象类可以包含具体的方法实现,也可以包含抽象方法供子类实现。在多态中,抽象类常常作为父类提供统一的行为规范,子类提供具体实现。抽象类和多态结合使得代码更加灵活和可扩展。
示例:
java">abstract class Animal {abstract void sound();
}class Dog extends Animal {@Overridevoid sound() {System.out.println("Dog barks");}
}class Cat extends Animal {@Overridevoid sound() {System.out.println("Cat meows");}
}public class Main {public static void main(String[] args) {Animal animal1 = new Dog();animal1.sound(); // 输出:Dog barksAnimal animal2 = new Cat();animal2.sound(); // 输出:Cat meows}
}
14. 多态与设计模式的关系
多态在很多设计模式中发挥着重要作用,尤其是在策略模式、工厂模式、模板方法模式等中。多态使得不同的实现可以被统一处理,从而使得系统更加灵活和可扩展。
- 策略模式:通过多态切换不同的策略。
- 工厂模式:使用工厂方法根据不同的条件返回不同类型的对象,通过多态来处理这些对象。
- 模板方法模式:通过多态允许子类重写父类的部分方法,从而使得流程控制更加灵活。
15. 虚拟机和多态
在Java的执行过程中,虚拟机会根据实际对象的类型来决定调用哪个方法,这个过程叫做动态方法分派(dynamic dispatch)。在多态中,虚拟机会根据对象的实际类型而不是引用类型来决定调用哪个版本的方法,这样就能实现运行时的多态性。
16. 接口默认方法与多态
从Java 8开始,接口可以包含默认方法(default
)。这些方法可以在接口中提供具体实现,子类可以选择使用默认实现或者重写它。使用接口的默认方法可以实现一种多态机制,使得接口可以演变而不破坏现有代码。
示例:
java">interface Animal {default void sound() {System.out.println("Animal makes a sound");}
}class Dog implements Animal {@Overridepublic void sound() {System.out.println("Dog barks");}
}class Cat implements Animal {// 不重写sound方法,使用默认实现
}public class Main {public static void main(String[] args) {Animal dog = new Dog();dog.sound(); // 输出:Dog barksAnimal cat = new Cat();cat.sound(); // 输出:Animal makes a sound}
}
17. 多态与集合框架
在Java集合框架中,接口和多态结合非常紧密。例如,List
、Set
、Map
等接口允许不同的实现类(如ArrayList
、HashSet
等)被统一处理,通过多态在运行时决定使用哪一种实现。
示例:
java">import java.util.List;
import java.util.ArrayList;public class Main {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("Apple");list.add("Banana");for (String fruit : list) {System.out.println(fruit);}}
}
在这个例子中,List
接口通过多态允许不同类型的列表实现,如ArrayList
、LinkedList
等,它们都可以在运行时根据需要被使用。