java引數傳遞:值傳遞
1 簡述棧、堆和方法區的用法
通常我們定義一個基本資料型別的變數(非成員變數,物件的成員變數和物件一起放在堆中)、一個物件的引用、還有就是函式呼叫的現場儲存都使用JVM中的棧(stack)空間。
通過new關鍵字和構造器建立的物件則放在堆(heap)空間,沒有被引用的物件就成為“垃圾”,因此堆是垃圾收集器(GC)管理的主要區域。由於現在的垃圾收集器都採用分代收集演算法,所以堆空間還可以細分為新生代和老年代,再具體一點可以分為Eden、Survivor、Tenured。堆是執行緒共享的記憶體區域。
方法區(method area)用於儲存已經被JVM載入的類資訊、常量、靜態變數、JIT編譯器編譯後的程式碼
棧空間操作起來最快但是棧空間很小,通常大量的物件都放在堆空間,棧和堆的大小都是可以通過JVM的啟動引數進行調整。棧空間用完了會引發StackOverflowError,而堆和常量池空間不足會引發OutOfMemoryError。
String str = new String(“Hello”);
上面語句中變數str是一個引用,放在棧空間上的,用new創建出來的字串物件是放在堆空間上的,而“Hello”這個字面量是放在方法區的。棧空間中存放的是new物件在堆空間存放的地址值。
Person p = new Person(1); Class Person{ int a=1; public Person(int a){ this.a=a; } }
上面語句中p是一個指向Person類的例項(物件)的一個引用,放在棧空間中。真正建立物件的語句是new Person(1),這個物件放在堆空間中,對應棧空間p中存放的是這個Person物件在堆空間中的地址。其中a是成員變數,存放在堆空間中的。
若聲明瞭一個int型別的非成員變數a,然後對a進行賦值為1,因為a為基礎資料型別直接儲存在棧空間中,因此棧空間中a對應儲存的就是1。
注意:
引用《java程式設計思想(第四版)》P23頁的一句話:
對於這些基本型別,java採取與C和C++相同的方法,也就是說,不用new來建立變數,而是建立一個並非是引用的“自動”變數。這個變數直接儲存“值”,並置於堆疊中,因此更加高效。
明白了上面關於物件和變數的儲存特點,現在我們來說說什麼是java值傳遞。
2 值傳遞
2.1 什麼是值傳遞?
按值傳遞指的是在方法呼叫時,傳遞的引數是把值的拷貝傳遞過去的。由於傳遞的是值的拷貝,因此按值傳遞的特點就是傳遞過後兩個引數互不相干。
示例如下:
public class Test {
public static void main(String[] args) {
Test test=new Test();
int a=1;
test.sum(a);
System.out.println("main方法裡的a="+a);
}
public void sum(int a){
a=2;
System.out.println("sum方法裡的a="+a);
}
}
執行結果:
sum方法裡的a=2
main方法裡的a=1
2.2 java值傳遞過程分析
(1)當執行到main方法第2行程式碼時:
首先在棧空間中建立一個變數為a,然後將1存入變數a所在記憶體空間中。
(2)當執行到sum方法中時:
main方法將a變數的拷貝傳給sum方法,此時在棧記憶體中會為sum方法中的變數a開闢一塊空間,存入由main方法傳過來的值1。通過a=2操作過後將sum方法中a的值改變成2。因此sum方法中的變數a更改不會影響main方法中的變數a。
圖(1)
3 引用傳遞
3.1 什麼是引用傳遞?
引用傳遞是C語言裡面的一種引數傳遞方式,就是在方法呼叫時,傳遞給另一個方法的是物件的引用,是同一個引用。但是在java語言中,並不存在像C語言這樣的引用傳遞,java語言中只存在值傳遞。之所以很多人把java傳遞物件誤以為是引用傳遞,是因為沒有深入的理解物件建立的記憶體分配規則。在java中傳遞物件,其實是傳遞的物件引用的值的拷貝,這個值也就是物件在堆記憶體中的地址。
Java中物件引用值的傳遞的特點是:兩個引用指向的是同一個物件,即同一個記憶體空間(試想,如果是引用傳遞,則就不會存在兩個引用了,只會有一個引用)。
示例程式碼如下:
public class Test {
public static void main(String[] args) {
Test test=new Test();
Person p;
p=new Person();
p.setAge(5);
test.handle(p);
System.out.println("main方法裡:"+p.toString());
}
public void handle(Person p){
p.setAge(10);
System.out.println("handle方法裡:"+p.toString());
}
}
class Person{
int age=0;
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [age=" + age + "]";
}
}
執行結果:
handle方法裡:Person [age=10]
main方法裡:Person [age=10]
3.2 Java“引用傳遞”過程分析
(1)當執行完main方法第4行時:
此時程式在棧空間中開闢了一塊空間存放Person的一個引用p,並且在堆記憶體裡面建立了一個物件。
圖(2)
(2)當執行完main方法第5行的時:
此時main方法將引用p地址值的一個拷貝傳遞給handle方法,handl方法首先建立一個新的引用p,並且將傳過來的地址值賦值給新引用p。此時兩個引用都指向同一個記憶體空間。
圖(3)
(3)當執行完handle方法的第1行時:
此時handle方法中的引用p,修改了地址值為0000的Person物件的age=10,由於兩個引用指向的是同一個物件,因此main方法中Person物件的age也等於10,因為是同一個Person物件。
圖(4)
假如將handle方法修改成如下程式碼:
public void handle(Person p){
p=new Person();
p.setAge(10);
System.out.println("handle方法裡:"+p.toString());
}
執行結果為:
handle方法裡:Person [age=10]
main方法裡:Person [age=5]
分析過程為:
執行到新handle方法第一行之前,執行過程和上面(1)(2)一樣,只不過當執行完第1行後放生了點變化。handle中重新new了一個Person物件,並且將handle方法中的引用p指向這個物件。
圖(5)
執行完handle方法第2行程式碼後,handle裡面的引用p修改了Person物件的age,此時由於main方法和handle方法中的引用指向了各自的Person物件,因此此次修改操作不會影響main方法中的Person物件,因此:main方法裡:Person [age=5]。
圖(6)
26.4 總結
(1)Java當中引數傳遞都是值傳遞!
(2)對於這些基本型別,java採取與C和C++相同的方法,也就是說,不用new來建立變數,而是建立一個並非是引用的“自動”變數。這個變數直接儲存“值”,並置於堆疊中,因此更加高效。
(3)對於大家理解的引用傳遞,其實傳遞的是引用的值,即傳遞引用物件的地址值。
注意:如有問題請批評指正!
【四川樂山程式設計師聯盟,歡迎大家加群相互交流學習5 7 1 8 1 4 7 4 3】