天天看點

JAVA 面向對象 成員變量和局部變量

本頁面更新日期: 2016年07月20日

前言

在 Java語言中, 根據定義變量位置的不同,可以将變量分成兩大類:
  • 成員變量
  • 局部變量
成員變量和局部變量的運作機制存在很大差異,下面我們看看差異在哪.

成員變量

成員變量指的是在類裡定義的變量.

局部變量指的是在方法裡定義的變量.

下面我給出Java程式中的變量劃分圖:

JAVA 面向對象 成員變量和局部變量

成員變量被分為類變量和執行個體變量兩種.

定義成員變量時沒有 static 修飾符的就是執行個體變量.

有static修飾符的就是類變量.

其中, 類變量從該類的準備階段起開始存在.

直到系統完全銷毀這個類,類變量的作用域與這個類的生存範圍相同.

而執行個體變量則從該類的執行個體被建立起開始存在,知道系統完全銷毀這個執行個體.

執行個體變量的作用域與對應執行個體的生存範圍相同.

小知識: 一個類在使用之前需要經過 類加載 / 類驗證 / 類準備 / 類解析 / 類初始化 等幾個階段.

關于類的生命周期, 我們以後會詳細介紹.

正是基于以上原因, 可以把類變量和執行個體變量統稱為成員變量.

其中, 類變量可以了解為 類成員變量, 它作為類本身的一個成員, 與類本身共存亡.

執行個體變量則可以了解為 執行個體成員變量, 它作為執行個體的一個成員, 與執行個體共存亡.

隻要類存在, 程式就可以通路該類的類變量.

在程式中通過如下文法通路:

類.類變量
           

隻要執行個體存在, 程式就可以通路該執行個體的執行個體變量.

在程式中通過如下文法通路:

執行個體.執行個體變量
           

當然, 類變量也可以讓該類的執行個體來通路.

通過執行個體通路類變量的文法如下:

執行個體.類變量
           

但由于這個執行個體并不擁有這個類變量.

是以它通路的并不是這個執行個體的變量,依然是通路它對應類的類變量.

也就是說, 如果通過一個執行個體修改了類變量的值, 由于這個類變量并不屬于它.

而是屬于它對應的類. 是以, 修改的依然是類的類變量.

與通過該類來修改類變量的結果完全相同.

這會導緻該類的其它執行個體來通路這個類變量時也獲得這個被修改過的值.

上面啰嗦了半天, 不過其内容我們之前已經體驗過, 你能明白就行.

下面我寫個程式, 定義了一個 Person 類, 在這個 Person 類中定義兩個成員變量.

一個類變量: eyeNum

一個執行個體變量: name

程式通過 PersonTest 類來建立 Person 執行個體.

并分别通過 Person 類 和 Person 執行個體來通路執行個體變量和類變量.

class Person
{
  //定義一個類變量
  public static int eyeNum;
  //定義一個執行個體變量
  public String name;
}

public class PersonTest
{
  public static void main(String[] args)
  {
    //第一次主動使用 Person 類, 該類自動初始化, 則 eyeNum 變量開始起作用, 輸出 0
    System.out.println("Person 的 eyeNum 類變量的值:" + Person.eyeNum);
    //建立 Person 對象
    Person p = new Person();
    //通過 Person 對象的引用 p 來通路 Person 對象的 name 執行個體變量
    //并通過執行個體通路 eyeNum 類變量
    System.out.println("p 對象的 name 變量的值是:" + p.name + "p 對象的 eyeNum 變量的值是:" + p.eyeNum);
    //直接為 name 執行個體變量指派
    p.name = "孫悟空";
    //通過 p 通路 eyeNum 類變量, 依然是通路 Person 的 eyeNum 類變量
    p.eyeNum = ;
    //再次通過 Person 對象來通路 name 執行個體變量 和 eyeNum 類變量
    System.out.println("p 對象的 name 變量值是:" + p.name + "p 對象的 eyeNum 變量值是:" + p.eyeNum);
    //前面通過 p 修改了 Person 的 eyeNum, 此處的 Person.eyeNum 将輸出 2
    System.out.println("Person 的 eyeNum 類變量值:" + Person.eyeNum);
    Person p2 = new Person();
    //p2 通路的 eyeNum 類變量依然引用 Person 類的, 是以依然輸出 2
    System.out.println("p2 對象的 eyeNum 類變量值:" + p2.eyeNum);
  }
}
           

從上面程式可以看出, 成員變量無須顯式初始化.

隻要為一個類定義了 類變量 或 執行個體變量.

系統就會在這個類的準備階段或建立該類的執行個體時進行預設初始化.

成員變量預設初始化的指派規則與我們之前講的 數組動态初始化 時數組元素的指派規則完全相同.

我們還可以得知, 類變量的作用域 比 執行個體變量的作用域 更大.

執行個體變量随執行個體的存在而存在.

而類變量則随類的存在而存在.

執行個體也可以通路類變量, 同一個類的所有執行個體通路類變量時.

實際上通路的是該類本身的同一個變量, 也就是說, 通路了同一片記憶體區.

注意!!!

Java 允許執行個體通路 static 修飾的類變量本身就是一個錯誤.

是以建議你以後看到通過執行個體來通路 static 修飾的類變量時, 都可以将它替換成通過類本身來通路. 這樣程式的可讀性 / 明确性 都會大大提高!

局部變量

局部變量根據定義形式的不同, 可以分為如下三種:
  • 形參: 在定義方法簽名時定義的變量, 形參的作用域在這個方法内有效.
  • 方法局部變量: 在方法體内定義的局部變量, 它的作用域就是從定義該變量的地方生效, 到該方法結束時失效.
  • 代碼塊局部變量: 在代碼塊中定義的局部變量, 這個局部變量的作用域從定義該變量的地方生效, 到該代碼塊結束時失效.

與成員變量不同的是, 局部變量除了形參之外, 都必需顯式初始化.

也就是說, 必需先給方法局部變量和代碼塊局部變量指定初始值, 否則不可以通路它們.

下面我寫個定義代碼塊局部變量的程式.

public class BlockTest
{
  public static void main(String[] args)
  {
    {
      //定義一個代碼塊局部變量 a
      int a;
      //下面代碼将會出現錯誤, 因為 a 變量沒有初始化
      System.out.println("代碼塊局部變量 a 的值:" + a);
    }
    //下面試圖通路 a 變量, 但 a 變量的作用域根本無法涉及這裡
    System.out.println(a);
  }
}
           

上面的代碼是一個錯誤示例, 如果你寫出來還要運作的話, 隻能說你 too yang to simple.

從上面代碼可以看出, 隻要離開了 代碼塊局部變量 所在的代碼塊, 這個局部變量就沒法用了.

對于方法局部變量, 其作用域從定義該變量開始, 直到該方法結束.

下面我寫個 方法局部變量的作用域 示例.

public class MethodLocalVariableTest
{
  public static void main(String[] args)
  {
    //定義一個方法局部變量 a
    int a;
    //下面代碼将會出現錯誤, 因為 a 變量沒有初始化
    System.out.println("方法局部變量 a 的值:" + a);
  }
}
           

下面我們說形參.

形參的作用域時整個方法體内有效, 而且形參也無須顯式初始化.

形參的初始化在調用該方法時由系統完成, 形參的值由方法的調用者負責指定.

在同一個類裡, 成員變量的作用域是整個類内有效.

一個類裡不能定義兩個同名的成員變量.

就算一個是類變量, 一個是執行個體變量也不行.

一個方法裡不能定義兩個同名的成員變量.

方法局部變量與形參名也不能同名.

同一個方法中不同代碼塊内的代碼塊局部變量可以同名.

如果先定義代碼塊局部變量, 後定義方法局部變量.

前面定義的代碼塊局部變量與後面定義的方法局部變量是可以同名的.

Java允許局部變量和成員變量同名.

如果方法裡的局部變量和成員變量同名, 局部變量會覆寫成員變量.

如果需要在這個方法裡引用被覆寫的成員變量.

可以使用 this (對于執行個體變量) 或 類名(對于類變量) 來作為調用者.

下面, 我寫個程式.

public class VariableOverrideTest
{
  //定義一個 name 執行個體變量
  private String name = "豬八戒";
  //定義一個 price 類變量
  private static double price = ;
  //主方法, 程式的入口
  public static void main(String[] args)
  {
    //方法裡的局部變量, 局部變量覆寫成員變量
    int price = ;
    //直接通路 price 變量, 将輸出 price 局部變量的值.
    System.out.println(price);
    //使用類名作為 price 變量的調用者, 通路被覆寫的 類變量
    System.out.println(VariableOverrideTest.price);
    //運作 info 方法
    new VariableOverrideTest().info();
  }
  public void info()
  {
    //方法裡的局部變量, 局部變量覆寫成員變量
    String name = "孫悟空";
    //直接通路 name 變量, 将輸出 孫悟空
    System.out.println(name);
    //使用 this 來作為 name 的調用者, 通路 執行個體變量
    System.out.println(this.name);
  }
}
           

從上面代碼可以看出, 當局部變量覆寫成員變量時.

依然可以在方法中顯式指定 類名和this 作為調用者來通路被覆寫的成員變量, 這使得變成更加自由.

不過, 不過, 不過 . 你應該盡量避免這種局部變量和成員變量同名的情形. (想個名字真的有那麼難麼 - - )

成員變量的初始化和記憶體中的運作機制

當系統加載類或建立該類的執行個體時.

系統将自動為成員變量配置設定記憶體空間.

并在配置設定記憶體空間後, 自動為成員變量指定初始值.

下面通過代碼來建立兩個執行個體(非完整代碼,能明白就行).

同時配合示意圖來說明 Java 成員變量的初始化和記憶體中的運作機制.

//建立第一個 Person 對象
Person p1 = new Person();
//建立第二個 Person 對象
Person p2 = new Person();
//分别為兩個 Person 對象的 name 執行個體變量指派
p1.name = "孫悟空";
p2.name = "皮卡丘";
//分别為兩個 Person 對象的 eyeNum 類變量指派
p1.eyeNum = ;
p2.eyeNum = ;
           

下面開始解讀:

當程式執行第一行代碼 Person p1 = new Person(); 時

如果這行代碼是第一次使用 Person 類.

則系統會加載并初始化這個類.

在類的準備階段.

系統将會為該類的類變量配置設定記憶體空間,并指定預設初始值.

當 Person 類初始化完成後, 系統記憶體中的存儲示意圖如下:

JAVA 面向對象 成員變量和局部變量

從上圖可以看出.

當 Person 類初始化完成後.

系統将在堆記憶體中為 Person 類配置設定一塊記憶體區.

在這塊記憶體區中, 包含了 儲存 eyeNum 類變量的記憶體.

并設定 eyeNum 的預設初始值為: 0

系統接着建立了一個 Person 對象.

并把這個 Person 對象賦給 p1 變量.

Person 對象裡包含了名為 name 的執行個體變量.

執行個體變量是在建立執行個體時配置設定記憶體空間并指定初始值的.

當建立了第一個 Person 對象後, 系統記憶體中的存儲示意圖如下:

JAVA 面向對象 成員變量和局部變量

從上圖可以看出, eyeNum 類變量并不屬于 Person 對象.

它是屬于 Person 類的.

是以建立第一個 Person 對象時并不需要為 eyeNum 類變量配置設定記憶體(廢話…)

系統隻為 name 執行個體變量配置設定了記憶體空間.

并指定預設初始值: null

接着執行 Person p2 = new Person();

代碼建立第二個 Person 對象.

此時因為 Person 類已經存在于堆記憶體了.

是以不需要對 Person 類進行初始化(廢話…Java 會那麼傻麼…)

建立第二個 Person 對象 與 建立第一個 Person 對象并沒有什麼不同.

當程式執行 p1.name = “孫悟空”; 時

将為 p1 的 name 執行個體變量指派.

也就是讓堆記憶體中的 name 指向 “孫悟空” 字元串.

我們之前說過, 字元串也是一種引用變量. 是以你懂的.

執行完成後, 兩個 Person 對象在記憶體中的存儲示意圖如下:

JAVA 面向對象 成員變量和局部變量

從上圖可以看出, name 執行個體變量是屬于單個 Person 執行個體的.

是以, 修改第一個 Person 對象的 name 執行個體變量時僅僅與 p1 對象有關.

與 Person 類和其它 Person 對象沒有任何關聯.

同理, 修改第二個 Person 對象 p2 的 name 執行個體變量時, 也與 Person 類和其它 Person 對象無關.

直到執行 p1.eyeNum = 2 時

此時呢, 就是犯大忌了. 你拿 對象來操作類變量了. 不過為了教學示範, 我拿自己當典型.

從我們看過的圖當中, 可以知道.

Person 的對象根本沒有儲存 eyeNum 這個變量.

通過 p1 通路的 eyeNum 類變量.

其實還是 Person 類的 eyeNum 類變量.

是以, 此時修改的是 Person 類的 eyeNum 類變量.

修改成功後, 記憶體中的存儲示意圖如下:

JAVA 面向對象 成員變量和局部變量

從上圖可以看出.

不管通過哪個 Person 執行個體來通路 eyeNum 類變量.

它們通路的其實都是同一塊記憶體.

是以就再次提醒你.

當程式需要通路 類變量時.

盡量使用類作為主調, 而不要使用對象作為主調.

這樣可以避免歧義, 提高程式的可讀性.

局部變量的初始化和記憶體中的運作機制

局部變量定義後.

必需經過顯式初始化後才能使用.

系統不會為局部變量執行初始化.

這意味着,定義局部變量之後,系統并未為這個變量配置設定記憶體控件.

直到等程式為這個變量賦初始值時.

系統才會為局部變量配置設定記憶體,并将初始值儲存到這塊記憶體中去.

與成員變量不同,局部變量不屬于任何類或執行個體.

是以它總是儲存在其所在的方法的棧記憶體中.

如果局部變量是基本類型變量,則直接把這個變量的值儲存在該變量對應的記憶體中.

如果局部變量是引用類型的變量,則這個變量裡存放的就是位址.

通過該位址引用到該變量實際引用的對象或數組.

變量的使用規則

對于新手來說.

什麼時候使用類變量?

什麼時候使用執行個體變量?

什麼時候使用方法局部變量?

什麼時候使用代碼塊局部變量?

這種選擇比較困難,如果僅僅從程式的運作結果來看,大部分時候都可以直接使用類變量或執行個體變量來解決問題.無須使用局部變量.

但實際上這種做法非常錯誤.

因為定義一個成員變量時,成員變量将被放置到堆記憶體中.

成員變量的作用域将擴大到類存在範圍或對象存在範圍,這種傳回的擴大有兩個害處.

  • 增大了變量的生存時間,這将導緻更大的記憶體開銷(如果你以後開發Android,大記憶體開銷會讓你的APP無情被使用者解除安裝)
  • 擴大了變量的作用域,這不利于提高程式的内聚性. (什麼是内聚性?)
讓我們做個對比, 這樣更好了解.
public class ScopeTest01
{
    //定義一個類成員變量作為循環變量
    static int i;
    public static void main(String[] args)
    {
        for(i = ; i < ; i++)
        {
            System.out.println("Hello");
        }
    }
}
           
public class ScopeTest02
{
    public static void main(String[] args)
    {
        //定義一個方法局部變量作為循環變量
        int i;
        for(i = ; i < ; i++)
        {
            System.out.println("Hello");
        }

    }
}
           
public class ScopeTest03
{
    public static void main(String[] args)
    {
        //定義一個代碼塊局部變量作為循環變量
        for(int i = ; i < ; i++)
        {
            System.out.println("Hello");
        }
    }
}
           

從程式的運作結果上來看, 這三者完全相同.

但程式的效果則有很大差異.

第三個程式最符合軟體開發規範:

對于一個循環變量而言, 隻需要它在循環體内有效即可.

是以隻需要把這個變量放在循環體内(也就是在代碼塊内定義), 進而保證這個變量的作用域僅僅在該代碼塊内.

我們要根據實際需要,來決定變量的作用域.例如下面的例子:

  • 如果需要定義的變量是用于描述某個類或對象的固有資訊的.例如人的身高 / 體重等資訊. 它們時人對象的固有資訊, 每個人對象都具有這些資訊. 這種變量應定義為成員變量.如果這種資訊對于這個類的所有執行個體完全相同,或者說它是類相關的,例如人類的眼睛數量,這種類相關的資訊就應該定義為類變量.如果資訊和執行個體相關,例如人的身高 / 體重等, 每個人執行個體的身高 / 體重可能不同, 這種資訊是執行個體相關的, 就應該定義為執行個體變量.

即使在程式中使用局部變量, 也應該盡可能的縮小局部變量的作用範圍.

局部變量的作用範圍越小, 它在記憶體裡停留的時間就越短, 程式性能就越好.

是以, 能用代碼塊局部變量的地方,就堅決不要使用方法局部變量.