對象與this
原文鏈接:https://www.yuque.com/bravo1988/primary/dg9gqe
self與this
我們先來看一段Python代碼
# 括號里的object,表示Student類繼承自object。在Java里默認繼承Object。當然,Python里不寫也可以 class Student(object):
# 構造函數,變量前面下劃線,是訪問修飾符,表示私有 def __init__(self, name, age): self.__name = name self.__age = age # get方法,可不寫。你會發現Python里方法形參都有self,其實就相當于Java里的this,只不過Java通常是隱式的 def get_name(self): return self.__name def get_age(self): return self.__age def print_info(self): print("姓名:" + self.__name, "年齡:" + str(self.__age))
你會發現,有了Java基礎后,上手Python其實很簡單,你可以用Java的思維去寫Python,盡管寫出來的代碼不那么Pythonic。Python中的self非常有意思,個人認為比Java友好些,因為是顯式的,初學者可以很清楚的知道調用方法時到底發生了什么。
我之前把對象的本質理解為“多個相關數據的統一載體”,現在依然這么認為。比如一個人,有name、age、height等社會或生理體征,而這些數據是屬于一個個體的,如果用數組去存,表現力有所欠缺,無法表達“它們屬于同一個個體”的含義。
但我們知道,在Java中對象是在堆空間中生成的,數據會在堆空間占據一定內存開銷。而方法只有一份。
那么,方法為什么被設計出只有一份呢?
因為多個個體,屬性可能不同,比如我身高180,你身高150,我18歲,你30了。但我們都能跑、能跳、能吃飯,這些技能(method)都是共通的,沒必要和屬性數據一樣單獨在堆空間各存一份,所以被抽取出來存放。(對象保存在內存中由三部分組成:對象頭、實例數據、對齊填充。對象的實例數據就是類中定義的成員變量。)
此時,方法相當于一套指令模板,誰都可以傳入數據交給它執行,然后得到執行后的結果返回。
但此時會存在一個問題:張三這個對象調用了eat()方法,你應該把飯送到他嘴里,而不是送到李四嘴里。那么方法如何知道把飯送到哪里呢?
這就涉及到共性的方法如何處理特定的數據。
而Python的self、Java的this其實就是解決這個問題的。你可以理解為對象內部持有一個引用,當你調用某個方法時,必須傳遞這個對象引用,然后方法根據這個引用就知道當前這套指令是對哪個對象的數據進行操作了。

static與this
我們都知道,static修飾的屬性或方法其實都是屬于類的,是所有對象共享的。但接觸Python后我多了一層理解:
之所以一個變量或者方法要聲明為static,是因為
- static變量:大家共有的,大家都一樣,不是特定的差異化數據
- static方法:這個方法不處理差異化數據
也就是說,static注定是與差異化數據無關,也就是與具體對象的數據無關。
以靜態方法為例,當你確定一個方法只提供通用的操作流程,而不會在內部引用具體對象的數據時,你就可以把它定為靜態方法。
這個其實和我們之前聽到的解釋不一樣。網絡上一貫的解釋都是上來就告訴你靜態方法不能訪問實例變量,再解釋為什么,是倒著解釋的。而上面這段話的出發點是,當你滿足什么條件時,你就可以把一個方法定為靜態方法。
我們還是來看看Python中的方法。
現在我在Student里新定義了一個方法:
def simple_print(self): print("方法中不涉及具體的對象數據,啦啦啦啦~")

IDE發現你并沒有操作具體的對象數據,是一個通用的操作,于是提醒你這個方法可以用static。
要解決這個警告,有兩種方式:
- 在方法中引用對象的數據,變成實例方法

- 堅持不在方法內使用對象引用,把它變成靜態方法

你會發現,抽取成靜態方法后,形參沒有self了,Python在調用這個方法時也不再傳遞當前對象,反正靜態方法是不處理特定對象數據的。
這或許可以反過來解釋,為什么Java中靜態方法無法訪問非靜態數據(實例字段)和非靜態方法(實例方法)。因為Java不會在調用靜態方法時傳遞this,靜態方法內沒有this當然無法調用實例相關的一切。
我們在一個實例方法中調用另一個實例方法或者實例變量時,其實都是通過this調用的,比如
public void test(){
System.out.println(this.name);
this.show();
}
只不過Java允許我們不顯示書寫。
當然,有些培訓班視頻會說靜態方法隨著類加載而加載,此時并沒有對象實例化,所以靜態方法無法訪問實例相關數據。從現在這個角度看,有些方法只提供通用的操作流程,而不會在內部引用實例對象的數據,我們為了防止這種方法訪問實例數據,結合類加載機制,使用static修飾。
一個神奇的現象
請大家試著運行以下代碼:
public class Demo { public static void main(String[] args) { /** * new一個子類對象 * 我們知道,子類對象實例化時,會隱式調用父類的無參構造 * 所以Father里的System.out.println()會執行 * 猜猜打印的內容是什么? */ Son son = new Son(); Daughter daughter = new Daughter(); } } class Father{ /** * 父類構造器 */ public Father(){ // 打印當前對象所屬Class的名字 System.out.println(this.getClass().getName()); } } class Son extends Father { } class Daughter extends Father { }
不出所料,果然是打印子類Son、Daughter的名字。
這個現象是非常不可思議的!我們編寫父類時,子類甚至都還沒寫呢,而我們卻在父類中得到了子類的名字!
看看這是怎么實現的。
我們都知道,子類實例化時會隱式調用父類的構造器,效果相當于這樣:
class Father{ /** * 父類構造器 */ public Father(){ // 打印當前對象所屬Class的名字 System.out.println(this.getClass().getName()); } } class Son extends Father { public Son() { // 顯示調用父類無參構造 super(); } }
我把這種現象稱為:繼承鏈回溯(我自己生造的一個詞)。

調用構造器,其實也是調用方法,只不過構造器比較特殊。但我們可以肯定,這個過程中一定也會傳遞this。你看,Python的構造器就是傳遞self:
# 構造函數,變量前面下劃線,是訪問修飾符,表示私有 def __init__(self, name, age): self.__name = name self.__age = age

這樣一解釋,剛才的案例就沒什么神秘感了:嗨,不就是方法調用傳參嘛!
本質和子類調用方法給父類傳參一樣一樣的!只不過傳參的過程很特殊:
- new的時候自動傳參,不是我們主動調用,所以感知不到
- Java中的this是隱式傳遞的,所以我們更加注意不到了
父類和子類之間的方法調用
儲備知識:
調用某個類的構造方法的時候總是會先執行父類的非靜態代碼塊,然后執行父類的構造方法,最后才是執行當前類的非靜態代碼塊再執行構造方法。執行過程中有先后順序。
如果想要顯式調用父類的構造方法則可以使用super(),來調用,但是super關鍵字必須放在構造器的第一行,而且只能使 用一個。
為什么要放在第一行呢?因為如果不放在第一行則先調用子類的初始化代碼,再調用父類的初始化代碼,則父類中的初始化后的值 會覆蓋子類中的初始化的值。
訪問子類對象的實例變量 :
子類的方法可以訪問父類中的實例變量,這是因為子類繼承父類就會獲得父類中的成員變量和方法,但是父類方法不能訪問子類的實例變量 ,因為父類根本無法知道它將被哪個類繼承,它的子類將會增加怎么樣的成員變量。但是,在極端的情況下,父類也可以訪問子類中的變量。
class A {
public String getName() {
return name;
}
String name = "A";
public A() {
//new B()的時候,this代表的是B的實例,但是編譯的時候類型為A,所以this.name是A,this.getClass為B
//通過聲明的變量調用方法時,方法的行為總是表現出他們的實際類型的行為.
//通過變量來訪問他們所指向對象的實例變量的時候,這些實例變量的值總是表現出聲明類型的行為.
System.out.println("A中 this.name=" + this.name);
System.out.println("A中 this.name=" + this.getName());
System.out.println("A中 this.getClass()=" + this.getClass());
test();
}
public void test() {
System.out.println(name);
}
}
class B extends A {
@Override
public String getName() {
return name;
}
String name = "B";
public B(String name) {
this.name = name;
}
public B() {
System.out.println("B中 this.name=" + this.name);
System.out.println("B中 this.getClass()=" + this.getClass());
}
@Override
public void test() {
System.out.println("重寫test中 this.name:" + this.name);
}
public static void main(String[] args) {
B b = new B();
}
}
打印結果

解釋:
執行new B( ) 創建B實例的時候,系統會為B對象分配內存空間,B會有兩個name實例變量,會分配兩個空間來保存值。分配完空間以后name的值都是null(name為引用類型)。
接下來程序去執行B的構造器之前會執行A的構造器,程序先將A中的name賦值為A,然后執行打印this.name。此處有一個關鍵字this,this到底代表誰?表面上看this代表的是A實例,但是實際是在執行B的構造方式時隱式調用super( )代碼,即super()是放在B的構造器中的,所以this最終代表的是B的當前實例(編譯類型是A而實際引用一個B對象)。
如果在父類的構造方法中直接打印this.name,則輸出的是A,但是調用this.test方法,此時調用的是子類重寫的test方法,輸出的變量name也是子類中的name,但是此時子類中的變量name還沒有賦值,所以輸出結果為null。
java中對成員變量的繼承和成員方法的繼承是不同的。
不管聲明時用了什么類型,當通過聲明的變量調用方法時,方法的行為總是表現出他們的實際類型的行為。
但是如果通過這些變量來訪問他們所指向對象的實例變量的時候,這些實例變量的值總是表現出聲明這些變量所用類型的行為。
引申:上述的通過變量來訪問實例變量是 變量名.字段名 的方式,如果是 變量名.getName() 實際調用的是方法,方法行為與實際類型行為一致,此時獲取到的name就是實際類型的中B的name不是聲明類型A中的name。
浙公網安備 33010602011771號