《Java编程思想》读书笔记(三)

第七章 多形性

面向对象的程序设计语言的基本特征:数据抽象、继承、多形性

上溯造型

  1. 取得一个对象句柄,并将其作为基础类型句柄使用的行为叫做“上溯造型”,因为继承树的画法是基础类位于最上方

  2. 从衍生类到基础类的上溯造型可能“缩小”接口,但不可能变得比基础类还要小

  3. 如果能不顾衍生类,只让自己的代码和基础类打交道,省下来的工作量将是难以估计的

原书示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//: Music.java 
// Inheritance & upcasting
package c07;
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note middleC = new Note(0),
cSharp = new Note(1), cFlat = new Note(2);
} // Etc.
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} ///:~

示例中,Music.tune()接收一个Instrument句柄,也接收从Instrument衍生出来的所有东西。如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多,但如果这样做,每当增加一个衍生类时,就要再次为这个新衍生类构造一个tune()函数。

深入理解

  1. 对于上述的示例,假设Instrument有不止一个衍生类,比如Wind、Brass、String等,编译器怎样才能知道Instrment指向的是一个Wind而不是Brass或String,这里涉及到“绑定”
  2. 将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)
  3. “早期绑定”:在程序运行以前执行绑定;“后期绑定”:绑定在运行期间进行,以对象的类型为基础,也叫做“动态绑定”或“运行期绑定”
  4. Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final(防止其他人覆盖方法;“关闭”动态绑定)

覆盖(override)与过载(overload)

重写,也称为覆盖,是指派生类重写了基础类的方法;

重载,指一个类里面的方法名相同但是参数列表不同,编译器可以根据不同的参数来调用不同的方法实现重载

抽象类和抽象方法

  1. “抽象方法”属于一种不完整的方法,只含有一个声明,没有方法主体
  2. 包含了抽象方法的一个类叫做“抽象类”。如果一个类包含了一个或多个抽象方法,类就必须指定成abstract
  3. 抽象类的意图是为从它衍生出去的 所有类都创建一个通用接口
  4. 如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义,否则衍生类也会是抽象的,而且编译器会强迫我们用abstract关键字标识它的“抽象”本质
  5. 即使不包括任何abstract 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例
  6. 使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象

接口

  1. 接口(interface),可将其想象为一个“纯”抽象类,它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体

  2. 为了生成与一个特定的接口(或一组接口)相符的类,要使用 implements(实现)关键字,除此之外,其他工作都与继承极为相似

  3. 具体实现了一个借口后,就获得了一个普通的类,可用标准方式对其进行扩展

  4. 接口中的方法会被隐式的指定为 public abstract(只能是public abstract,其他修饰符会报错);接口中的变量会被隐式的指定为 public static final(只能是public,其他修饰符会报错)

  5. 可以上溯造型到一个接口,其会被认为是“普通类”

  6. 多重继承:可以从一个抽象或具体(没有抽象方法)的基础类继承,同时合并多个接口

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    //: Adventure.java
    // Multiple interfaces
    import java.util.*;
    interface CanFight {
    void fight();
    }
    interface CanSwim {
    void swim();
    }
    interface CanFly {
    void fly();
    }
    class ActionCharacter {
    public void fight() {}
    }
    class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
    public void swim() {}
    public void fly() {}
    }
    public class Adventure {
    static void t(CanFight x) { x.fight(); }
    static void u(CanSwim x) { x.swim(); }
    static void v(CanFly x) { x.fly(); }
    static void w(ActionCharacter x) { x.fight(); }
    public static void main(String[] args) {
    Hero i = new Hero();
    t(i); // Treat it as a CanFight
    u(i); // Treat it as a CanSwim
    v(i); // Treat it as a CanFly
    w(i); // Treat it as an ActionCharacter
    }
    } ///:~

    从中可以看到,Hero将具体类ActionCharacter 同接口CanFight,CanSwim 以及CanFly 合并起来。按这种形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。在类Adventure 中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的自变量使用,Hero会依次上溯造型到每一个接口

  7. 使用接口的重要原因:能上溯造型至多个基础类;防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口

  8. 如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类

  9. 利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。通常,我们只能对单独一个类应用 extends(扩展)关键字,但由于接口可能由多个其他接口构成,所以在构建一个新接口时,extends 可能引用多个基础接口,多个接口之间用逗号隔开

  10. 接口中定义的字段会自动具有 static 和final 属性。虽然是final的,但可初始化成非常数表达式;由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。字段并不是接口的一部分,而是保存在那个接口的static存储区域中

内部类

  1. 若想在除外部类非 static 方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外部类名.内部类名”

  2. 由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此,所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //: Parcel3.java
    // Returning a handle to an inner class
    package c07.parcel3;
    abstract class Contents {
    abstract public int value();
    }
    interface Destination {
    String readLabel();
    }
    public class Parcel3 {
    private class PContents extends Contents {
    private int i = 11;
    public int value() { return i; }
    }
    protected class PDestination implements Destination {
    private String label;
    private PDestination(String whereTo) {
    label = whereTo;
    }
    public String readLabel() { return label; }
    }
    public Destination dest(String s) {
    return new PDestination(s);
    }
    public Contents cont() {
    return new PContents();
    }
    }
    class Test {
    public static void main(String[] args) {
    Parcel3 p = new Parcel3();
    Contents c = p.cont();
    Destination d = p.dest("Tanzania");
    // Illegal -- can't access private class:
    //! Parcel3.PContents c = p.new PContents();
    }
    } ///:~

    在Parcel3 中:内部类PContents 被设为 private,所以除了 Parcel3 之外,其他任何东西都不能访问它;PDestination 被设为 protected,所以除了 Parcel3,Parcel3 包内的类,以及Parcel3 的继承者之外,其他任何东西都不能访问 PDestination。这意味着客户程序员对这些成员的认识与访问将会受到限制

  3. 普通(非内部)类不可设为private 或 protected——只允许 public 或者“友好的”

  4. 使用内部类的原因:

    (1) 我们准备实现某种形式的接口,使自己能创建和返回一个句柄;

    (2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案,同时不愿意把它公开

  5. 几种情况下的内部类:

    (1)在一个方法内定义内部类:内部类是该方法的一部分,不能从方法外部访问

    (2)在方法的一个作用域内定义的内部类:在定义这个内部类的作用域外,是不可访问它的

    (3)匿名内部类:

    若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //: Destination.java
    package c07.innerscopes;
    interface Destination {
    String readLabel();
    } ///:~

    //: Parcel8.java
    // An anonymous inner class that performs
    // initialization. A briefer version
    // of Parcel5.java.
    package c07.innerscopes;
    public class Parcel8 {
    // Argument must be final to use inside
    // anonymous inner class:
    public Destination dest(final String dest) {
    return new Destination() {
    private String label = dest;
    public String readLabel() { return label; }
    };
    }
    public static void main(String[] args) {
    Parcel8 p = new Parcel8();
    Destination d = p.dest("Tanzania");
    }
    } ///:~

    假如需要在匿名内部类中采取一些类似于构建器的行动,要使用实例初始化模块,即匿名内部类的构建器,不能对实例初始化模块进行重载处理

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //: Parcel9.java
    // Using "instance initialization" to perform
    // construction on an anonymous inner class
    package c07.innerscopes;
    public class Parcel9 {
    public Destination dest(final String dest, final float price) {
    return new Destination() {
    private int cost;
    // Instance initialization for each object:
    {
    cost = Math.round(price);
    if(cost > 100)
    System.out.println("Over budget!");
    }
    private String label = dest;
    public String readLabel() { return label; }
    };
    }
    public static void main(String[] args) {
    Parcel9 p = new Parcel9();
    Destination d = p.dest("Tanzania", 101.395F);
    }
    } ///:~
  6. 一个内部类可以访问封装类的成员。内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类,随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成(???)

  7. 内部类的对象默认持有创建它的那个封装类的一个对象的句柄

  8. static 内部类意味着:

    (1)为创建一个 static 内部类的对象,我们不需要一个外部类对象;

    (2)不能从 static 内部类的一个对象中访问一个外部类对象

    由于static 成员只能位于一个类的外部级别,所以内部类不可拥有static 数据或static 内部类

    倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。

  9. 我们不在一个接口里设置任何代码,但 static 内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规——static 内部类只位于接口的命名空间内部

  10. 引用外部类对象:必须利用外部类的一个对象生成内部类的一个对象。除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。然而,如果生成一个static 内部类,就不需要指向外部类对象的 一个句柄

  11. 从内部类继承:必须在构建器中采用下述语法:

    enclosingClassHandle.super();

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //: InheritInner.java 
    // Inheriting an inner class

    class WithInner {
    class Inner {}
    }

    public class InheritInner extends WithInner.Inner {
    //! InheritInner() {} // Won't compile
    InheritInner(WithInner wi) {
    wi.super();
    }
    public static void main(String[] args) {
    WithInner wi = new WithInner();
    InheritInner ii = new InheritInner(wi);
    }
    } ///:~
  12. 内部类的覆盖:默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。当从外部类继承的时候,内部类不会被覆盖

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //: BigEgg.java 
    // An inner class cannot be overriden
    // like a method

    class Egg {
    protected class Yolk {
    public Yolk() {
    System.out.println("Egg.Yolk()");
    }
    }
    private Yolk y;
    public Egg() {
    System.out.println("New Egg()");
    y = new Yolk();
    }
    }

    public class BigEgg extends Egg {
    public class Yolk {
    public Yolk() {
    System.out.println("BigEgg.Yolk()");
    }
    }
    public static void main(String[] args) {
    new BigEgg();
    }
    } ///:~

    结果:

    1
    2
    New Egg() 
    Egg.Yolk()

    如果“明确”地继承了内部类,且覆盖了它的方法,就会使用被覆盖的版本

    原书示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    //: BigEgg2.java 
    // Proper inheritance of an inner class

    class Egg2 {
    protected class Yolk {
    public Yolk() {
    System.out.println("Egg2.Yolk()");
    }
    public void f() {
    System.out.println("Egg2.Yolk.f()");
    }
    }
    private Yolk y = new Yolk();
    public Egg2() {
    System.out.println("New Egg2()");
    }
    public void insertYolk(Yolk yy) { y = yy; }
    public void g() { y.f(); }
    }

    public class BigEgg2 extends Egg2 {
    public class Yolk extends Egg2.Yolk {
    public Yolk() {
    System.out.println("BigEgg2.Yolk()");
    }
    public void f() {
    System.out.println("BigEgg2.Yolk.f()");
    }
    }
    public BigEgg2() { insertYolk(new Yolk()); }
    public static void main(String[] args) {
    Egg2 e2 = new BigEgg2();
    e2.g();
    }
    } ///:~

    结果:

    1
    2
    3
    4
    5
    Egg2.Yolk() 
    New Egg2()
    Egg2.Yolk()
    BigEgg2.Yolk()
    BigEgg2.Yolk.f()
  13. 内部类的标识符:每个类都会生成一个.class文件,内部类也会生成.class文件,这些.class文件的命名遵循一种严格的形式:封装类名$内部类名.class,如由InheritInner.java文件就会生成三个.class文件:

    InheritInner.class

    WithInner$Inner.class

    WithInner.class

构建器和多形性

  1. 基础类的构建器肯定在一个衍生类的构建器中调用,且逐渐向上链接,使每个类使用的构建器都能得到调用

  2. 对于一个复杂的对象,构建器的调用遵照如下顺序:

    (1)调用基础类构建器。首先得到的是根部基础类的构建器,不断重复直到抵达最深一层的衍生类;

    (2)按照声明顺序调用成员初始化模块;

    (3)调用衍生类构建器的主体

  3. 如果在基础类中设计了某个特殊的清除进程,即finalize()方法,那么在衍生类中必须覆盖这个方法,覆盖finalize()时,务必调用基础类的finalize()方法

  4. 设计构建器时的规则:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一安全的调用是final方法和private方法,因为这些方法不会被覆盖

通过继承进行设计

  1. 用继承表达行为间的差异,用成员变量表达状态的变化

  2. 纯继承与扩展:只有在基础类或接口中已建立的方法,才可以在衍生类中被覆盖,即衍生类与基础类拥有同样的接口,使用时无需知道子类的任何额外信息

  3. “下溯造型”:在Java中,所有造型都会自动得到检查,即使是进行普通的括弧造型,进入运行期后,仍会对造型进行检查

    “运行期类型标识”(RTTI):在运行期间对类型进行检查的行为