文章目录
  1. 1. 静态绑定
  2. 2. 动态绑定
  3. 3. 缺陷
  4. 4. 构造器与多态
    1. 4.1. 构造器的调用顺序
    2. 4.2. 构造器内部的多态方法的行为

面向对象编程有三大特性:封装、继承、多态。

封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。

继承是为了重用父类代码。两个类若存在IS-A的关系就可以使用继承。同时继承也为实现多态做了铺垫。

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态也称作动态绑定。

静态绑定

静态绑定又名前期绑定。

如果编译器可以在编译阶段就完成绑定,就叫作静态绑定或前期绑定。基本上实例方法都在运行时绑定。

所有的静态方法static都在编译时绑定,所以静态方法是静态绑定的。因为静态方法是属于类的方法,可以通过类名来访问(我们也应该使用类名来访问静态方法,而不是使用对象引用来访问),所以要访问它们就必须在编译阶段就使用编译类型信息来进行绑定。这也就解释了为什么静态方法实际上不能被重写。

为了防止方法被他人覆盖,我们会将某个方法声明为final(private方法属于final方法),这样做可以有效的关闭动态绑定,或者说告诉编译器不需要对其进行动态绑定。这样编译器就可以为final方法调用生成更有效的代码。

类似的,访问成员变量也是静态绑定的,因为Java不支持(实际上是不鼓励)成员变量的多态行为。下面看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SuperClass{
...
public String someVariable = "Some Variable in SuperClass";
...
}
class SubClass extends SuperClass{
...
public String someVariable = "Some Variable in SubClass";
...
}
SuperClass superClass1 = new SuperClass();
SuperClass superClass2 = new SubClass();
System.out.println(superClass1.someVariable);
System.out.println(superClass2.someVariable);

输出

1
2
Some Variable in SuperClass
Some Variable in SuperClass

我们可以发现成员变量由对象引用声明的类型决定,是由编译器在编译阶段就知道的信息,所以是静态绑定。另外一个静态绑定的例子是私有的方法,因为它们不会被继承,编译器在编译阶段就完成私有方法的绑定了。

动态绑定

动态绑定又名后期绑定。

动态绑定是指编译器在编译阶段不知道要调用哪个方法,直到运行时才能确定。Java中除了static方法和final方法之外,其他所有方法都是后期绑定。让我们用个例子来解释。譬如我们有一个叫作’SuperClass’的父类,还有一个继承它的子类’SubClass’。现在SuperClass引用也可以赋给SubClass类型的对象。如果SuperClass中有个someMethod()的方法,而子类也重写了这个方法,那么当调用SuperClass引用的这个方法的时候,编译器不知道该调用父类还是子类的方法,因为编译器不知道对象到底是什么类型,只有到运行时才知道这个引用指向什么对象。

1
2
3
4
5
6
SuperClass superClass1 = new SuperClass();
SuperClass superClass2 = new SubClass();
...
superClass1.someMethod(); // SuperClass version is called
superClass2.someMethod(); // SubClass version is called

我们可以看到虽然对象引用superClass1和superClass2都是SuperClass类型的,但是在运行时它们分别指向SuperClass和SubClass类型的对象。

所以在编译阶段,编译器不清楚调用引用的someMethod()到底是调用子类还是父类的该方法。

所以方法的动态绑定是基于实际的对象类型,而不是它们声明的对象引用类型。

缺陷

  • “覆盖”私有方法
1
2
3
4
5
6
7
8
9
10
11
12
public class PrivateOverride{
private void f(){system.out.println("private f()");}
public static void main(String[] args){
PrivateOverride po = new PrivateOverride();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){system.out.println("public f()");}
}

输出

1
private f()

我们期望输出public f(),但是private方法被自动认为是final方法,而且对导出类是自动屏蔽的

因此Derived中的f()方法是一个全新的方法,虽然和PrivateOverride中的方法同名…

所以在导出类中,对于基类的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
class Meal{
Meal(){print("Meal()");}
}
class Bread{
Bread(){print("Bread()");}
}
class Cheese{
Cheese(){print("Cheese()");}
}
class Lunch extends Meal{
Lunch(){print("Lunch()");}
}
class PortableLunch extends Meal{
PortableLunch(){print("PortableLunch()");}
}
public class Sandwich extend PortableLunch {
private Bread b = new Bread;
private Cheese c = new Cheese;
public Sandwich(){print("Sandwich()")}
public static void main(String[] args) {
new Sandwich();
}
}
  1. 调用基类构造器
  2. 按声明顺序调用成员的初始化方法
  3. 调用导出类构造器的主体

因此输出结果为:

1
2
3
4
5
6
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Sandwich()

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

如果在一个构造器内部调用正在构造的对象的动态绑定方法,就要用到那个方法被覆盖后的定义。然后,这个方法所操纵的成员可能还未进行初始化——这可能会导致灾难。

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
class Glyph {
void draw() {print("Glyph()");}
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph.radius = " + radius);
}
void draw() {
print("RoundGlyph.RoundGlyph.radius = " + radius);
}
}
public class PlyConstructor {
public static void main(String[] args){
new RoundGlyph(5);
}
}

上一节讲述的初始化顺序其实并不完整,实际过程是:

  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的零
  2. 调用基类构造器。由于步骤1,发现radius是0
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器主题

输出结果为:

1
2
3
4
Glyph() before draw()
RoundGlyph.RoundGlyph.radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph.radius = 5


Thinking in Java : As a result, a good guideline for constructors is, “Do as little as possible to set the object into a good state, and if you can possibly avoid it, don’t call any other methods in this class.”

The only safe methods to call inside a constructor are those that are final in the base class. (This also applies to private methods, which are automatically final.) These cannot be overridden and thus cannot produce this kind of surprise. You may not always be able to follow this guideline, but it’s something to strive towards.

分享一篇较为通俗的讲解

文章目录
  1. 1. 静态绑定
  2. 2. 动态绑定
  3. 3. 缺陷
  4. 4. 构造器与多态
    1. 4.1. 构造器的调用顺序
    2. 4.2. 构造器内部的多态方法的行为