問題
先來看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);
}
}
當我們執(zhí)行CovariantTest中的main()方法時,輸出結(jié)果會是什么?
答案是:
sub getobj
5
這不科學!既然c1.getObject()執(zhí)行的是SubCovariantTest中的getObject()方法,那么應該返回的是B對象,所以應該輸出6才對啊。
這個解釋聽上去很有道理,但忽略了Java中的靜態(tài)綁定和動態(tài)綁定的知識。
靜態(tài)綁定 vs 動態(tài)綁定
- 綁定:綁定指的是一個方法的調(diào)用與方法所在的類(方法主體)關(guān)聯(lián)起來。
- 靜態(tài)綁定:程序運行前方法已被綁定。即Java中編譯期進行的綁定。
- 動態(tài)綁定:程序運行時根據(jù)具體對象的類型進行綁定。
Java中程序分為編譯和解釋兩個階段。
也就是說,Java文件被編譯成class文件時,已經(jīng)對其中的方法和域根據(jù)類信息進行了一次綁定(靜態(tài)綁定)。
而運行時方法又會根據(jù)運行時對象信息進行另外一次綁定(動態(tài)綁定),也就說我們常說的多態(tài)
值得注意的是:Java中private,final,static方法以及域都是靜態(tài)綁定,這也是Java中域不能被重寫只能被隱藏的原因。
進一步理解兩種綁定
什么?你還是不明白這兩種綁定!沒關(guān)系,我們來點更詳細的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()
結(jié)果在意料當中,我們來看下反編譯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類綁定在了一起。而在程序執(zhí)行時,doSomething方法會再根據(jù)運行的對象類型進行第二次的動態(tài)綁定,從執(zhí)行了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中域是靜態(tài)綁定的,所以程序運行時便不會根據(jù)運行時對象類型來確定,所有最后輸出了5
兩種綁定各自的優(yōu)缺點
靜態(tài)綁定能夠讓我們在編譯時就發(fā)現(xiàn)代碼的許多錯誤,而且也提高了程序的運行效率。動態(tài)綁定的好處在于犧牲了運行效率但實現(xiàn)了多態(tài)
這里引用 Java靜態(tài)綁定與動態(tài)綁定中的一段話來總結(jié):
java因為什么要對屬性要采取靜態(tài)的綁定方法。這是因為靜態(tài)綁定是有很多的好處,它可以讓我們在編譯期就發(fā)現(xiàn)程序中的錯誤,而不是在運行期。這樣就可以提高程序的運行效率!而對方法采取動態(tài)綁定是為了實現(xiàn)多態(tài),多態(tài)是java的一大特色。多態(tài)也是面向?qū)ο蟮年P(guān)鍵技術(shù)之一,所以java是以效率為代價來實現(xiàn)多態(tài)這是很值得的。
最后的啰嗦
這個問題我起初在CSDN上提問:stackoverflow上面的java的域不能“重寫”問題,感謝caozhy 的回答。雖然當時還是有些不理解。過了段時間開始有點明白,所以寫了這篇博客作總結(jié)。水平有限,僅供參考。
關(guān)于動態(tài)綁定更詳細的實現(xiàn)機制可以看 Simple Java—Compiler and JVM(一)Java對象運行時的內(nèi)存結(jié)構(gòu)