1. Java 的值傳遞
通常認爲Java 方法傳參數都是值傳遞,關於值傳遞
的定義如下:
在方法被調用時,實參通過形參把它的
內容副本傳入方法內部
,此時形參接收到的內容是實參值的一個拷貝,因此在方法內對形參的任何操作,都僅僅是對這個副本的操作,不影響原始值的內容
簡單的代碼示例和打印結果如下
public static void main(String[] args) {
int a = 10;
changeTest(a);
System.out.println("main:" + a);
}
public static void changeTest(int src) {
System.out.println("changeTest before:" + src);
src++;
System.out.println("changeTest after:" + src);
}
changeTest before:10
changeTest after:11
main:10
以上變量a定義爲基本數據類型 int,我們知道它是存儲在虛擬機棧內存中的
。虛擬機棧是Java方法執行的內存模型,棧中存放着棧幀,每個棧幀分別對應一個被調用的方法,方法的調用過程其實就是棧幀在虛擬機中入棧到出棧的過程。當程序中某個線程開始執行一個方法時就會相應的創建一個棧幀並且入棧(位於棧頂),在方法結束後,棧幀出棧
棧幀
: 用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素,每個棧幀中包括:
1.局部變量表:用來存儲方法中的局部變量(非靜態變量、函數形參)。當變量爲基本數據類型時,直接存儲值,當變量爲引用類型時,存儲的是指向具體對象的引用
2.操作數棧: Java虛擬機的解釋執行引擎被稱爲"基於棧的執行引擎",其中的棧就是指操作數棧
3.指向運行時常量池的引用:存儲程序執行時可能用到常量的引用
4.方法返回地址:存儲方法執行完成後的返回地址
瞭解以上情況後其實很好解釋示例代碼的結果:
- 首先調用
mian()
方法,此時JVM爲main()
方法往虛擬機棧中壓入一個棧幀,即爲當前棧幀,用來存放main()
中的局部變量表(包括參數)、操作棧、方法出口等信息,如a
是mian()
方法中的局部變量,此時虛擬機棧如圖所示
- 當執行到
changeTest()
方法時,JVM也爲其往虛擬機棧中壓入一個棧幀,用來存放changeTest()
中的局部變量等信息,因此方法參數src
是在changeTest()
方法所在的棧幀中,而其值是從a
複製得到的。此時虛擬機棧如圖所示,在changeTest()
方法棧幀內部操作變量src
顯然不會影響到main()
方法棧幀變量a
2. Java 的"引用傳遞"
引用傳遞
的定義如下:
引用也就是指向真實內容的地址值,在方法調用時,實參的地址通過方法調用被傳遞給相應的形參,在方法體內,形參和實參指向同一塊內存地址,對形參的操作會影響的真實內容
上文解釋了值傳遞
的過程,但是以下方法入參爲引用類型數據的示例代碼1
的打印結果與其明顯不符。可以看到,在 main() 方法中,List
類型變量 a
沒有做任何的元素操作,但是經過 changeTest()
方法添加了元素 99之後,main()
方法中的List
變量 a
也被加入了元素 99。內容發生了改變,這似乎完全符合“引用傳遞”的定義,對形參的操作,改變了實際對象的內容
public static void main(String[] args) {
List<Integer> a = new ArrayList<>();
changeTest(a);
System.out.println("main:" + a);
}
public static void changeTest(List<Integer> src) {
System.out.println("changeTest before:" + src);
src.add(99);
System.out.println("changeTest after:" + src);
}
changeTest before:[]
changeTest after:[99]
main:[99]
但當我們使用幾乎完全相同的示例代碼2
,只是添加一行代碼之後,結果又不一樣了:main() 方法中的List
類型變量 a
的內容並沒有被 changTest() 方法中的操作改變
public static void main(String[] args) {
List<Integer> a = new ArrayList<>();
changeTest(a);
System.out.println("main:" + a);
}
public static void changeTest(List<Integer> src) {
System.out.println("changeTest before:" + src);
// 添加以下一行代碼
src = new ArrayList<>();
src.add(99);
System.out.println("changeTest after:" + src);
}
changeTest before:[]
changeTest after:[99]
main:[]
要解釋以上結果,首先我們要知道變量a定義爲對象類型 List,它是存儲在虛擬機堆內存中的
。JVM 中堆內存是一塊線程共享的區域,對象和數組一般都分配在這一塊內存中。這樣,我們就可以很容易地解釋:
- 程序執行到
main()
方法中的代碼List<Integer> a = new ArrayList<>();
時,JVM會在堆內開闢一塊內存,用來存儲List
對象的所有內容,同時在main()
方法所在線程的棧幀中創建一個引用a
存儲堆區中List
對象的內存地址,如圖
- 對於
示例代碼1
,執行到changeTest()
方法時,JVM也爲其往虛擬機棧中壓入一個棧幀,其中變量src
的值爲main()
方法棧幀中變量a
的副本,故其指向了同一塊內存區域
- 對於
示例代碼2
,執行到changeTest()
方法時,在方法內部的代碼src = new ArrayList<>();
重複了步驟 1 的過程,首先在堆中開闢內存,存儲新建的對象,再將新對象內存地址賦值給變量src
。這樣在對src
的操作其實是在操作新對象的內存地址,也就是改變新對象的內容
結語
通過以上分析,我們知道在Java中所有的參數傳遞,不管基本類型還是引用類型,都是值傳遞,只是在這個過程也分爲兩種情況:
1.如果是對基本數據類型的數據進行操作,
由於原始內容和副本都是存儲實際值,並且是在不同的棧幀內
,因此形參的操作,不影響原始內容
2.如果是對引用類型的數據進行操作,分兩種情況,一種是形參和實參保持指向同一個對象地址
,則形參的操作,會影響實參指向的對象的內容。一種是形參被改動指向新的對象地址(如重新賦值引用)
,則形參的操作,不會影響實參指向的對象的內容