問題
先來看stack overflow上的一個問題:
import java.util.*;
import java.lang.*;
import java.io.*;
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();
}
}
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().x);
}
}
當我們執行CovariantTest
中的main()
方法時,輸出結果會是什么?
答案是:
sub getobj
5
這不科學!既然c1.getObject()
執行的是SubCovariantTest
中的getObject()
方法,那么應該返回的是B對象,所以應該輸出6才對啊。
這個解釋聽上去很有道理,但忽略了Java中的靜態綁定和動態綁定的知識。
靜態綁定 vs 動態綁定
- 綁定:綁定指的是一個方法的調用與方法所在的類(方法主體)關聯起來。
- 靜態綁定:程序運行前方法已被綁定。即Java中編譯期進行的綁定。
- 動態綁定:程序運行時根據具體對象的類型進行綁定。
Java中程序分為編譯和解釋兩個階段。
也就是說,Java文件被編譯成class文件時,已經對其中的方法和域根據類信息進行了一次綁定(靜態綁定)。
而運行時方法又會根據運行時對象信息進行另外一次綁定(動態綁定),也就說我們常說的多態
值得注意的是:Java中private,final,static方法以及域都是靜態綁定,這也是Java中域不能被重寫只能被隱藏的原因。
進一步理解兩種綁定
什么?你還是不明白這兩種綁定!沒關系,我們來點更詳細的Demo
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();
}
}
Output:
5
B.doSomething()
結果在意料當中,我們來看下反編譯Main.class文件后的信息:
//javap -c Main
Compiled from "Main.java"
public class test.Main {
public test.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class test/B
3: dup
4: invokespecial #3 // Method test/B."<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: getfield #5 // Field test/A.x:I
15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
18: aload_1
19: invokevirtual #7 // Method test/A.doSomething:()V
22: return
}
通過12:
和19:
后的注釋我們可以知道,編譯后的class文件中,域x
和方法doSomething
都是和A
類綁定在了一起。而在程序執行時,doSomething
方法會再根據運行的對象類型進行第二次的動態綁定,從執行了B
類中的方法。
解答問題
回到最開始問題。
我們反編譯下CovariantTest.class文件:
//javap -c CovariantTest
Compiled from "CovariantTest.java"
public class test.CovariantTest {
public test.CovariantTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public test.A getObject();
Code:
0: new #2 // class test/A
3: dup
4: invokespecial #3 // Method test/A."<init>":()V
7: areturn
public static void main(java.lang.String[]);
Code:
0: new #4 // class test/SubCovariantTest
3: dup
4: invokespecial #5 // Method test/SubCovariantTest."<init>":()V
7: astore_1
8: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #7 // Method getObject:()Ltest/A;
15: getfield #8 // Field test/A.x:I
18: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
21: return
}
從12:
和15:
可以看出,在編譯期,c1
跟類CovariantTest
綁定在一起,所以c1.getObject()
編譯后認為是類A
,c1.getObject().x
編譯后便成為了A.x
。
又因為Java中域是靜態綁定的,所以程序運行時便不會根據運行時對象類型來確定,所有最后輸出了5
兩種綁定各自的優缺點
靜態綁定能夠讓我們在編譯時就發現代碼的許多錯誤,而且也提高了程序的運行效率。動態綁定的好處在于犧牲了運行效率但實現了多態
這里引用 Java靜態綁定與動態綁定中的一段話來總結:
java因為什么要對屬性要采取靜態的綁定方法。這是因為靜態綁定是有很多的好處,它可以讓我們在編譯期就發現程序中的錯誤,而不是在運行期。這樣就可以提高程序的運行效率!而對方法采取動態綁定是為了實現多態,多態是java的一大特色。多態也是面向對象的關鍵技術之一,所以java是以效率為代價來實現多態這是很值得的。
最后的啰嗦
這個問題我起初在CSDN上提問:stackoverflow上面的java的域不能“重寫”問題,感謝caozhy 的回答。雖然當時還是有些不理解。過了段時間開始有點明白,所以寫了這篇博客作總結。水平有限,僅供參考。
關于動態綁定更詳細的實現機制可以看 Simple Java—Compiler and JVM(一)Java對象運行時的內存結構