欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > 【Java17】组合

【Java17】组合

2025/2/22 16:31:12 来源:https://blog.csdn.net/wangxiaochaun/article/details/140389665  浏览:    关键词:【Java17】组合

继承是一把双刃剑,在实现代码复用的同时破坏了封装。

组合则在实现代码复用的同时,保留了原本类的封装性。

从面向对象的思想来看,有两个维度需要权衡:

  1. 客观世界的抽象;
  2. 软件工程的效率,即代码复用。

这两者一定程度上是有取舍的。

子类的访问权限

为保证父类有良好的封装性,不会被子类随意访问或修改,设计父类时建议遵循如下规则:

  • 尽量隐藏父类的内部数据,即成员变量。尽可能把父类的成员变量设置为private,不要让子类直接访问父类的成员变量;
  • 不要让子类随意访问、修改父类的方法。
    • 父类中那些辅助其他的工具方法,尽量使用private修饰,不让子类访问;
    • 父类中那些需要被外部类调用的方法,必须使用public修饰,但又不想让子类重写,可以再使用final修饰;
    • 如果希望父类某个方法被子类重写,但不希望被其他类自由访问,则使用protected修饰。
  • 尽量不在父类的构造器里调用将要被子类重写的方法。
class Base
{public Base(){test();}public void test(){System.out.println("将被子类重写的方法");}
}public class Sub extends Base
{private String name;public void test(){System.out.println("子类重写父类的方法" + name.length());}public static void main(String[] args){var s = new Sub(); // 空指针异常}
}
  • 第22行,实例化Sub对象时,首先会调用Base的构造器。此时,Base的构造器里调用了被子类重写后的方法,也就是第16行的test()。这时,对象的实例变量name是空指针,导致name.length()出现空指针异常。

通过两种方式可以把类设置为不能被其他类继承:

  1. 使用final修饰这个类,这种类叫最终类,不能被当成父类;
  2. 把这个类的所有构造器都修饰为private。对这种类,可提供一个静态方法,用于实例化该类的对象。

要避免滥用继承。什么时候适合从父类派生出子类呢?

  • 子类需要额外的成员变量。例如Person类没有提供”年级“这个field(以前叫属性,现在翻译成域),而Student类可以在继承Person的基础上派生出grade这个属性。
  • 子类需要增加自己独特的行为方式。Person类不一定都studying(),但是子类Studentstudying()

Good good study, day day up!

组合

如果只是出于代码复用的角度,使用组合更合适。

把一个类当做另一个类的组合(部件),从而允许新类直接复用该类的public方法。

组合的核心机制是把部件类的对象(实例)当做自己的成员变量,从而能驱使这个对象去调用部件类的public方法。从外部看,看到的是新类在调用,而不是部件类的方法,从而保证了封装性。此外,把这个”成员对象“修饰为private,可以更好保护其不被外部类直接修改。

从类复用的角度,部件类其实扮演了父类的角色,即将自己的方法提供给新类。

从继承的角度实现代码复用

Animal, Wolf, Bird这三个类,它们从继承关系来看如下图:

在这里插入图片描述

class Animal
{private void beat(){System.out.println("心跳");}public void breathe(){best(); // 类内部调用System.out.println("呼吸");}
}class Wolf extends Animal
{public void run(){System.out.println("奔跑");}
}class Bird extends Animal
{public void fly(){System.out.println("飞翔");}
}public class InheritTest
{public static void main(String[] args){var b = new Bird();b.breathe(); // 继承b.fly();var w = new Wolf();w.breathe();w.run();}
}
  • 通过继承,实现了对breathe()方法代码的复用。

从组合的角度实现代码复用

从组合的角度,这三类的关系如下图:
在这里插入图片描述

对应代码如下:

class Animal
{private void beat(){System.out.println("心跳");}public void breathe(){best(); // 类内部调用System.out.println("呼吸");}
}class Wolf extends Animal
{private Animal ani; // 把Animal类的对象当做成员public Wolf(Animal a){this.ani = a;}public breathe(){ani.breathe(); // 复用Animal类的方法}public void run(){System.out.println("奔跑");}
}class Bird extends Animal
{private Animal ani;public Bird(Animal a){this.ani = a;}public breathe(){ani.breathe();}public void fly(){System.out.println("飞翔");}
}public class CompositeTest
{public static void main(String[] args){// 首先要创建一个animal的对象var a1 = new Animal();var b = new Bird(a1); // 利用创建的Animal对象a1去初始化b里的成员变量b.anib.breathe();b.fly();//------var a2 = new Animal();var w = new Wolf(a2);w.breathe();b.run();}
}

第53行和第58行创建了两个Animal的实例a1,a2。如果用同一个实例来初始化b和w,会有问题吗?

在使用组合时,创建了两个Animal对象,是不是意味着组合的内存开销大?

不是。继承的开销也很大:在创建子类对象时,除为子类的实例变量分配空间,还要为父类的实例变量分配空间。

设父类有2个实例变量,子类有3个实例变量,则继承方式下,创建子类实例需要分配2+3=5块内存空间;

使用组合时,首先给部件类的对象分配2个内存空间,然后给整体类的对象分配3个内存空间。只不过这时有一个额外的引用变量来引用部件类的对象。

从这个角度看,二者的内存开销没有本质差别。

从抽象的角度看

从抽象的角度,上述类的关系更适合用继承来描述。继承表达”是“(is-a)的关系;组合则表达”有“(has-a)的关系。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词