1.物件和類的簡單解析

ethanSung發表於2021-07-02

1.物件和類的簡單解析

1.1.物件的簡單記憶體

堆(Heap)

此記憶體區域的功能是存放物件的例項,存放由new建立的物件或者記憶體陣列,在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收器來管理。幾乎所有的物件都在這裡分配,注意:物件的屬性跟區域性變數可是不一個概念,區域性變數儲存在棧中,物件的屬性存在堆中

在堆中產生一個陣列或物件之後,還可以在棧中定義一個特殊的變數p1,讓棧中的這個變數賦值等於陣列或物件在堆記憶體中的首地址。

例如:Person p1 = new Person();

棧中的這個變數就是陣列或物件的引用變數也就是引用型別的資料,儲存的是物件在堆中的首地址,以後就可以在程式中使用棧中的變數p1來訪問堆中的陣列或者物件,引用型別的變數就相當於為在堆記憶體中的陣列或者物件所在的地址起的一個別名(可以理解下C語言的指標),便於我們程式的理解,總不能直接用記憶體地址作為變數使用,可讀性太差。引用變數是普通的變數,定義時在棧中分配,引用變數在程式執行到其他作用域之外後邊釋放。而陣列和物件本省在堆中分配,即使程式執行到使用new產生的陣列或者物件的語句所在的程式碼塊之外,陣列和物件本省佔據的記憶體不會被釋放。陣列和物件在沒有引用變數指向它的時候,才變為垃圾,不能再被使用,在隨後的一個不確定時間被垃圾回收器收走(釋放掉)。這也是Java比較佔記憶體的原因,實際上,棧中的變數指向堆記憶體中的變數,這就是Java中的指標。

  • 棧(Stack),虛擬機器棧

    一般是指虛擬機器棧,此處記憶體用於儲存區域性變數(方法中的變數都是區域性變數,所以棧中儲存的可以是基本資料型別的變數,也可以是物件的首地址這種引用型別的變數)等,區域性變數表存放了編譯期可知長度的各種基本資料型別(),物件引用(reference型別,它不是物件本身,是物件在堆記憶體中的首地址)

    在函式中定義的一些基本型別的變數和物件的引用變數都是在函式的棧記憶體中分配,當在一段程式碼塊定義一個變數時,Java 就在棧中為這個變數分配記憶體空間,當超過變數的作用域後(比如,在函式A中呼叫函式B,在函式B中定義變數a,變數a的作用域只是函式B,在函式B執行以後,變數a會自動被銷燬。分配給它的記憶體會被回收),Java會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立即另做他用。

  • 方法區(Method Area)

    用於儲存已經被類載入器載入的類資訊,常量,靜態變數等資料;

  • 本地方法棧

  • 程式計數器

2.1.程式分析

  1. 例項1:
package com.ethan.objandthis1;

public class Demo2 {
    public static void main(String[] args) {
        Person per1 = new Person() ;
        Person per2 = new Person() ;
        per1.name="張三" ;
        per1.age=30 ;
        per2.age=33 ;
        per1.tell();
        per2.tell();
    }
}


class Person {
    String name;
    int age;
    boolean isMale;
    public void tell() {
        System.out.println("姓名:"+name+",年齡:"+age);
    }
}

對上面的程式進行記憶體分析如下圖:

  1. 例項2:
package com.ethan.objandthis1;

public class Demo2 {
    public static void main(String[] args) {
        Person per1 = new Person() ;
        Person per2 = new Person() ;
        per1.name="張三" ; 
        per2.name="李四" ;
        Person per3 = per1;//注意
        per1.age=30 ;
        per2.age=33 ;
        per3.age=20;//注意
        System.out.println(per1.age);//注意
    }
}


class Person {
    String name;
    int age;
    boolean isMale;
    public void tell() {
        System.out.println("姓名:"+name+",年齡:"+age);
    }
}

對上面的程式進行記憶體分析如下圖:

總結:

  1. 實際上所謂的引用傳遞,就是將一個堆記憶體空間的使用權交給多個棧記憶體空間,每個棧記憶體空間都可以修改堆記憶體空間的內容;
  2. 引用型別就是指同一個堆記憶體可以被多個棧記憶體指向就如同上面的per3和per1,兩個引用變數都可以操作堆記憶體中的同一個物件per1;
  3. 通過new操作,才能例項化物件,也就是在堆中為物件開闢記憶體空間,而上述的per3操作只是宣告瞭一個引用變數;
  4. 在Java中主要存在4塊記憶體空間,這些記憶體的名稱及作用如下:

  1) 棧記憶體空間:儲存所有物件的名稱。

  2)堆記憶體空間:儲存每個物件的具體屬性內容。

  3)全域性資料區:儲存static型別的屬性值。

  4)全域性程式碼區:儲存所有的方法定義。

3.1.屬性(成員變數)和區域性變數的對比

相同點:

  1. 都是變數,變數的先宣告後使用,定義變數的格式,都有其對應的作用域;

不同點:

  1. 在類中定義和宣告的位置不一樣

    屬性:直接定義在類中;

    區域性變數:宣告在方法內,形參,程式碼塊,構造器引數,構造器內部變數

  2. 屬性需要加許可權修飾符,指明其許可權範圍,private,protected,預設,public

  3. 屬性有預設初始化值,區域性變數沒有預設賦值,意味著我們呼叫時候需要顯式賦值,宣告區域性變數時必須初始化值

  4. 在記憶體中載入的位置不一樣

    1. 屬性(非static型別的屬性)載入到堆空間;
    2. 區域性變數載入到棧空間;

以下學習來自bravo1988 - 知乎 (zhihu.com)的java小冊!!!

4.1. 關於java中方法的引用

我們知道,在Java中物件是在堆空間中生成的,屬性產生賦值的資料會在堆空間佔據一定記憶體開銷。而方法只有一份。

那麼,方法為什麼被設計成只有一份呢?

因為多個個體,屬性可能不同,比如我身高180,你身高150,我18歲,你30了。但我們都能跑、能跳、能吃飯,這些技能(method)都是共通的,沒必要和屬性資料一樣單獨在堆空間各存一份,所以被抽取出來存放,可以減少記憶體的開支(猜測)

此時,方法相當於一套指令模板,誰都可以傳入資料交給它執行,然後得到執行後的結果返回。

但此時會存在一個問題:張三這個物件呼叫了eat()方法,你應該把飯送到他嘴裡,而不是送到李四嘴裡。那麼方法如何知道把飯送到哪裡呢?

換句話說:共性的方法如何處理不同物件的特定的資料?

Java的this其實就是解決這個問題的。可以理解為一個物件內部一直持有一個引用,這個引用就是this,當你呼叫某個方法時,必須傳遞這個物件引用,然後方法根據這個引用就知道當前這套指令是對哪個物件的資料進行操作了。

2. static與this

我們都知道,static修飾的屬性或方法其實都是屬於類的,是所有物件共享的。但接觸Python後我多了一層理解。之所以一個變數或者方法要宣告為static,是因為

  • static變數:大家共有的,大家都一樣,不是特定的差異化資料
  • static方法:這個方法不處理差異化資料

也就是說,static註定與差異化資料無關,即與具體物件的資料無關。

靜態方法為例,當你確定一個方法只提供通用的操作流程,而不會在內部引用具體物件的資料時,你就可以把它定為靜態方法。

這個其實和我們之前聽到的解釋不一樣。網路上一貫的解釋都是上來就告訴你靜態方法不能訪問例項變數,再解釋為什麼,是倒著解釋的。而上面這段話的出發點是,當你滿足什麼條件時,你就可以把一個方法定為靜態方法。

反過來解釋,為什麼Java中靜態方法無法訪問非靜態資料(例項欄位)和非靜態方法(例項方法)。因為Java不會在呼叫靜態方法時傳遞this,靜態方法內沒有this當然無法處理例項相關的一切。

3. 關於子類和父類

首先看一個demo關於子類和父類生成物件時候:

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 {
}

首先我們需要明確的知道,子類在生成物件呼叫構造器時候,是肯定會預設呼叫父類的無參構造器,隱式的呼叫super(),但是我們一定要知道:並沒有生成父類物件。

下面的demo是便於理解this以及父類和子類的關係,無法正常編譯。

class Father{
    /** this引數名接收引數值, 
    this預設指向父類自身, 
    如果this是通過子類物件super(this)傳入的, 那麼this當然指向該子類物件
    當然這種寫法不合語法, 只是為了方便表達, Java做了相應的隱式處理
    */
    public Father(this){
        System.out.println(this.getClass().getName());
    }
}
class Son extends Father {
    public Son(this) {
        //把指向自身的this傳給父類構造方法
        super(this);
    }
}

重點的理解:!!!

生成子類物件時候,雖然是呼叫了父類的無參構造器,但是並不意味著生成了父類物件,呼叫了父類構造器的目的是初始化了一些必要的屬性, 並沒有建立父類物件,有自己獨立空間的才是一個物件, 此時父類初始化的屬性都在子類物件所屬的空間裡面, 所以並沒有建立出父類物件。 this只指向子類物件。

一個子類物件的例項會包含其所有基類所宣告的欄位,外加自己新宣告的欄位。那些父類宣告的欄位並不構成一個完整的父類的例項。super()是讓父類封裝對自己所宣告的欄位做初始化的手段。"

相關文章