變數和物件
1.變數
在程式執行過程中,其值能被改變的量稱為變數。在Java中,所有的變數必須宣告才能使用,宣告方式為:變數型別 變數名;例如:int age,聲明瞭一個int型別的age變數。變數在使用前進行宣告是為了告訴編譯器這個變數的資料型別,這樣編譯器才能知道分配多大的記憶體空間給它,以及它能存放什麼樣的型別變數。
根據程式碼能訪問該變數的區域將變數分為成員變數和區域性變數。成員變數在整個類中都有效,類的成員變數又分為靜態變數和例項變數。靜態變數是被static修飾的變數,有效範圍可以跨類,使用類名.靜態變數名訪問。區域性變數是在方法體中定義的變數,只在當前程式碼塊中有效,即區域性變數的生命週期取決於當前方法的呼叫。結合JVM來看,成員變數存放在執行時資料區的方法區中,區域性變數存放在Java虛擬機器棧中。
2.物件和變數之間的關係
類是封裝物件的屬性和行為的載體,而物件的屬性以成員變數的形式存在。物件的方法以成員方法的形式存在,在成員方法內定義的變數為區域性變數。
3.物件的建立過程
在程式碼中,new操作符呼叫構造方法建立物件。那麼虛擬機器中物件是怎樣建立的?
①虛擬機器在遇到new指令時,首先檢查該指令引數是否能在方法區的常量池中定位到一個符號,並且檢查這個符號引用是否已經被類載入,解析和初試化過,若沒有,應先進行相應的類載入過程。
- 載入是取得類的二進位制流,並且將類資訊轉換為方法區的執行時資料結構,最後在堆中生成一個java.lang.Class物件,用作訪問方法區中該類的資訊的入口。
- 解析是將常量池內的符號引用轉換為直接引用。
- 初始化階段執行類中定義的Java程式碼程式,包括為變數賦值(不是賦預設值)。
②為新生物件分配記憶體
如何為物件確認記憶體大小?物件在堆記憶體中的儲存區域包括三部分,物件頭,例項資料以及對齊填充。物件頭中主要包括兩種資訊,一種用於儲存物件自身的執行時資料,比如雜湊碼,GC分代年齡,鎖狀態標誌等。另一種儲存型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標確定這個物件是哪個類的例項。例項資料是物件真正儲存的有效資訊,也就是程式程式碼中定義的各種型別的欄位資訊。對齊填充不是必須的。由於JVM系統要求物件的起始地址必須是8位元組的整數倍,也就是說物件大小是8位元組的倍數,由於物件頭的大小剛好是8位元組倍數,因此有的例項資料部分需要對齊填充來佔位。
③將分配到的記憶體空間初始化為零值(物件頭不包括)。
④對物件頭進行必要的設定,例如這個物件是哪個類的例項,如何才能找到元資料資訊,物件的雜湊碼,物件的GC分代年齡等資訊。
自此,從虛擬機器視角來看,一個新的物件就產生了。接下來,虛擬機器在new指令之後執行<init>方法,將物件按照程式碼中的值進行初始化。
4.物件的訪問定位
建立物件是為了使用,使用之前必須先要找到該物件,即對物件的訪問定位。目前的主流方式有使用控制代碼和直接指標兩種定位方式。
如果使用控制代碼訪問的話,Java堆中會劃分出一塊記憶體區域作為控制代碼池,reference中存的就是物件的控制代碼地址,控制代碼地址中儲存了到物件例項資料的地址資訊以及到物件型別資料的地址資訊,分別指向Java堆例項池中的物件例項資料和方法區中的物件型別資料。
如果使用直接指標的定位方式,那麼在Java堆物件的佈局中必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。
5.new物件放在迴圈內和迴圈外
5.1 先來看看物件的建立。如下的示例程式碼:
public class VariableTest { int a; static int b=12; public VariableTest(){ System.out.println("構造方法執行:"); } public static void main(String[] args){ //vt-->物件的引用,儲存在Java棧的本地變量表中 // new VariableTest()-->物件,儲存在堆中,每次new VariableTest() new出來的物件都不一樣。 VariableTest vt=new VariableTest(); VariableTest vt1=new VariableTest(); System.out.println(vt); //[email protected] System.out.println(new VariableTest()); //@6d06d69c System.out.println(new VariableTest()); //7852e922 System.out.println(new VariableTest()); //@4e25154f System.out.println(vt1); //@70dea4e } }
在 VariableTest類中建立了構造方法public VariableTest(){},程式中使用new 構造方法就建立了物件。
VariableTest vt=new VariableTest(); vt為物件的引用,也就是物件的訪問定位中提到的Java棧本地變量表中儲存的reference引用,new VariableTest(); new指標呼叫構造方法建立一個物件,也就是圖中Java堆中的例項資料物件,vt物件引用指向例項資料物件。
接著程式碼中又new出一個物件,VariableTest vt1=new VariableTest(); 打印出vt和vt1的值是不一樣的,也就是說vt和vt1是Java棧本地變量表中儲存的兩個不同的reference引用,那麼vt1 後面的new VariableTest(); 和vt 後面的new VariableTest();是否是同一個物件呢?打印出該物件,發現只要new VariableTest();就會建立一個物件,每次建立的物件都是不同的。
5.2 new物件在迴圈內和迴圈外
如下的示例程式碼:
public static void m1(){ HashMap<Integer,String> hashMap=null; hashMap=new HashMap<Integer,String>(); for(int j=0;j<b;j++){ hashMap.put(j,"A"); } for(Map.Entry<Integer,String> entry:hashMap.entrySet()){ System.out.println(entry.getKey()+":"+entry.getValue()); } }
在VariableTest類中新建了一個成員方法m1(),在方法內建立了一個HashMap<Integer,String>型別的物件hashMap,for迴圈中存入資料然後遍歷,整個過程只new出一個物件,所以for迴圈每次存資料都是向同一個hashMap中存入資料。遍歷結果:
0:A 1:A 2:A 3:A 4:A 5:A 6:A 7:A 8:A 9:A 10:A 11:A
若改為入下的示例程式碼:
public static void m1(){ HashMap<Integer,String> hashMap=null; for(int j=0;j<b;j++){ hashMap=new HashMap<Integer,String>(); hashMap.put(j,"A"); } for(Map.Entry<Integer,String> entry:hashMap.entrySet()){ System.out.println(entry.getKey()+":"+entry.getValue()); } }
將new HashMap<Integer,String>();放在迴圈內,之前分析每次new出來的物件都是不同的,所以在for迴圈的程式碼塊中,一次迴圈結束,該次迴圈new出來的物件就已經銷燬。也就是提前宣告的hashMap引用每次指向的物件都是不同的,一次for迴圈結束new出來的物件銷燬之後hashMap引用指向下一次for迴圈new出來的 HashMap<Integer,String>()物件。遍歷結果:
11:A