Java学习笔记之深入理解动态绑定和静态绑定



基本概念

动态绑定和静态绑定是Java中两个重要的概念。首先思考这么一个问题,当一个类中存在方法名相同但参数不同(重载)的函数或同一类层次结构下同一名称的方法(重写),程序在执行的时候该如何辨别区分呢?这里就需要用到Java中的动态绑定和静态绑定来解决。那么什么是动态绑定和静态绑定呢?

  • 绑定:指一个方法的调用与方法所在的类关联起来
  • 静态绑定:方法在程序编译期进行绑定
  • 动态绑定:方法在程序运行时根据具体对象的类型进行绑定

静态绑定 VS 动态绑定

  • 静态绑定发生在编译期(Compile time),而动态绑定发生在运行时(Runtime)
  • private, final and static方法和变量使用静态绑定,而虚函数(virtual methods)则会根据运行时的具体对象进行绑定
  • 静态绑定使用的是类信息,而动态绑定使用的是对象信息
  • 重载方法(overloaded methods)使用的是静态绑定,而重写方法(overridden methods)使用的是动态绑定

虚函数:在Java语言中, 所有的方法默认都是”虚函数”。只有以关键字 final 标记的方法才是非虚函数。虚函数是面向对象编程实现多态的基本手段。


静态绑定示例

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public void hello(String str) {
System.out.println("a String instance in A");
}

public void hello(Object str) {
System.out.println("a Object instance in A");
}
}

public class BindingDemo {
public static void main(String args[]) {
String str = "";
Object obj = "";
A a = new A();
a.hello(str);
a.hello(obj);
}
}

输出结果:

1
2
a String instance in A
a Object instance in A

分析:可以看出,在A中有两个同名但参数不同的方法(重载),在调用方法a.hello()时,程序会自动根据输入的参数类型来选择具体调用哪个方法,其后的原理就是静态绑定,即在编译期根据参数类型进行静态绑定。


动态绑定实例

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public void hello() {
System.out.println("hello in A");
}
}

class B extends A {
@Override
public void hello() {
System.out.println("hello in B");
}
}

public class StaticBindingDemo {
public static void main(String args[]) {
A a = new B();
a.hello();
}
}

输出结果:

1
hello in B

分析:B继承于A,并重写了A中的方法hello(),从结果可知,a.hello()调用的不是A中的hello(),而是B中的hello(),这正是因为程序在运行时发生了动态绑定。同时也说明了重写方法使用的是动态绑定。

相关问题探讨

Stack Overflow上探讨了这么一个问题,具体如下:

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

int x = 5;
}
class B extends A
{

int x = 6;
}
class SubCovariantTest extends CovariantTest
{

public B getObject()
{

System.out.println("sub getobj");
return new B();
}
}

public class CovariantTest
{

public A getObject()
{

System.out.println("ct getobj");
return new A();
}
public static void main(String[]args)
{

CovariantTest c1 = new SubCovariantTest();
System.out.println(c1.getObject().x);
}
}

输出结果:

1
2
sub getobj
5

看到这样的输出结果,很多人可能会很纳闷,从第一个输出结果sub getobj可以看出调用的是B对象,但从x输出值上看,却是A对象,这是怎么一回事?!先不急着解决这个问题,我们先来看下下面这段程序:

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

int x = 5;

public void doSomething() {
System.out.println("A.doSomething()");
}
}
class B extends A
{

int x = 6;

public void doSomething() {
System.out.println("B.doSomething()");
}
}

public class Main {

public static void main(String args[]) {
A a=new B();
System.out.println(a.x);
a.doSomething();
}
}

输出结果:

1
2
5
B.doSomething()

分析:根据前面讲解的静态绑定和动态绑定知识,我们可以清楚的知道,在调用方法a.doSomething()时发生的是动态绑定,故输出结果是B.doSomething(),但当调用a.x时,又发生了些什么呢?!
这里,我们需要了解一个概念:在Java中,变量(fields)没有多态这个说法,只有方法(methods)有。即变量不存在动态绑定,它是在编译期进行绑定。
在编译期,变量x和方法doSomething()都是和A类进行绑定的,而在程序运行时,doSomething()方法会根据运行的对象类型进行动态绑定,即绑定B类,从而执行的是B类中的doSomething()方法。而x变量不进行动态绑定,所以执行的还是A类中的x变量。
再回到最开始的那个问题:在编译期,c1绑定的是类CovariantTest,则c1.getObject()返回的是类A,故c1.getObject().x指的是A.x;在运行时,c1.getObject()则会根据运行的对象进行动态绑定,即绑定类SubCovariantTest,而c1.getObject().x不会在进行动态绑定,所以x的值还是类A中的x的值。


有人可能对变量x到底是属于哪个类还是很疑惑,那么我们再通过一个例子来进行说明:虽然类A和类B都有一个变量x,但是它们并不是同一个x,既然这样,我们就可以用不同的名称表示这两个变量,将类B中的变量x改为y,具体如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
int x = 5;
}
class B extends A {
int y = 6;
}
class SubCovariantTest extends CovariantTest {
public B getObject() {
System.out.println("sub getobj");
return new B();
}
}
public class CovariantTest {
public A getObject() {
System.out.println("ct getobj");
return new A();
}

public static void main(String[] args) throws java.lang.Exception {
CovariantTest c1 = new SubCovariantTest();
System.out.println(c1.getObject().y);
}
}


在编译期,如果c1.getObject()返回的是类B,那么上面程序完全没问题,但实际上,会出现以下编译错误。说明,在编译期,c1.getObject()返回的是类A,c1.getObject().x指的是A.x,而且在运行期间不会发生改变。

上面的问题解决了,可能有人还会继续问,那么怎样才能达到变量被重写的效果?我们可以这样实现
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 {
int x = 5;

public int getX() {
return x;
}

public void doSomething() {
System.out.println("A.doSomething()");
}
}
class B extends A {
int x = 6;

public int getX() {
return x;
}

public void doSomething() {
System.out.println("B.doSomething()");
}
}

public class Main {

public static void main(String args[]) {
A a = new B();
System.out.println(a.getX());
a.doSomething();
}
}


输出结果:
1
2
6
B.doSomething()


显然,正是利用方法的动态绑定达到变量被重写的效果。

参考资料

  1. Java中的静态绑定和动态绑定
  2. Java中的静态绑定和动态绑定
  3. What is Static and Dynamic binding in Java with Example