Java面向对象04——三大特性之多态
多态
1、什么是多态
在Java中,多态是面向对象编程中的一个重要概念,它允许不同类型的对象对同一方法进行不同的实现。具体来说,多态性指的是通过父类的引用变量来引用子类的对象,从而实现对不同对象的统一操作。
2、多态实现的条件
在Java中,要实现多态性,就必须满足以下条件:
-
继承关系
存在继承关系的类之间才能够使用多态性。多态性通常通过一个父类用变量引用子类对象来实现。
-
方法重写
子类必须重写(Override)父类的方法。通过在子类中重新定义和实现父类的方法,可以根据子类的特点行为改变这个方法的行为,如猫和狗吃东西的独特行为。
-
父类引用指向子类对象
使用父类的引用变量来引用子类对象。这样可以实现对不同类型的对象的统一操作,而具体调用哪个子类的方法会在运行时多态决定
例如,下面的案例是根据猫和狗叫的动作的不同,而实现的多态:
class Animal {
public void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("狗发出汪汪声");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("猫发出喵喵声");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 父类引用指向子类对象
Animal animal2 = new Cat(); // 父类引用指向子类对象
animal1.sound(); // 输出:狗发出汪汪声
animal2.sound(); // 输出:猫发出喵喵声
}
}
在这个示例中,Animal
类是父类,Dog
和 Cat
类是它的子类。通过将父类的引用变量分别指向子类对象,实现了多态性。在运行时,根据引用变量的实际类型来调用相应的子类方法,从而输出不同的声音。
3、重写(Overrride)
重写(override):也称为覆盖。重写是子类对父类非静态、非 private 修饰,非 final 修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
方法重写的规则:
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被
public
修饰,则子类中重写该方法就不能声明为protected
- 重写的方法, 可以使用
@Override
注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成aet
), 那么此时编译器就会发现父类中没有aet
方法, 就会编译报错, 提示无法构成重写 - 重写的方法不能抛出比父类中被重写的方法更多或更宽泛的异常。子类中重写的方法可以抛出相同的异常或更具体的异常,或者不抛出异常。
- 例如,如果父类的方法声明抛出
IOException
,则子类中重写的方法可以抛出IOException
或FileNotFoundException
,或者不抛出异常,但不能抛出比IOException
更通用的异常,如Exception
。
- 例如,如果父类的方法声明抛出
- 重写的方法必须具有相同的方法体,或者可以进行方法体的扩展。
- 子类中重写的方法可以调用父类中被重写的方法,使用
super
关键字。
- 子类中重写的方法可以调用父类中被重写的方法,使用
看下面的示例:
class Mineral {
public void mine() {
System.out.println("矿物");
}
}
class Iron extends Mineral {
@Override
public void mine() {
System.out.println("铁矿");
}
public void broked() {
System.out.println("石镐可以破坏它");
}
}
class Diamond extends Mineral {
@Override
public void mine() {
System.out.println("钻石");
}
public void what() {
System.out.println("掉落钻石");
}
}
调用:
public class Minecraft {
public static void main(String[] args) {
Mineral p = new Iron();
p.mine();
// 调用特有的方法
Iron s = (Iron) p;
s.broked();
// ((Iron) p).broked();
}
}
输出:
铁矿
石镐可以破坏它
**重写和重载的区别: **
- 定义位置:重载方法定义在同一个类中,而重写方法定义在父类和子类之间。
- 方法签名:重载方法具有相同的名称,但方法签名(参数类型和个数)不同。重写方法具有相同的名称和方法签名。
- 继承关系:重载方法不涉及继承关系,可以在同一个类中定义。重写方法是在子类中对父类方法的重新定义和实现。
- 运行时调用:重载方法是根据方法的参数列表的不同进行静态绑定,在编译时确定。重写方法是根据对象的实际类型进行动态绑定,在运行时确定。
- 目的:重载方法用于在同一个类中实现相似功能但具有不同参数的方法。重写方法用于子类重新定义父类方法的行为,以适应子类的特定需求。
总结来说,重载是在同一个类中根据参数列表的不同定义多个具有相同名称但参数不同的方法,而重写是子类重新定义和实现了从父类继承的方法。重载方法通过静态绑定在编译时确定调用,重写方法通过动态绑定在运行时确定调用。重载用于实现相似功能但具有不同参数的方法,重写用于改变父类方法的行为以适应子类的需求。
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
重写的设计原则:
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
多态实现的就是动态绑定。
4、向上转型和向下转型
向上转型:
向上转型(Upcasting)是指将一个子类的对象引用赋值给其父类类型的引用变量。这是在面向对象编程中的一种常见操作,用于实现多态性和灵活的对象处理。
在向上转型中,子类对象可以被视为父类对象,可以使用父类类型的引用变量来引用子类对象。这样做的好处是可以以统一的方式处理不同类型的对象,实现代码的灵活性和可扩展性。
向上转型的特点和规则如下:
- 子类对象可以隐式地转型为父类对象,不需要任何显式的类型转换操作。
- 父类引用变量可以引用子类对象,但通过父类引用变量只能访问到子类对象中定义的父类成员,无法访问子类独有的成员。
- 子类对象中重写的方法,在通过父类引用变量调用时,会调用子类中的实现(动态绑定)。
- 向上转型是安全的操作,因为子类对象本身就是一个父类对象。
使用场景:
- 直接赋值
- 方法传参
- 方法返回
下面看代码
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗".equals(var) ){
return new Dog("狗狗",1);
}else if("猫" .equals(var)){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
在上述示例中,存在一个继承关系:类 Dog
继承自类 Animal
。在 Main
类的 main
方法中,首先创建了一个 Dog
类的对象,并将其赋值给一个 Animal
类型的引用变量 anim
al,这就是向上转型的过程。通过 animal
引用变量,可以调用 eat()
方法,而在运行时,实际执行的是 Dog
类中重写的 eat()
方法。
需要注意的是,虽然 animal
引用变量的类型是 Animal
,但是它指向的是一个 Dog
类的对象,因此可以将其重新转型为 Dog
类型(向下转型),并通过 dog
引用变量访问 Dog
类中独有的成员方法 bark()
。
总结起来,向上转型允许将子类对象视为父类对象,以父类类型的引用变量来引用子类对象,实现多态性和灵活的对象处理。
向下转型:
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
// 编译失败,编译时编译器将animal当成Animal对象处理
// 而Animal类中没有bark方法,因此编译失败
// animal.bark();
// 向上转型
// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
cat = (Cat)animal;
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
5、多态的优缺点
假如有如下代码
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
Java多态性的优点:
-
代码的可复用性
能够降低代码的 “圈复杂度”, 避免使用大量的 if - else 上述代码如果不使用多态,就要使用大量的if - else语句,可以促进代码的复用
-
可扩展能力更强:
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高
Java多态性的缺点:
- 运行时性能损失:多态性需要在运行时进行方法的动态绑定,这会带来一定的性能损失。相比于直接调用具体的子类方法,多态性需要在运行时确定要调用的方法,导致额外的开销。
- 代码可读性下降:多态性使得代码的行为变得更加动态和不确定。在某些情况下,可能需要跟踪代码中使用的对象类型和具体的方法实现,这可能降低代码的可读性和理解性。
- 限制访问子类特有成员:通过父类类型的引用变量,只能访问父类及其继承的成员,无法直接访问子类特有的成员。如果需要访问子类特有的成员,就需要进行向下转型操作,这增加了代码的复杂性和维护的难度。
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
//执行结果:
D.func() 0
- 构造 D 对象的同时, 会调用 B 的构造方法.
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
总结:“用尽量简单的方式使对象进入可工作状态”,尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题。
热门相关:婚婚欲睡:腹黑老公请节制 秦吏 罪恶围城 战国明月 一世倾心:误惹腹黑师弟