Q9:如何让表达“是一个”和“有一个”关系?或者请解释下“继承”和“组合”。组合和聚合之间有什么区别?
A9:“是一个”的关系表示继承而“有一个”的关系是表示组合。继承和组合都允许你将子对象放入新类中。代码重用的两个主要技术便是类继承和对象组合。
继承是单向的。例如房子是一栋建筑,但建筑不是一个房子。继承使用extends关键字。
组合:用于表达房子有一个浴室。说房子是一个浴室就不准确了。组合简单地使用实例变量引用其他对象,如House类拥有一个实例变量,引用一个Bathroom对象。
Q:哪一个更好?组合还是继承?
A:指南是,仅当子类“是一个”父类时,才使用继承。
Q:组合和聚合有什么区别?
A:它们都表达整体和局部的关系。聚合关系,局部可以独立于整体而存在。例如,一个行项目和产品是整体和局部的关系。如果一个行项目被删除,对应的产品不需要被删除。所以聚合是一种较弱的关系。组合关系,局部不可独立于整体而存在。如果一个整体被删除,那么所有零件也会被删除。例如,订单和项目是整体和局部的关系。如果一个订单被删除,那么相应的行项目也应该被删除。所以组合具有更强的关系。
Q10:你怎么理解继承、封装、多态和动态绑定?
A10:Polymorphism多态——描述了这样一种能力,一个给定类型的变量可以被用来引用多个类型不同的对象(当然,需要这些类型是给定类型的子类),而调用的却是这个变量引用的对象的具体类型上的方法。简而言之,多态是一种自下而上的方法调用。多态的好处是,很容易添加新的扩展类而不破坏原有的调用代码。当给一个对象发送一条消息(调用方法)时,你甚至不知道这个对象的具体类型,但是正确的行为会发生,这就是多态。
面向对象编程语言实现多态的过程称为动态绑定。(运行期类型推断。)
Inheritance继承——是将基类的行为(即方法)和状态(即变量)包含到派生类中,这样它们就可在派生类中被访问了。关键的好处是,它提供了代码重用的正式机制。
任何业务逻辑的公共部分都可以从派生类移至基类,在重构时这样做,可以避免代码重复而提高代码的可维护性。
现有的类被称为基类而派生类被称为子类。继承也可以被定义为一个过程,即对象获得一个或多个其他对象的特征的过程,就像孩子从父母那里获得特征一样。
有两种类型的继承:
1、实作继承:可以通过继承部分或全部父类中已经实现的功能来扩展程序。在Java中,您只可以从一个超类继承。实作继承提升了重用性,但是不正确的继承使用可能导致编程噩梦,因为它会破坏封装性并且为将来的变化带来问题。使用实作继承,子类变得和父类紧密耦合起来。这将使得设计变得脆弱,如果你想改变父类,就不得不了解子类的细节以免破坏他们。所以使用实作继承,确保子类只依赖父类的行为,而不是实际的实现。
2、接口继承:接口提供了一种机制,将无关的类联系起来——通过指定系列普通方法,这些实现类都必须包含。(实现类之间可以是互不相关的。)接口继承提升了“面向接口编程而不是面向实现编程”的原则。这样降低了系统之间的耦合。在Java中,你可以实现任意数量的接口。这比“实作继承”更灵活,因为它不会把你锁定在具体实现中,具体实现会使子类变得难以维护。也要小心,修改接口会破坏实现类。
Which one to use?优先选择接口,因为它符合“面向接口编程”的理念并且可以降低耦合。接口继承可以在对象组合的帮助下实现代码的重用。如果你看GOF设计模式,你会发现他们更偏爱接口继承而不是实作继承。
实作继承案例:
package ch08_extends3; /** * 假设活期存款和定期存款在存取行为上有类型的行为,我们把这两个行为的实现定义在父类中。 * <p>但是活期存款和定期存款在计算利息这个行为上表现是不同的。 * @author zhengwei 2013-7-13 */ public abstract class Account { public void deposit (double amount) { System.out.println("depositing " + amount); } public void withdraw (double amount) { System.out.println ("withdrawing " + amount); } public abstract double calculateInterest (double amount); } class SavingsAccount extends Account { public double calculateInterest (double amount) { // calculate interest for SavingsAccount return amount * 0.03; } public void deposit (double amount) { super.deposit (amount); // get code reuse // do something else } public void withdraw (double amount) { super.withdraw (amount); // get code reuse // do something else } } class TermDepositAccount extends Account { public double calculateInterest (double amount) { // calculate interest for SavingsAccount return amount * 0.05; } public void deposit(double amount) { super.deposit (amount); // get code reuse // do something else } public void withdraw(double amount) { super.withdraw (amount); // get code reuse // do something else } }
接口继承案例:
package ch08_extends3; /** * 接口继承示例代码,使用组合来重用代码。 * <p>在下例中,deposite和withdraw方法共享了AccountHelper中的代码片段。 * <p>而calculateInterest方法在各自实现中有独特的实现 * @author zhengwei 2013-7-13 */ public interface Account { public abstract double calculateInterest(double amount); public abstract void deposit(double amount); public abstract void withdraw(double amount); } interface AccountHelper { public abstract void deposit(double amount); public abstract void withdraw(double amount); } /** * class AccountHelperImpl has reusable code as methods deposit (double amount) * and withdraw (double amount). * <p>AccountHelperImpl含有可重用代码:deposit方法和withdraw方法 */ class AccountHelperImpl implements AccountHelper { public void deposit(double amount) { System.out.println("depositing " + amount); } public void withdraw(double amount) { System.out.println("withdrawing " + amount); } } class SavingsAccountImpl implements Account { // composed helper class (i.e. composition ). AccountHelper helper = new AccountHelperImpl(); public double calculateInterest(double amount) { // calculate interest for SavingsAccount return amount * 0.03; } public void deposit(double amount) { helper.deposit(amount); // code reuse via composition } public void withdraw(double amount) { helper.withdraw(amount); // code reuse via composition } } class TermDepositAccountImpl implements Account { // composed helper class (i.e. composition ). AccountHelper helper = new AccountHelperImpl(); public double calculateInterest(double amount) { // calculate interest for SavingsAccount return amount * 0.05; } public void deposit(double amount) { helper.deposit(amount); // code reuse via composition } public void withdraw(double amount) { helper.withdraw(amount); // code reuse via composition } }
两种方式可以使用如下的测试代码:
package ch08_extends3; /** * * @author zhengwei 2013-7-13 */ public class Test { public static void main(String[] args) { Account acc1 = new SavingsAccountImpl(); acc1.deposit(50.0); Account acc2 = new TermDepositAccountImpl(); acc2.deposit(25.0); acc1.withdraw(25); acc2.withdraw(10); double cal1 = acc1.calculateInterest(100.0); double cal2 = acc2.calculateInterest(100.0); System.out.println("Savings --> " + cal1); System.out.println("TermDeposit --> " + cal2); } }
输出结果:
depositing 50.0
depositing 25.0
withdrawing 25.0
withdrawing 10.0
Savings --> 3.0
TermDeposit --> 5.0
问:为什么优先通过组合来重用代码而不是继承?
答:可以看到两种方式都可利用多态,并重用了代码,结果也是一致的,但是:
public class EfficientAccountHelperImpl implements AccountHelper { public void deposit(double amount) { System.out.println(" efficient depositing " + amount); } public void withdraw(double amount) { System.out.println(" efficient withdrawing " + amount); } }
译注:感觉这里没说透。我来说下这个问题,“父母是不可以动态替换的,但是朋友可以是动态替换的”。一旦继承了某类,你不可能替换这种继承关系,但是组合,我们可以通过向构造器传参或者setter方法临时改变组装进来的对象的类型,当然前提是这些对象的类型否和依赖的接口类型。
再进一步,重用代码要么用super.someMethod(),这个super指向父类对象,这个super你是没法换的。要么是通过brother.someMethod()重用代码,这个brother是组合语法中的域成员,它指向和我们协作的,或者说依赖的对象,这种对象只需一个set方法就可替换了。
还是我上面说的那点。
因为对象组合的灵活性和强大,大部分设计模式只要有可能,就优先强调对象组合而非继承。很多时候,一个设计模式使用组合就展示了一个聪明的办法来解决一类常见问题,而不是用标准的、不那么灵活的基于继承的解决方案。
Encapsulation封装——指的是保持所有相关成员(变量和方法)在一起,在一个对象中。指定成员变量为私有可以隐藏变量和方法。对象应该向外界隐藏他们的内部运作。好的封装提高代码模块化,通过防止对象以一种意想不到的方式相互作用,从而使未来的开发和重构工作更容易。
示例代码:
class MyMarks { private int vmarks = 0; private String name; public void setMarks(int mark) throws MarkException { if (mark > 0) this.vmarks = mark; else { throw new MarkException("No negative Values"); } } public int getMarks() { return vmarks; } // getters and setters for attribute name goes here. }
能够封装类的成员对于安全性和完整性来说是极其重要的。我们可以保护变量接收不合法的值。上面的示例代码描述了如何通过封装来保护MyMarks不拥有负值。任何修改成员变量”vmarks”的行为必须通过setter方法setMarks(int)。这可以防止对象”MyMarks”拥有负值,调用者传入负值将得到一个异常。