原文鏈接: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,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/211006.html
標籤:其他
下一篇:SpringBoot啟動原理
