📑前言
在面向对象编程中,向上转型和向下转型是常用的技术手段,可以实现不同类之间的转换和灵活应用。同时,多态作为面向对象编程的重要特性,具有诸多优点和缺陷,对代码的设计和性能都有一定影响。本文将深入探讨向上转型、向下转型以及多态的优缺点,帮助读者更好地理解和运用这些概念在Java编程中的实际应用和注意事项。
一、向上转型和向下转型
1.1 向上转型
实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
Animal animal = new Cat(“小小”,3);
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
【使用场景】
- 直接赋值
- 方法传参
- 方法返回
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) {
// 1. 直接赋值:子类对象赋值给父类对象
Animal cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
**向上转型的优点:**让代码实现更简单灵活。
**向上转型的缺陷:**不能调用到子类特有的方法。
1.2 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
public static void main(String[] args) {
Animal animal1 = new Dog("小七",7);//向上转型
Animal animal2 = new Cat("咪咪",3);//向上转型
animal1 = (Dog)animal1;//向下转型
animal2 = (Cat)animal1;//向下转型编译报错,因为animal1实际上是指向的是Dog,强制转化为Cat无法还原
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
二、多态的优缺点
2.1 多态优点
- 提高代码的可扩展性和可维护性:通过多态,可以将具体的实现与抽象的接口分离,使得系统的各个模块之间的耦合度降低,从而方便对系统进行扩展和维护。
- 增强代码的灵活性:通过多态,可以在运行时动态地决定对象的具体类型,从而实现不同对象的不同行为。这样可以根据实际需求灵活地进行对象的选择和使用。
- 提高代码的可读性和可理解性:通过多态,可以将对象的具体类型隐藏起来,只关注对象的抽象类型和接口,从而使得代码更加简洁、清晰,易于理解和阅读
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
扩展:
- 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
- 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”。如果一个方法的圈复杂度太高, 就需要考虑重构,不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 。
class Book{
public void read(){
System.out.println("看书");
}
}
class English extends Book{
public void read(){
System.out.println("看英语书");
}
}
class Maths extends Book{
public void read(){
System.out.println("看数学书");
}
}
class Language extends Book{
@Override
public void read() {
System.out.println("看语文书");
}
}
public class Exercise3 {
public static void readBook(){
English english = new English();
Maths maths = new Maths();
Language language = new Language();
Book[] book = {english,language,maths,language,maths,english};
for (Book x:book) {
x.read();
}
}
public static void main(String[] args) {
readBook();
}
}
2.2 多态缺陷
性能损失:由于多态需要在运行时进行类型的判断和方法的动态绑定,所以会带来一定的性能损失。相比于直接调用具体类型的方法,多态需要进行额外的判断和查找,从而导致一定的性能下降。
可能引发运行时错误:由于多态是在运行时动态决定对象的具体类型,所以如果在使用多态的过程中出现了类型错误或者类型转换错误,就会导致运行时错误的发生。这就需要在使用多态时进行严格的类型检查和错误处理,增加了代码的复杂性和难度。
可能导致代码的混乱和难以理解:多态的使用会使得代码中出现更多的抽象和接口,从而增加了代码的复杂性和难度。如果使用不当,可能会导致代码的混乱和难以理解,降低代码的可读性和可维护性。因此,在使用多态时需要谨慎设计和使用。
三、避免避免构造方法中调用重写的方法
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
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func。
构造 D 对象的同时, 会调用 B 的构造方法.
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
四、好的习惯
“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
🌤️全篇总结
向上转型可以实现子类对象当成父类对象使用,提高代码的灵活性和复用性;向下转型则可以让父类引用还原为子类对象,调用子类特有方法。多态能够提高代码的可扩展性和可维护性,但也存在性能损失和潜在的错误风险,需要谨慎使用和设计。
避免在构造函数中调用重写方法,保持构造过程简洁和清晰,避免潜在的问题。形成良好的编程习惯,能够提高代码的质量和可维护性,减少潜在的错误和调试成本,是每位Java程序员都应该注意的重要方面。