Thinking in Java 读书笔记 第七章 复用类(继承、组合)

17 篇文章 0 订阅

章七 继承和组合

前言

  随着编程语言的发展,项目规模的日益增大,代码的复用愈加重要。程序语言始终朝着越来越抽象,越来越易用的方式发展;从汇编,到C语言,到OOP语言。在C语言时代,代码复制和函数是复用的主要途径,在OOP中,通过复用类,使得代码复用更加方便和自然。
  本章讲述了三种复用类的方式:组合继承代理。虽然继承是众所周知的OOP三大特性之一且是另一大特性多态的基础,但在实际使用中,继承很容易被滥用,组合往往代表着更好的实现。Java并不直接支持代理,但可以通过组合来模拟代理。本章的主要内容即为组合继承

1. 组合语法

  对于组合的语法,并没有什么好说的,因为创建类的域就是在不停的使用组合,只需将对象引用置于新的类中即可。下面举例说明对对象引用进行初始化的四种途径:

public class Bath {
  private String s1 = "Happy",s2, s3, s4;// Way 1:Initializing at point of definition:
  public Bath() {
    s2 = "New";  // Way 2: Initializing in the constructor
  } 
  { s3= "Year"; }  // Way 3: Instance initialization:
  public String toString() {
    if(s4 == null) // Way 4: Delayed initialization:
      s4 = "Everybody";
    return
      "s1 = " + s1 + "\n" +
      "s2 = " + s2 + "\n" +
      "s3 = " + s3 + "\n" +
      "s4 = " + s4 + "\n" +
  } 
}

  如上述程序所示,可以在程序的四个位置初始化对象引用:

  • 在定义对象的地方,则其可以在构造器调用之前被初始化
  • 在构造器中初始化
  • 实例初始化
  • 惰性初始化,在实际使用时才初始化

2.继承语法

  继承是所有OOP语言中不可缺少的组成部分。需要注意的是,与C++不同,Java存在标准根类Object。也就是说,在创建类时,总是使用了继承,要么是显示继承,要么是隐式继承Object
  继承通过使用关键字extends实现,通过继承,可以自动得到基类中所有的域和方法,Java不支持多继承。为了方便继承,一般的规则是将所有的域指定为private,将所有的方法指定为public或protected。在子类中,可以用super关键字调用父类的方法。此外,在每个类中都设置一个main()有助于类的调试

初始化基类

   继承并不只是复制基类的接口。当创建一个子类的对象时,其内含了一个父类的子对象。因此,需要通过调用父类构造器对父类子对象进行初始化。若在子类构造器中未显示调用基类构造器,则编译器会自动调用基类的默认构造器对父类部分进行初始化。若基类没有默认构造器,或者需要调用其他含参构造器进行初始化,需要用super关键字进行显式初始化。

class Father{
    Father(int i){
        System.out.println("Father Constructor");
    }
}

class Son extends Father{
    Son(int j){
        super(j);
        System.out.println("Son Constructor");  
    }
}

  如上述代码所示,super()语句必须位于子类构造器中的第一句。

3.代理

  Java中并不直接支持代理,但可以用组合实现代理的效果。代理一般用于当两个类不符合继承的关系,但是又想在一个类中取得对另一个类方法的直接使用权(一般只有子类可以),书中用SpaceShip做了一个很好的示例:

public class SpaceShipControls {
  void up(int velocity) {}
  void down(int velocity) {}
  void left(int velocity) {}
  void right(int velocity) {}
  void forward(int velocity) {}
  void back(int velocity) {}
  void turboBoost() {}
} ///:~
public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls =
    new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  // Delegated methods:
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  }
  public static void main(String[] args) {
    SpaceShipDelegation protector =
      new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
} ///:~

  如上述程序所示,通过使用代理,将组合使用出了继承的效果,取得了与成员完全一致的接口。另外,使用代理可以选择该成员的方法的一个子集,选择余地较直接继承大。

4. 组合使用继承和组合

  这种应用场景很常见,需要注意的是构造顺序:首先执行父类初始化,然后执行对象引用在定义处的初始化(包括实例初始化),最后执行子类构造器。父类和引用对象成员存在引用对象和父类的时候,顺序同上,形成树状结构。
  另一点需要注意的是,编译器只监督必须初始化基类,但并不监督对成员对象引用的初始化,可以会错误使用未经初始化的成员对象引用。

确保正确清理

  如第五章所述,因为Java中没有析构函数,所以清理工作需要由程序员自己定义,这里需要注意两点。一是需要将清理语句放置于try-catch-finally语句中,以确保其能被正确执行。二是要确保清理与构建的顺序完全相反,如析构函数一样,在Java中,只能靠程序员本身提供这种保证,下面是示例程序。

public class CADSystem extends Shape {
  private Circle c;
  private Triangle t;
  private Line[] lines = new Line[3];
  public CADSystem(int i) {
    super(i + 1);
    for(int j = 0; j < lines.length; j++)
      lines[j] = new Line(j, j*j);
    c = new Circle(1);
    t = new Triangle(1);
    print("Combined constructor");
  }
  public void dispose() {
    print("CADSystem.dispose()");
    // The order of cleanup is the reverse
    // of the order of initialization:
    t.dispose();
    c.dispose();
    for(int i = lines.length - 1; i >= 0; i--)
      lines[i].dispose();
    super.dispose();   //dispose() executes exactly the opposite order of constructor
  }
  public static void main(String[] args) {
    CADSystem x = new CADSystem(47);
    try {
      // Code and exception handling...
    } finally {
      x.dispose(); // The clean work should be put in the finally-sentence
    }
  }
} 

名字屏蔽

  若Java中的父类含有某个被多次重载的函数,在子类中重新定义该方法名称并不会屏蔽父类中的任何版本(这点与C++不同)。这样更符合习惯,但容易引起函数重载函数覆盖之间的混淆,Java引入@Override注解来解决这一问题。通过对需要覆写的函数加上该注解,若是不小心写成重载则会报错。

5. 在组合和继承之间选择

  组合和继承都允许在新的类中放置子对象,组合是显式这样做,而继承则是隐式这样做。组合适用于想在新类中使用现有类的功能而非它的接口这种情形,可以通过加入现有类的一个private对象引用来实现。偶尔,会需要允许类的用户直接访问类中的组合成分,则需要将其成员对象设置为public。
  继承则适用于开发现有类的一个特殊版本,表达is-a的关系。需要说明的是,Java中的继承相当于C++中的共有继承。根据Effective C++书中的观点,Java中的继承和组合才是正确的复用方式。

6.再论protected关键字

  这里补充一下protected的一般使用方法:将父类中的域设置成private,再将对应的access routines设置为protected,从而方便子类访问和设置域,又实现了封装。

7. 向上转型

  继承最重要的方面是表现新类和基类之间的关系:新类是现有类的一种类型。

向上转型名字的由来

  在UML的类图中,子类位于父类的下方。将子类看做父类,在继承图上是向上移动的,故称为向上转型。因为向上转型是从一个专用类型向一个通用类型转化,所以很安全。考虑到子类是父类的超集,向上转型可能会丢失方法。向下转型将在下面的章节说明。

再论组合和继承

  最清晰的判断是用组合还是继承的方法是:询问自己是否需要向上转型。在需要的情况下再考虑继承

8. final关键字

  final通常用以说明无需改变,出于两种理由:设计效率。在三种情形下可能用到final:数据方法

final数据

  final数据一种应用情形是编译期常量,该常量必须是基本数据类型,由final修饰,在定义时初始化。由于计算在编译期完成,会减少运行期开销。
  对于基本类型,final使其数值恒定不变;对于对象引用,final使其引用恒定不变。Java中并未提供使对象本身恒定不变的技术(包括数组,数组也是对象)。
  空白final是指被声明为final但又初始值的空白final必须在该类的所有构造器中被执行初始化。
  **final参数**Java中可以通过把函数参数设置为final,使得无法在函数体中更改参数的值或其指向的对象

final方法

  使用final方法的原因有两个:一个防止继承类覆写;二是提高效率。随着JVM技术提高,二的情况基本不复存在,主要看原因一。

final和private

  所有的private方法都是隐式final的。因为private方法被完美隐藏,在新类中覆写private方法相对于添加新方法。

final类

  final类表示不能被继承的类,表示对该类的设计不变,或者出于安全不希望其有子类。
  最后,使用final需慎重。

9.初始化及类加载

  在Java中,每个类的编译代码都存在于其独立的文件中,该文件只有当需要使用其程序代码时才会被加载。总而言之,类是在其任意static元素(static域,static方法,构造器)被访问时加载的。加载按照基类、成员和子类的顺序进行。其中,成员对象引用仅在其在定义时被初始化或被实例初始化时被加载。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值