零基础入门JAVA:多态

零基础入门JAVA:多态
多态多态的定义多态的概念通俗来说就是多种形态具体点就是去完成某个⾏为当不同的对象去完成时会产⽣出不同的状态总的来说同一件事情发生在不同对象身上就会产生不同的结果多态的实现条件在Java中要实现多态必须要满足如下几个条件缺一不可必须在继承体系下子类必须堆父类中方法进行重写通过父类的引用调用重写方法多态体现在代码运行时当传递不同类对象时会调用对应类中的方法publicclassAnimal{Stringname;intage;publicAnimal(Stringname,intage){this.namename;this.ageage;}publicvoideat(){System.out.println(name吃饭);}}publicclassCatextendsAnimal{publicCat(Stringname,intage){super(name,age);}Overridepublicvoideat(){System.out.println(name吃鱼~~~);}}publicclassDogextendsAnimal{publicDog(Stringname,intage){super(name,age);}Overridepublicvoideat(){System.out.println(name吃骨头~~~);}}publicclassTestAnimal{// [cite: 83]// 编译器在编译代码时,并不知道要调用Dog还是Cat中eat的方法// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法// 注意:此处的形参类型必须是父类类型才可以publicstaticvoideat(Animala){a.eat();}publicstaticvoidmain(String[]args){CatcatnewCat(元宝,2);DogdognewDog(小七,1);eat(cat);eat(dog);}}输出结果元宝吃鱼~~~小七吃骨头~~~在上述代码中当类的调用者在编写 eat 这个方法的时候参数类型为 Animal父类此时在该方法内部并不知道也不关注当前的 a 引用指向的是哪个类型哪个子类的实例 。此时 a 这个引用调用 eat 方法可能会有多种不同的表现和 a 引用的实例相关这种行为就称为多态重写重写 (override)也称为覆盖 。重写是子类对父类非静态、非private修饰非final修饰非构造方法等的实现过程进行重新编写返回值和形参都不能改变 。即外壳不变核心重写重写的好处在于子类可以根据需要定义特定于自己的行为 。也就是说子类能够根据需要实现父类的方法方法重写的规则子类在重写父类的方法时一般必须与父类方法原型一致返回值类型、方法名(参数列表)要完全一致被重写的方法返回值类型可以不同但是必须是具有父子关系的访问权限不能比父类中被重写的方法的访问权限更低 。例如如果父类方法被 public 修饰则子类中重写该方法就不能声明为 protected父类被 static、private 修饰的方法、构造方法都不能被重写重写的方法可以使用 Override 注解来显式指定有了这个注解能帮我们进行一些合法性校验 。例如不小心将方法名字拼写错了比如写成 aet那么此时编译器就会发现父类中没有 aet 方法就会编译报错提示无法构成重写【重写与重载的区别】区别点重写 (override)重载 (overload)参数列表一定不能修改必须修改返回类型一定不能修改除非可以构成父子类关系可以修改访问限定符一定不能做更严格的限制可以降低限制可以修改方法重载是一个类的多态性表现而方法重写是子类与父类的一种多态性表现重写的设计原则对于已经投入使用的类尽量不要进行修改 。最好的方式是重新定义一个新的类来重复利用其中共性的内容并且添加或者改动新的内容// 旧手机类定义了基础功能publicclassOldPhone{publicvoidcall(){System.out.println(打电话);}publicvoidsendMessage(){System.out.println(发短信);}// 这是我们需要在子类中重写的方法publicvoidshowCaller(){System.out.println(来电显示显示号码);}}// 新手机类继承旧手机的基础功能并重写特定方法publicclassNewPhoneextendsOldPhone{// 使用 Override 注解显式指定重写让编译器帮忙校验方法签名OverridepublicvoidshowCaller(){// 可以通过 super 调用父类原有的核心逻辑可选super.showCaller();// 扩展新的特定行为外壳不变核心重写System.out.println(来电显示显示地区 (例如广东深圳));System.out.println(来电显示显示姓名和头像);}}publicclassTestPhone{publicstaticvoidmain(String[]args){System.out.println(--- 使用老手机 ---);OldPhoneoldPhonenewOldPhone();oldPhone.showCaller();System.out.println(\n--- 使用新手机 ---);// 这里体现了多态和重写的好处子类实现了特定于自己的行为NewPhonenewPhonenewNewPhone();newPhone.showCaller();System.out.println(\n--- 向上转型触发多态 ---);// 父类引用指向子类对象运行时会动态绑定到子类重写的方法OldPhoneupcastPhonenewNewPhone();upcastPhone.showCaller();}}输出结果— 使用老手机 —来电显示显示号码— 使用新手机 —来电显示显示号码来电显示显示地区 (例如广东深圳)来电显示显示姓名和头像— 向上转型触发多态 —来电显示显示号码来电显示显示地区 (例如广东深圳)来电显示显示姓名和头像向上转型和向下转型向上转型实际就是创建一个子类对象将其当成父类对象来使用语法格式父类类型 对象名 new 子类类型()AnimalanimalnewCat(元宝,2);animal 是父类类型但可以引用一个子类对象因为是从小范围向大范围的转换使用场景直接赋值方法传参方法返回publicclassTestAnimal{// 方法传参:形参为父类型引用,可以接收任意子类的对象publicstaticvoideatFood(Animala){a.eat();}// 作返回值:返回任意子类对象publicstaticAnimalbuyAnimal(Stringvar){if(狗.equals(var)){returnnewDog(狗狗,1);}elseif(猫.equals(var)){returnnewCat(猫猫,1);}else{returnnull;}}publicstaticvoidmain(String[]args){AnimalcatnewCat(元宝,2);// 直接赋值:子类对象赋值给父类对象DogdognewDog(小七,1);eatFood(cat);eatFood(dog);AnimalanimalbuyAnimal(狗);animal.eat();animalbuyAnimal(猫);animal.eat();}}向上转型的优点让代码实现更简单灵活向上转型的缺陷不能调用到子类特有的方法向下转型将一个子类对象经过向上转型之后当成父类方法使用再无法调用子类的方法但有时候可能需要调用子类特有的方法此时将父类引用再还原为子类对象即可即向下转换publicclassTestAnimal{publicstaticvoidmain(String[]args){CatcatnewCat(元宝,2);DogdognewDog(小七,1);// 向上转型Animalanimalcat;animal.eat();animaldog;animal.eat();// 编译失败Animal类没有bark方法// animal.bark();// 强制转换报错当前animal实际是Dog不能转Cat抛出ClassCastException// cat (Cat)animal;// cat.mew();// 当前animal实际对象为Dog强转Dog安全dog(Dog)animal;dog.bark();}}向下转型用的比较少而且不安全万一转换失败运行时就会抛异常 。Java中为了提高向下转型的安全性引入了instanceof如果该表达式为 true则可以安全转换publicclassTestAnimal{publicstaticvoidmain(String[]args){CatcatnewCat(元宝,2);DogdognewDog(小七,1);// 向上转型Animalanimalcat;animal.eat();animaldog;animal.eat();if(animalinstanceofCat){cat(Cat)animal;cat.mew();}if(animalinstanceofDog){dog(Dog)animal;dog.bark();}}}多态的优缺点多态的优点能够降低代码的“圈复杂度”避免使用大量的 if-else“圈复杂度”是衡量一段代码逻辑复杂程度的指标核心反映代码分支、循环的多少例如我们现在需要打印的不是一个形状了而是多个形状如果不基于多态实现代码如下publicstaticvoiddrawShapes(){RectrectnewRect();CyclecyclenewCycle();FlowerflowernewFlower();String[]shapes{cycle,rect,cycle,rect,flower};for(Stringshape:shapes){if(shape.equals(cycle)){cycle.draw();}elseif(shape.equals(rect)){rect.draw();}elseif(shape.equals(flower)){flower.draw();}}}如果使用使用多态则不必写这么多的 if-else 分支语句代码更简单publicstaticvoiddrawShapes(){// 创建Shape对象数组Shape[]shapes{newCycle(),newRect(),newCycle(),newRect(),newFlower()};for(Shapeshape:shapes){shape.draw();}}可扩展能力更强 如果要新增一种新的形状使用多态的方式代码改动成本也比较低classTriangleextendsShape{Overridepublicvoiddraw(){System.out.println(△);}}对于类的调用者来说drawShapes 方法只要创建一个新类的实例就可以了改动成本很低 。而对于不用多态的情况就要把 drawShapes 中的 if-else 进行一定的修改改动成本更高多态的缺点无法直接调用子类特有的方法核心缺陷问题 当把子类对象当成父类对象使用向上转型时不能调用到子类特有的方法代价 如果必须调用子类特有方法就必须要额外进行向下转型强制类型转换// 父类classAnimal{publicvoideat(){System.out.println(动物在吃东西);}}// 子类 DogclassDogextendsAnimal{// 1. 重写父类的方法Overridepublicvoideat(){System.out.println(狗在啃骨头);}// 2. 子类特有的方法publicvoidbark(){System.out.println(汪汪汪);}}// 子类 CatclassCatextendsAnimal{Overridepublicvoideat(){System.out.println(猫在吃鱼);}// 子类特有的方法publicvoidcatchMouse(){System.out.println(猫在抓老鼠);}}publicclassTestPolymorphism{publicstaticvoidmain(String[]args){// 【向上转型】父类引用指向子类对象AnimalmyAnimalnewDog();// 成功调用的是被重写的方法myAnimal.eat();// 输出: 狗在啃骨头// 报错尝试调用子类特有的方法// myAnimal.bark(); // 编译错误Cannot resolve method bark in Animal}}向下转型存在类型安全风险问题 向下转型是不安全的。如果转换的目标类型和真实指向的对象类型不匹配在程序运行时会直接抛出 ClassCastException 异常代价 为了保证安全性必须编写额外的冗余代码使用 instanceof 关键字进行判断验证后方可转换// 基础类结构classAnimal{publicvoideat(){System.out.println(动物在吃东西);}}classDogextendsAnimal{publicvoidbark(){System.out.println(汪汪汪);}}classCatextendsAnimal{publicvoidmew(){System.out.println(喵喵喵~);}}publicclassTestDowncastDanger{publicstaticvoidmain(String[]args){// 【第一步向上转型安全】// 内存中实际创建的是一只狗(Dog)但我们用动物(Animal)的引用指向它AnimalmyAnimalnewDog();// 【第二步危险的向下转型】// 编译器在检查时发现 myAnimal 是一个 Animal而 Cat 也是继承自 Animal。// 编译器心想“虽然不知道这具体是个什么动物但它确实有可能是只猫语法没毛病放行”// 于是编译成功通过没有标红报错。// 但是当程序跑起来时JVM去内存里一看“这明明是只狗你非要把它强转成猫”// 啪直接抛出异常程序崩溃。CatmyCat(Cat)myAnimal;myCat.mew();}}标准解决方案使用instanceof护航publicclassTestDowncastSafe{publicstaticvoidmain(String[]args){AnimalmyAnimalnewDog();// 实际是一只狗// 转型前先用 instanceof 探明真身if(myAnimalinstanceofCat){// 如果内存里真的是一只猫才进行强转CatmyCat(Cat)myAnimal;myCat.mew();}elseif(myAnimalinstanceofDog){// 发现真身是一只狗于是安全强转为狗System.out.println(扫描确认这是一只狗准备强转...);DogmyDog(Dog)myAnimal;myDog.bark();// 安全调用子类特有方法}else{System.out.println(未知的动物类型);}}}通过 instanceof 的保护完美避开了 ClassCastException 异常保证了代码的健壮性构造函数中触发动态绑定的“隐形陷阱”问题 如果在父类的构造方法中调用了被子类重写的方法会触发动态绑定直接调用子类的方法风险 此时子类对象还没有构造完成成员变量尚未初始化仍为默认值如 0 或 null这会导致子类方法在执行时拿到错误的数据或引发异常// 父类classBase{publicBase(){// 隐形陷阱在父类构造函数中调用了会被子类重写的方法System.out.println(1. 进入 Base 构造函数准备调用 func());func();}publicvoidfunc(){System.out.println(Base.func());}}// 子类classDerivedextendsBase{// 子类成员变量显式初始化为 1privateintnum1;publicDerived(){// 这里隐含了 super(); 会先调用父类构造函数System.out.println(4. 进入 Derived 构造函数此时 num num);}Overridepublicvoidfunc(){// 由于动态绑定父类构造函数中调用的 func() 实际上会执行这里System.out.println(2. 执行 Derived 重写后的 func());System.out.println(3. 此时读取到的 num 值 num);}}// 测试入口publicclassTestTrap{publicstaticvoidmain(String[]args){System.out.println(*** 开始创建子类 Derived 对象 ***);DeriveddnewDerived();}}输出结果*** 开始创建子类 Derived 对象 ***1.进入 Base 构造函数准备调用 func()2.执行 Derived 重写后的 func()3.此时读取到的 num 值 04.进入 Derived 构造函数此时 num 1执行new Derived()创建子类 Derived 对象时会优先隐式调用父类 Base 的无参构造方法进入 Base 构造方法打印提示语句后调用 func () 方法由于 Java 方法动态绑定机制即便在父类构造器中调用实际执行的是子类 Derived 重写后的 func 方法此时子类 Derived 的对象还未完成完整初始化子类成员变量num的显式赋值逻辑要等父类构造全部执行完毕后才会执行当前num仅为 int 类型默认初始值 0执行子类重写的 func 方法打印对应提示并输出未初始化完成的 num 值 0父类 Base 构造方法执行完毕回到子类 Derived 自身构造方法此时成员变量num 1初始化完成打印构造内提示语句与 num 最终值 1结论在父类构造函数中调用可被子类重写的普通实例方法会触发动态绑定提前执行子类重写逻辑但子类的成员变量、构造代码尚未初始化完成成员变量会读取到默认初始值产生不符合预期的数据引发隐蔽 bug。因此构造器中尽量不要调用可重写的实例方法若必须调用仅可使用 private、final 修饰的方法无法被子类重写不会触发动态绑定**多态的核心思想是同一行为在不同对象上呈现不同表现Java 里多态分为编译时多态方法重载静态绑定和运行时多态方法重写动态绑定我们常说的运行时多态必须同时满足三个条件存在继承关系、子类重写父类可重写实例方法、使用父类引用调用重写方法它以向上转型作为代码载体依靠动态绑定机制在运行时识别对象真实类型并执行对应子类方法。合理运用多态既能大幅降低代码圈复杂度省去大量冗余 if-else 判断也能增强程序扩展性新增子类无需修改原有业务代码同时使用时要注意两点向上转型后若要调用子类独有方法需向下转型且必须配合instanceof判断防止类型转换异常另外不要在构造方法中调用可重写的实例方法否则子类对象未初始化完成会读取到变量默认值产生隐蔽 bug