这是我参与 8 月更文挑战的第 20 天,活动详情查看: 8月更文挑战
复用代码是 Java 众多引人注目的功能之一,如何实现对已有类更加灵巧的复用是程序设计中需要考虑的重要问题。本文是在研读 Thinking in Java 复用类一章后的思考与总结。
复用的方法
要使用现有类而不破坏现有代码主要有两种方式:组合与继承。前者直接在新的类中产生现有类的对象,后者则是在现有类的形式中添加代码。二者均是利用现有类型产生新类型,均在新类中包含了现有类的引用,对于组合这种引用是显式的,而对于继承则是隐式的。
组合
在新类中显式地产生现有类的对象,新类是由现有类组合而成的。现有类的对象默认初始化为 null,若我们需要进行初始化,有以下四种方式
- 在定义对象的地方
- 在类的构造器中
- 使用对象前进行惰性初始化:当不必要每次都生成对象时,使用惰性初始化可以减少负担
- 使用实例初始化:实例初始化是在类内部的一个代码块,在创建对象时执行,为该类的实例对象赋值
继承
使用 extends 关键字实现类的继承,子类会自动得到超类中可以继承的所有的域和方法。可以认为在子类中包含了一个超类的引用 super,在子类中可以通过 super 关键字可以使用超类。
要明确的是,在继承中 JVM 先加载超类,然后加载子类。创建子类对象时,会在子类对象的构造器中默认调用超类的无参构造器 super() (若要使用有参构造器则必须进行覆盖)。这个超类对象以 super 关键字来指代。
一个有继承关系的子类如上图所示,其中包括了子类特有的方法,子类重写超类中的方法和超类中提供的调用接口。超类中提供的接口会自动被子类继承,若要在子类中调用现有类的方法需要使用 super 进行指代。
代理
代理是一种复用类的设计模式,是继承与组合的中庸之道,Java 中并没有直接提供对代理的支持。代理的思想是建立一个包含了现有对象(目标对象)的代理对象,在代理对象中对目标对象进行包装和扩展。即对目标类加一层包装,对于目标对象的方法进行选择性的提供,这样就可以让用户调用代理对象来避免其使用目标对象中不希望暴露给使用者的方法。
组合与继承的选择
组合与继承是实现复用类功能的两个主要方式,我们常听说设计模式中强调少用继承来降低程序的耦合性,因此在选择组合与继承时常常偏向于组合。但我们需要理解组合与继承的设计思想,才能做出更好的选择。
- 组合的意义在于使用现有类的功能能让新类产生新的功能,新类的用户只能看到新类定义的接口。
- 继承的意义在于使用一个通用类,为了需要可以将其特殊化,新类与现有类是 is-a 关系。
另外,如果必须实现向上转型,那么继承是必要的
总结
继承和组合都能从现有类型中产生新的类型,组合将现有类型作为新类型底层实现的一部分加以复用,而继承复用的是接口。对于继承、组合或代理的选择需要综合程序耦合性、灵活性、是否需要多态机制等多方面考虑,需要理解这几种方式的设计思想才能做出更好的选择。