多态

封装,通过合并特征和行为来创建新的数据类型。实现隐藏,通过将细节私有化,把接口和实现分离开来。而多态的作用,是消除类型之间的耦合关系。

多态不但能够改善代码的组织结构和可读性,还可以创建可扩展程序:

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
class Shape {

public void draw() {
}
}

class Circle extends Shape {

@Override
public void draw() {
System.out.println("Circle");
}
}

class Square extends Shape {

@Override
public void draw() {
System.out.println("Square");
}
}

Shape shape1 = new Circle();
Shape shape2 = new Square();
shape1.draw(); // Circle
shape2.draw(); // Square

// 由于有多态机制,我们可根据自己的需求对系统添加任意多的新类型

绑定

将一个方法调用同一个方法主体关联起来,被称作 绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做 前期绑定。若在程序运行时根据对象的类型进行绑定,叫做 后期绑定(也叫做动态绑定或运行时绑定)。Java 中除了 staticfinal 方法(private 方法属于 final 方法)之外,其他方法都是后期绑定。

后期绑定是自动发生的,但是有两方面需要注意:

  • 私有方法。由于基类的私有方法对于导出类是不可见的,即不存在被导出类覆盖的情况
  • 域与静态方法。只有普通的方法调用可以是多态的,所以通常将域设置成 private,使用方法来进行访问
    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
    class A {

    public int n = 0; // 通常设置成 `private`

    public int getN() {
    return n;
    }
    }

    class B extends A {

    public int n = 1;

    @Override
    public int getN() {
    return n;
    }

    public int getSuperN() {
    return super.n;
    }

    }

    A a = new B();
    System.out.println(a.n); // 0
    System.out.println(a.getN()); // 1

    B b = new B();
    System.out.println(b.getN()); // 1
    System.out.println(b.getSuperN()); // 0

构造器和多态

尽管构造器并不具有多态性(它们实际上是 static 方法,只不过该 static 声明是隐式的),但还是有必要再次了解构造器的调用顺序:

  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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class A {

private int refcount = 0; // 记录共享的数量
private static long counter = 0; // 记录创建的实例数量,并且作为 id
private final long id = counter++; // 使用 final ,防止被改变

public A() {
System.out.println("Creating " + this);
}

public void addRef() {
refcount++;
}

protected void dispose() {
if (--refcount == 0) {
System.out.println("Disposing " + this);
}
}

@Override
public String toString() {
return "A " + id;
}
}

class B {

private A a;
private static long counter = 0;
private final long id = counter++;

public B(A a) {
System.out.println("Creating " + this);
this.a = a;
this.a.addRef();
}

protected void dispose() {
System.out.println("Disposing " + this);
a.dispose();
}

@Override
public String toString() {
return "B " + id;
}
}

A a = new A();
B[] bs = {new B(a), new B(a), new B(a)};
for (B b : bs) {
b.dispose();
}

// 输出:
// Creating A 0
// Creating B 0
// Creating B 1
// Creating B 2
// Disposing B 0
// Disposing B 1
// Disposing B 2
// Disposing A 0

构造器内部的多态方法的行为

在一个构造器的内部调用正在构造的对象的某个动态绑定,可能因操作的成员未初始化,而造成结果的偏差。

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
class A {

public void f() {
System.out.println("A: f");
}

A() {
System.out.println("before");
f(); // 被 B 方法覆盖,但此时 n 还未进行初始化
System.out.println("after");
}
}

class B extends A {

private int n = 1;

B(int n) {
this.n = n;
System.out.println("B: n = " + n);
}

@Override
public void f() {
System.out.println("B: n = " + n);
}
}

new B(5);

// 输出:
// before
// B: n = 0
// after
// B: n = 5

A 构造器里面的 f() 方法被 B 所覆盖,但是 n 的值却为 0 ,从这一点可以看出,初始化的实际过程是:

  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零
  2. 如前所述那样调用基类构造器,调用的确实是覆盖后的方法,但是 n的值因为步骤 1 的缘故,所以为 0
  3. 在导出类中,按声明的顺序调用成员的初始化方法
  4. 调用导出类构造器的主体

因此,在编写构造器时,尽量避免调用其他方法。构造器内唯一能够安全调用的那些方法是基类中的 final 方法(也适用于 private 方法,它们自动属于 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
26
27
28
29
30
31
32
33
34
35
36
37
38
class A {

@Override
public String toString() {
return "A";
}
}

class B extends A {

@Override
public String toString() {
return "B";
}
}

class AA {

public A f() {
return new A();
}
}

class BB extends AA {

@Override
public B f() {
return new B();
}
}

AA aa = new AA();
A a = aa.f();
System.out.println(a); // A

aa = new BB();
a = aa.f();
System.out.println(a); // B

用继承进行设计

在不能十分确定应该用哪种方式的时候,更好的选择是组合。一条通用的规则:用继承表达行为间的差异,并用字段表达状态上的变化:

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
38
39
class A {

public void f() {
}
}

class B extends A {

@Override
public void f() {
System.out.println("B");
}
}

class C extends A {

@Override
public void f() {
System.out.println("C");
}
}

class K {

private A a = new B();

public void change() {
a = new C();
}

public void f() {
a.f();
}
}

K k = new K();
k.f(); // B
k.change();
k.f(); // C