Java中類,對象,方法的內存分配
以下針對引用數據類型:
在內存中,類是靜態的概念,它存在於內存中的CodeSegment中。
當我們使用new關鍵字生成對象時,JVM根據類的代碼,去堆內存中開辟一塊控件,存放該對象,該對象擁有一些屬性,擁有一些方法。但是同一個類的對象和對象之間並不是沒有聯系的,看下面的例子:
1 class Student{ 2 static String schoolName; 3 String name; 4 int age; 5 6 void speak(String name){ 7 System.out.println(name); 8 } 9 10 void read(){ 11 } 12 } 13 class Test{ 14 public statis void main(String[] args){ 15 Student a = new Student(); 16 Student b = new Student(); 17 } 18 }
在上面的例子中,生成了兩個Student對象,兩個對象擁有各自的name和age屬性,而schoolName屬性由於是static的,被所有的對象所共有,而且每個對象都有權更改這個屬性。
而對於方法speak()和read()來講,它們被所有的對象所共有,但並不是說哪個對象擁有這些方法。方法僅僅是一種邏輯片段而已,它不是實物,嚴格來講不能被擁有。
方法存在的意義就是對對象進行修改。(我的理解)
上述speak()方法,雖然兩個對象都擁有相同的方法,但是由於其操作的對象不同,所以執行起來的效果也不同。再說一次,方法是看不見摸不着的,它僅僅是一種邏輯操作!只有當作用於具體的對象時,方法才具體化了!
方法在不執行的時候不占用內存空間,只有在執行的時候才會占用內存空間。
就好比說一個人會翻跟斗,他翻跟斗的時候是需要空間的,但是他不翻跟斗的時候是不需要額外的空間的。但是不管他翻不翻跟斗,他始終是具有翻跟斗的技能的。
Java中的內存布局(其他面向對象的語言也是如此)
Java中的內存空間分為四種:
1. code segment
存儲class文件的內容,即我們寫的代碼。
2. data segment
存儲靜態變量
3. heap segment
堆空間,存儲new出來的對象
4. stack segment
棧空間,存儲引用類型的引用(注意,這里存儲的不一定是對象所處的物理地址,但是一定能夠根據這個內容在堆中找到對應的對象),局部變量和方法執行時的方法的代碼
以上面的speak(String name)方法的調用為例來分析下內存:
調用該方法時,首先在棧控件開辟了一塊區域存放name引用。然后將傳入的那個對象的“地址”賦值給這個引用。於是出現了什么情況?兩個引用指向同一個對象。而我們操作對象時是通過對象的引用來執行操作,所以當一個對象有一個以上的引用同時指向它時,就會出現一些比較混亂的事情了。
通過馬士兵在課上講的一個小例子來看看:
1 public class Test { 2 3 public static void main(String[] args) { 4 Test test = new Test(); 5 int data = 10; 6 BirthDate b1 = new BirthDate(5,4,1993); 7 BirthDate b2 = new BirthDate(25,5,1992); 8 9 test.change(data); 10 test.change1(b1); 11 test.change2(b2); 12 System.out.println(data); 13 b1.display(); 14 b2.display(); 15 } 16 17 void change(int i){ 18 i = 100; 19 } 20 21 void change1(BirthDate b){ 22 b = new BirthDate(1,1,1); 23 } 24 25 void change2(BirthDate b){ 26 b.setDay(100); 27 } 28 } 29 30 class BirthDate{ 31 int day; 32 int month; 33 int year; 34 35 BirthDate(int day,int month,int year){ 36 this.day = day; 37 this.month = month; 38 this.year = year; 39 } 40 41 public int getDay() { 42 return day; 43 } 44 public void setDay(int day) { 45 this.day = day; 46 } 47 public int getMonth() { 48 return month; 49 } 50 public void setMonth(int month) { 51 this.month = month; 52 } 53 public int getYear() { 54 return year; 55 } 56 public void setYear(int year) { 57 this.year = year; 58 } 59 60 public void display(){ 61 System.out.println("day = "+day+"\nmonth = "+month+"\nyear = "+year); 62 } 63 }
輸出為:
10 day = 5 month = 4 year = 1993 day = 100 month = 5 year = 1992
從輸出結果來看看發生了什么:
1. 調用change(int i)
此時在棧空間中新建了一個i,並把data的值復制給i。這個時候在棧空間中有兩個int類型的數,二者的值雖然都為10,但是二者毫無關系,棧空間中有兩個10.
在方法體中對i進行賦值,該操作是對i進行的,並不影響data,所以當方法結束時data還是原來的data,連地址都沒變一下。同時在方法結束時i自動從棧空間中消失。
2. 調用change1(BirthDate b)
此時在棧空間中新建一個引用b,在調用該方法的時候,將傳進來的引用的值復制給b,即b和b1擁有相同的內容,指向同一個對象。在方法體中對b又進行賦值操作,首先在堆空間中new出一個新的對象,然后將b改為指向這個新的對象。該操作也未影響b1。方法結束后,引用b消失,剛才new出來的新對象成了垃圾,等待GC的回收。
3. 調用change2(BirthDate b)
同上,先在對中新建一個引用,名為b,然后將其指向b2所指向的對象。注意,此時調用了該對象的方法,修改了部分屬性,所以此操作改變了這個對象,而b2也指向這個對象,所以最后b2的輸出發生了變化。
從上面的三個方法可以看出,當方法中的參數列表不為空時:
- 如果參數是引用數據類型,該方法執行的過程首先是創建若干個引用,然后將這些引用的值和傳進來的引用的值一一對應復制。復制完之后,傳進來的參數(引用)的工作也就完成了。
- 如果參數是基本數據類型,那么首先在棧中創建對應數量個變量,將這些變量的值和傳進來的參數一一對應復制。復制完之后也沒有外面什么事了。
綜上:傳參時要注意,如果對傳進去的參數(實際上是引用)進行了重新的賦值操作,那么該方法應該有一個返回值,否則該方法是沒有意義的,如同上面的change1()。
- 方法一般有兩個作用:1. 對某變量進行改變。 2. 根據傳進來的參數返回另一變量。
- 如果一個方法不想有返回值,只是想對某變量進行改變,不要將該對象作為參數傳進去,而直接在方法中獲得其訪問權限然后直接更改。方法的參數列表為空。
- 如果一個方法要有返回值,最好先在方法內部new一個臨時變量,先將傳進來的參數復制一下,邏輯執行完后,把臨時變量return出去。
20170620更新:
其實以上問題涉及到的東西是值傳遞與引用傳遞。在C++中二者都有,但是在Java中只有值傳遞。具體到實踐中分兩種情況:
- 傳遞的是基本數據類型:
其實傳遞的是值的拷貝。在方法中對值進行操作,並不影響傳進去的那個值。如上面的change()方法,傳值進去時只是按照data的樣子重新創建了一個i,本質上data和i除了值相同以外,是兩個獨立的個體。
- 傳遞的是數組對象或者其他對象:
實際上傳遞的是對象的引用,但是並不是把引用傳過去,而是把引用復制過去。就像上面的change1()方法一樣,其本質是將傳參b1這個引用的值復制給引用b。b1和b除了值相同外,是兩個獨立的個體。但是由於二者值相同,所以指向了堆內存中的同一個對象,二者都可以用來操作對象。
總結一下:
傳值,傳的都是棧中所儲存的東西的拷貝。如果傳進去的東西是基本數據類型,那么就直接復制一份,對其操作不影響原來的數據。
如果傳進去的是一個引用,那么其實也是復制一份,所以指向同一對象。當操作這個引用時,改變了這個引用所指向的對象,看起來會讓人覺得當時傳進去的是對象本身,不然怎么在方法中對其修改會改變原本的對象呢?其實這是個假象。時刻記住,傳進函數的都是棧內存中的東西,堆內存的東西是不會被傳進去的。而函數內部能不能改變原來對象的值,就要看你是不是保持了原來傳進去的引用所指向的對象沒變。
PS:才疏學淺,如有錯誤請指出,謝謝!
