多态详解
多态
多态的概念
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)。
- 静态多态:模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。可以用来实现类型安全、运行高效的同质对象集合操作。
java中多态的机制原理
在Java中,多态(Polymorphism)是面向对象编程(OOP)的核心特性之一,它允许不同类的对象对同一消息作出响应。多态性主要体现在编译时多态性和运行时多态性上。编译时多态性通常是通过方法重载(Overloading)实现的,而运行时多态性则是通过方法重写(Overriding)和动态绑定来实现的。
运行时多态的实现原理
运行时多态性主要依赖于继承、接口、方法重写以及动态绑定。以下是多态的一些实现原理:
继承(Inheritance)
多态的基础之一是继承。通过继承,一个类可以获得另一个类的属性和方法。子类可以继承父类,并重写(Override)父类的方法,这使得父类引用可以指向子类对象,从而实现多态。
接口实现(Interface Implementation)
除了继承之外,实现接口也是多态的另一种方式。一个类可以实现多个接口,从而拥有多种行为。通过接口的实现,可以达到类似于继承的效果。
方法重写(Method Overriding)
子类重写父类的方法是实现多态的关键。子类可以提供与父类方法相同签名(方法名、参数列表和返回类型相同)的实现。通过方法重写,子类可以根据需要改变父类方法的行为。
动态绑定(Dynamic Binding)
1.当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
2.当调用对象属性是,没有动态绑定机制,哪里声明,哪里使用
示例一:
class A { // 父类
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A { // 子类
public int i = 20;
public int sum() {
return i + 20;
}
public int getI() {
return i;
}
public int sum1() {
return i + 10;
}
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum()); // ?
System.out.println(a.sum1()); // ?
}
}
代码分析
-
A a = new B();
- 这里创建了一个
B
类的实例,但是将其赋值给了A
类的引用a
。这是多态性的典型用法。
- 这里创建了一个
-
System.out.println(a.sum());
- 尽管
a
是A
类的引用,但是sum
方法在B
类中被重写了。 - 因此,即使我们通过
A
类的引用调用sum
方法,Java 运行时环境会检查实际对象的类型(这里是B
),并调用B
类中的sum
方法。 B
类中的sum
方法返回i + 20
,其中i
是B
类中的i
,值为 20。所以结果是20 + 20 = 40
。
- 尽管
-
System.out.println(a.sum1());
sum1
方法在A
类中定义,并且在B
类中也被重写了。- 但是,
sum1
方法内部访问了i
,这是一个实例变量。 - 由于
a
实际指向的是B
类的实例,所以访问的是B
类中的i
,值为 20。 - 因此,
sum1
方法的结果是20 + 10 = 30
。
输出
40
30
示例二:
class A { // 父类
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A { // 子类
public int i = 20;
public int getI() {
return i;
}
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum()); // ?
System.out.println(a.sum1()); // ?
}
}
在这段代码中,A
是父类,B
是继承自 A
的子类。子类 B
重写了父类 A
的 getI
方法,但并没有重写 sum
和 sum1
方法。我们来分析这段代码的执行结果。
代码分析
-
A a = new B();
- 这里创建了一个
B
类的实例,但是将其赋值给了A
类的引用a
。这是多态性的典型用法。
- 这里创建了一个
-
System.out.println(a.sum());
sum
方法在A
类中定义,并没有在B
类中被重写。- 因此,即使我们通过
A
类的引用调用sum
方法,也会调用A
类中的sum
方法。 A
类中的sum
方法调用getI()
,这是动态绑定。由于getI()
在B
类中被重写,调用的是B
类的getI()
方法,返回20。- 因此,
sum
方法的结果是20 + 10 = 30
。
-
System.out.println(a.sum1());
sum1
方法在A
类中定义,并没有在B
类中被重写。- 因此,即使我们通过
A
类的引用调用sum1
方法,也会调用A
类中的sum1
方法。 A
类中的sum1
方法直接访问了i
,这是一个实例变量。- 由于
a
实际指向的是B
类的实例,但是sum1
方法中的i
是在A
类中定义的,所以访问的是A
类中的i
,值为 10。 - 因此,
sum1
方法的结果是10 + 10 = 20
。
输出
30
20
动态绑定机制
动态绑定机制允许 Java 在运行时根据对象的实际类型来调用相应的方法。以下是父类和子类在不同状态下的动态绑定机制:
-
方法重写:
- 如果子类重写了父类的方法,那么即使引用类型是父类,也会调用子类的实现。
-
方法未被重写:
- 如果子类没有重写父类的方法,那么无论引用类型是什么,都会调用父类的方法。
-
访问实例变量:
- 实例变量的访问不涉及动态绑定。即使引用类型是父类,如果实际对象是子类,那么访问的将是子类中定义的实例变量。
-
访问静态变量:
- 静态变量的访问也不涉及动态绑定。它们属于类,而不是对象。因此,无论引用类型是什么,访问的都是定义静态变量的类中的静态变量。
-
访问私有方法:
- 私有方法不能被重写,因此不存在动态绑定的问题。
多态的实现机制
RTTI(Run-Time Type Identification)
多态实现的技术基础是RTTI,即运行时类型识别。RTTI使得在不知道某个对象的确切类型信息的情况下,可以通过相关机制帮助我们获取对象的类型信息。
方法表(Method Table)
在Java虚拟机(JVM)中,每个类都有一个方法表,表中记录了类中所有方法的地址。当一个对象被创建时,它会包含一个指向其类的方法表的指针。当调用一个方法时,JVM会通过这个指针找到方法的具体实现并执行。
动态分派
多态方法的调用涉及到动态分派的过程。在调用方法时,首先需要完成实例方法的符号引用解析,也就是将符号引用解析为方法表的偏移量。虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法;但实际上,虚拟机会从实际类的方法表中根据偏移量获取该方法名对应的指针,进而指向实际类的方法。
方法调用指令
在JVM中,方法调用指令如invokevirtual
和invokeinterface
用于动态绑定,即调用虚方法和接口方法。
多态的应用
1.多态数组:
数组的定义类型类父类类型,里面保存的实际元素为子类类型
应用实例:现有一个继承结构如下,要求创建1个person对象,属性有name和age,2个student对象,属性有name、age和score,2个teacher对象,属性有name、age和salary,统一放在数组中,并调用每个对象say方法以及如何调用子类特有的方法,比如teacher有一个teacher,student有一个study怎么调用
为了实现这个要求,我们首先定义一个基类 Person
和两个继承自 Person
的子类 Student
和 Teacher
。每个类都会有一个 say
方法,而 Student
和 Teacher
类还会有它们特有的方法 study
和 teacher
。
下面是实现这个要求的 Java 代码示例:
// 定义Person类
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void say() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
}
// 定义Student类,继承自Person
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.println("Hello, I am a student. My name is " + name + ", I am " + age + " years old and my score is " + score);
}
public void study() {
System.out.println("I am studying.");
}
}
// 定义Teacher类,继承自Person
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.println("Hello, I am a teacher. My name is " + name + ", I am " + age + " years old and my salary is " + salary);
}
public void teach() {
System.out.println("I am teaching.");
}
}
public class Main {
public static void main(String[] args) {
// 创建1个Person对象,2个Student对象和2个Teacher对象
Person[] persons = new Person[5];
persons[0] = new Person("Alice", 30); // Person对象
persons[1] = new Student("Bob", 20, 88.5); // Student对象
persons[2] = new Student("Charlie", 22, 92.0); // Student对象
persons[3] = new Teacher("David", 35, 5000.0); // Teacher对象
persons[4] = new Teacher("Eva", 40, 6000.0); // Teacher对象
// 调用每个对象的say方法
for (Person person : persons) {
person.say();
}
// 调用子类特有的方法
for (Person person : persons) {
if (person instanceof Student) {
((Student) person).study();
} else if (person instanceof Teacher) {
((Teacher) person).teach();
}
}
}
}
代码解释
-
Person类:定义了
name
和age
属性,以及一个say
方法。 -
Student类:继承自
Person
类,增加了score
属性,并重写了say
方法。它还有一个特有的study
方法。 -
Teacher类:继承自
Person
类,增加了salary
属性,并重写了say
方法。它还有一个特有的teach
方法。 -
Main类:
- 创建了一个
Person
类型的数组,包含5个元素。 - 分别创建了一个
Person
对象和四个继承自Person
的对象(两个Student
对象和两个Teacher
对象),并将它们存储在数组中。 - 遍历数组,调用每个对象的
say
方法。 - 再次遍历数组,使用
instanceof
操作符检查对象是否为Student
或Teacher
的实例,然后调用相应的特有方法。
- 创建了一个
输出
Hello, my name is Alice and I am 30 years old.
Hello, I am a student. My name is Bob, I am 20 years old and my score is 88.5
Hello, I am a student. My name is Charlie, I am 22 years old and my score is 92.0
Hello, I am a teacher. My name is David, I am 35 years old and my salary is 5000.0
Hello, I am a teacher. My name is Eva, I am 40 years old and my salary is 6000.0
I am studying.
I am studying.
I am teaching.
I am teaching.
这个例子展示了多态性的应用,即通过父类的引用来调用子类对象的方法。同时,它也展示了如何调用子类特有的方法。这是通过使用 instanceof
操作符来检查对象的实际类型,然后进行类型转换来实现的。
2.多态参数:
方法定义的形参类型为父类类型,实参类型为子类类型
应用实例:定义员工类Employee,包含姓名和月工资[private],以及计算年工资getAnnual的方法。普通员工和经理继承了员工,经理类多了奖金bonus属性和管理manage方法,普通员工类多了work方法,普通员工和经理类要求分别重写getAnnual方法
测试类中添加一个方法showEmpAnnal(Employee e),实现获取任何员工对象的年工资,并在main方法中调用该方法[e.getAnnual()]
测试类中添加一个方法,testWork,如果是普通员工,则调用work方法,如果是经理,则调用manage方法
// 定义员工类
class Employee {
private String name;
private double monthlySalary;
public Employee(String name, double monthlySalary) {
if (monthlySalary < 0) {
throw new IllegalArgumentException("月薪不能为负数");
}
this.name = name;
this.monthlySalary = monthlySalary;
}
public double getAnnual() {
return monthlySalary * 12;
}
public String getName() {
return name;
}
}
// 定义普通员工类,继承自Employee
class RegularEmployee extends Employee {
public RegularEmployee(String name, double monthlySalary) {
super(name, monthlySalary);
}
@Override
public double getAnnual() {
// 假设普通员工有第13个月工资
return super.getAnnual() + getMonthlySalary();
}
private double getMonthlySalary() {
return super.getAnnual() / 12; // 简单生成一个月薪
}
public void work() {
System.out.println(getName() + " is working.");
}
}
// 定义经理类,继承自Employee
class Manager extends Employee {
private double bonus;
public Manager(String name, double monthlySalary, double bonus) {
super(name, monthlySalary);
if (bonus < 0) {
throw new IllegalArgumentException("奖金不能为负数");
}
this.bonus = bonus;
}
@Override
public double getAnnual() {
// 经理的年工资包括基本工资和奖金
return super.getAnnual() + bonus;
}
public void manage() {
System.out.println(getName() + " is managing.");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
try {
RegularEmployee regularEmployee = new RegularEmployee("Alice", 3000);
Manager manager = new Manager("Bob", 5000, 10000);
showEmpAnnual(regularEmployee);
showEmpAnnual(manager);
testWork(regularEmployee);
testWork(manager);
} catch (IllegalArgumentException e) {
System.err.println("错误: " + e.getMessage());
}
}
public static void showEmpAnnual(Employee e) {
System.out.println("Annual salary of " + e.getName() + " is: " + e.getAnnual());
}
public static void testWork(Employee e) {
if (e instanceof RegularEmployee) {
((RegularEmployee) e).work();
} else if (e instanceof Manager) {
((Manager) e).manage();
} else {
System.err.println("未知的员工类型: " + e.getName());
}
}
}
代码解释
-
Employee类:定义了
name
和monthlySalary
属性,以及一个getAnnual
方法计算年工资。 -
RegularEmployee类:继承自
Employee
类,重写了getAnnual
方法,增加了第13个月工资。 -
Manager类:继承自
Employee
类,增加了bonus
属性,并重写了getAnnual
方法来包括奖金。 -
Test类:
showEmpAnnual
方法:接受一个Employee
类型的参数,并调用其getAnnual
方法来打印年工资。testWork
方法:接受一个Employee
类型的参数,使用instanceof
操作符检查参数的类型,并调用相应的work
或manage
方法。
输出
Annual salary of Alice is: 39000.0
Annual salary of Bob is: 70000.0
Alice is working.
Bob is managing.
这个例子展示了多态性的应用,即通过父类的引用来调用子类对象的方法。同时,它也展示了如何调用子类特有的方法,这是通过使用 instanceof
操作符来检查对象的实际类型,然后进行类型转换来实现的。
多态的注意事项
- 方法重写的规则:重写的方法必须具有相同的名称、参数列表和返回类型(在Java 5及以后版本中,允许返回类型是父类方法返回类型的子类型,称为协变返回类型)。重写的方法不能比父类方法具有更严格的访问权限。重写的方法不能抛出比父类方法更多的异常(可以抛出更少或相同的异常)。
- 构造方法:构造方法不能被继承或重写,但子类构造方法可以通过
super
关键字调用父类的构造方法。 - 字段隐藏:字段不能被重写,但可以被隐藏。如果子类定义了与父类同名的字段,那么在子类中访问该字段时,将会隐藏父类的字段。
通过以上机制,Java实现了强大的多态性,提高了代码的灵活性和可扩展性。多态不仅使得代码更加简洁和优雅,还增强了系统的可维护性和可扩展性。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)