【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.ani
    b.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)的关系。

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值