本文旨在用最通俗的語言講述最枯燥的基本知識(shí)
創(chuàng)新互聯(lián)建站,為您提供網(wǎng)站建設(shè)、網(wǎng)站制作公司、網(wǎng)站營銷推廣、網(wǎng)站開發(fā)設(shè)計(jì),對(duì)服務(wù)混凝土攪拌罐等多個(gè)行業(yè)擁有豐富的網(wǎng)站建設(shè)及推廣經(jīng)驗(yàn)。創(chuàng)新互聯(lián)建站網(wǎng)站建設(shè)公司成立于2013年,提供專業(yè)網(wǎng)站制作報(bào)價(jià)服務(wù),我們深知市場的競爭激烈,認(rèn)真對(duì)待每位客戶,為客戶提供賞心悅目的作品。 與客戶共同發(fā)展進(jìn)步,是我們永遠(yuǎn)的責(zé)任!
學(xué)過Java基礎(chǔ)的人都知道:值傳遞和引用傳遞是初次接觸Java時(shí)的一個(gè)難點(diǎn),有時(shí)候記得了語法卻記不得怎么實(shí)際運(yùn)用,有時(shí)候會(huì)的了運(yùn)用卻解釋不出原理,而且坊間討論的話題又是充滿爭議:有的論壇帖子說Java只有值傳遞,有的博客說兩者皆有;這讓人有點(diǎn)摸不著頭腦,下面我們就這個(gè)話題做一些探討,對(duì)書籍、對(duì)論壇博客的說法,做一次考證,以得出信得過的答案。
其實(shí),對(duì)于值傳遞和引用傳遞的語法和運(yùn)用,百度一下,就能出來可觀的解釋和例子數(shù)目,或許你看一下例子好像就懂,但是當(dāng)你參加面試,做一道這個(gè)知識(shí)點(diǎn)的筆試題時(shí)感覺自己會(huì),胸有成熟的寫了答案,卻發(fā)現(xiàn)是錯(cuò)的,或者是你根本不會(huì)做。
是什么原因?
那是因?yàn)槟銓?duì)知識(shí)點(diǎn)沒有了解透徹,只知道其皮毛。要熟讀一個(gè)語法很簡單,要理解一行代碼也不難,但是能把學(xué)過的知識(shí)融會(huì)貫通,串聯(lián)起來理解,那就是非常難了,在此,關(guān)于值傳遞和引用傳遞,小編會(huì)從以前學(xué)過的基礎(chǔ)知識(shí)開始,從內(nèi)存模型開始,一步步的引出值傳遞和引用傳遞的本質(zhì)原理,故篇幅較長,知識(shí)點(diǎn)較多,望讀者多有包涵。
我們先來重溫一組語法:
形參:方法被調(diào)用時(shí)需要傳遞進(jìn)來的參數(shù),如:func(int a)中的a,它只有在func被調(diào)用期間a才有意義,也就是會(huì)被分配內(nèi)存空間,在方法func執(zhí)行完成后,a就會(huì)被銷毀釋放空間,也就是不存在了
實(shí)參:方法被調(diào)用時(shí)是傳入的實(shí)際值,它在方法被調(diào)用前就已經(jīng)被初始化并且在方法被調(diào)用時(shí)傳入。
舉個(gè)栗子:
1
public
static
void
func
(
int
a)
{
2
a=
20
;
3
System.out.println(a);
4
}
5
public
static
void
main
(String[] args)
{
6
int
a=
10
;
//實(shí)參
7
func(a);
8
}
例子中
int a=10;中的a在被調(diào)用之前就已經(jīng)創(chuàng)建并初始化,在調(diào)用func方法時(shí),他被當(dāng)做參數(shù)傳入,所以這個(gè)a是實(shí)參。
而func(int a)中的a只有在func被調(diào)用時(shí)它的生命周期才開始,而在func調(diào)用結(jié)束之后,它也隨之被JVM釋放掉,,所以這個(gè)a是形參。
所謂數(shù)據(jù)類型,是編程語言中對(duì)內(nèi)存的一種抽象表達(dá)方式,我們知道程序是由代碼文件和靜態(tài)資源組成,在程序被運(yùn)行前,這些代碼存在在硬盤里,程序開始運(yùn)行,這些代碼會(huì)被轉(zhuǎn)成計(jì)算機(jī)能識(shí)別的內(nèi)容放到內(nèi)存中被執(zhí)行。
因此
數(shù)據(jù)類型實(shí)質(zhì)上是用來定義編程語言中相同類型的數(shù)據(jù)的存儲(chǔ)形式,也就是決定了如何將代表這些值的位存儲(chǔ)到計(jì)算機(jī)的內(nèi)存中。
所以,數(shù)據(jù)在內(nèi)存中的存儲(chǔ),是根據(jù)數(shù)據(jù)類型來劃定存儲(chǔ)形式和存儲(chǔ)位置的。
那么
Java的數(shù)據(jù)類型有哪些?
基本類型:編程語言中內(nèi)置的最小粒度的數(shù)據(jù)類型。它包括四大類八種類型:
4種整數(shù)類型:byte、short、int、long
2種浮點(diǎn)數(shù)類型:float、double
1種字符類型:char
1種布爾類型:boolean
引用類型:引用也叫句柄,引用類型,是編程語言中定義的在句柄中存放著實(shí)際內(nèi)容所在地址的地址值的一種數(shù)據(jù)形式。它主要包括:
類
接口
數(shù)組
有了數(shù)據(jù)類型,JVM對(duì)程序數(shù)據(jù)的管理就規(guī)范化了,不同的數(shù)據(jù)類型,它的存儲(chǔ)形式和位置是不一樣的,要想知道JVM是怎么存儲(chǔ)各種類型的數(shù)據(jù),就得先了解JVM的內(nèi)存劃分以及每部分的職能。
Java語言本身是不能操作內(nèi)存的,它的一切都是交給JVM來管理和控制的,因此Java內(nèi)存區(qū)域的劃分也就是JVM的區(qū)域劃分,在說JVM的內(nèi)存劃分之前,我們先來看一下Java程序的執(zhí)行過程,如下圖:
我們接著來了解一下每部分的原理以及具體用來存儲(chǔ)程序執(zhí)行過程中的哪些數(shù)據(jù)。
1. 虛擬機(jī)棧
虛擬機(jī)棧是Java方法執(zhí)行的內(nèi)存模型,棧中存放著棧幀,每個(gè)棧幀分別對(duì)應(yīng)一個(gè)被調(diào)用的方法,方法的調(diào)用過程對(duì)應(yīng)棧幀在虛擬機(jī)中入棧到出棧的過程。
棧是線程私有的,也就是線程之間的棧是隔離的;當(dāng)程序中某個(gè)線程開始執(zhí)行一個(gè)方法時(shí)就會(huì)相應(yīng)的創(chuàng)建一個(gè)棧幀并且入棧(位于棧頂),在方法結(jié)束后,棧幀出棧。
下圖表示了一個(gè)Java棧的模型以及棧幀的組成:
每個(gè)棧幀中包括:
局部變量表 :用來存儲(chǔ)方法中的局部變量(非靜態(tài)變量、函數(shù)形參)。當(dāng)變量為基本數(shù)據(jù)類型時(shí),直接存儲(chǔ)值,當(dāng)變量為引用類型時(shí),存儲(chǔ)的是指向具體對(duì)象的引用。
操作數(shù)棧 :Java虛擬機(jī)的解釋執(zhí)行引擎被稱為"基于棧的執(zhí)行引擎",其中所指的棧就是指操作數(shù)棧。
指向運(yùn)行時(shí)常量池的引用 :存儲(chǔ)程序執(zhí)行時(shí)可能用到常量的引用。
方法返回地址 :存儲(chǔ)方法執(zhí)行完成后的返回地址。
2. 堆:
堆是用來存儲(chǔ)對(duì)象本身和數(shù)組的,在JVM中只有一個(gè)堆,因此,堆是被所有線程共享的。
3. 方法區(qū):
方法區(qū)是一塊所有線程共享的內(nèi)存邏輯區(qū)域,在JVM中只有一個(gè)方法區(qū),用來存儲(chǔ)一些線程可共享的內(nèi)容,它是線程安全的,多個(gè)線程同時(shí)訪問方法區(qū)中同一個(gè)內(nèi)容時(shí),只能有一個(gè)線程裝載該數(shù)據(jù),其它線程只能等待。
方法區(qū)可存儲(chǔ)的內(nèi)容有:類的全路徑名、類的直接超類的權(quán)全限定名、類的訪問修飾符、類的類型(類或接口)、類的直接接口全限定名的有序列表、常量池(字段,方法信息,靜態(tài)變量,類型引用(class))等。
4. 本地方法棧:
本地方法棧的功能和虛擬機(jī)棧是基本一致的,并且也是線程私有的,它們的區(qū)別在于虛擬機(jī)棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧是為執(zhí)行本地方法服務(wù)的。
有人會(huì)疑惑:什么是本地方法?為什么Java還要調(diào)用本地方法?
5. 程序計(jì)數(shù)器:
線程私有的。
記錄著當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,在程序運(yùn)行過程中,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成。
從上面程序運(yùn)行圖我們可以看到,JVM在程序運(yùn)行時(shí)的內(nèi)存分配有三個(gè)地方:
堆
棧
靜態(tài)方法區(qū)
常量區(qū)
相應(yīng)地,每個(gè)存儲(chǔ)區(qū)域都有自己的內(nèi)存分配策略:
堆式:
棧式
靜態(tài)
我們已經(jīng)知道:Java中的數(shù)據(jù)類型有基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,那么這些數(shù)據(jù)的存儲(chǔ)都使用哪一種策略呢?
這里要分以下的情況進(jìn)行探究:
1. 基本數(shù)據(jù)類型的存儲(chǔ):
A. 基本數(shù)據(jù)類型的局部變量
B. 基本數(shù)據(jù)類型的成員變量
C. 基本數(shù)據(jù)類型的靜態(tài)變量
2. 引用數(shù)據(jù)類型的存儲(chǔ)
1. 基本數(shù)據(jù)類型的存儲(chǔ)
我們分別來研究一下:
定義基本數(shù)據(jù)類型的局部變量以及數(shù)據(jù)都是直接存儲(chǔ)在內(nèi)存中的棧上,也就是前面說到的
“虛擬機(jī)?!?
,數(shù)據(jù)本身的值就是存儲(chǔ)在??臻g里面。
如上圖,在方法內(nèi)定義的變量直接存儲(chǔ)在棧中,如
1
int
age=
50
;
2
int
weight=
50
;
3
int
grade=
6
;
當(dāng)我們寫“int age=50;”,其實(shí)是分為兩步的:
1
int
age;
//定義變量
2
age=
50
;
//賦值
首先JVM創(chuàng)建一個(gè)名為age的變量,存于局部變量表中,然后去棧中查找是否存在有字面量值為50的內(nèi)容,如果有就直接把a(bǔ)ge指向這個(gè)地址,如果沒有,JVM會(huì)在棧中開辟一塊空間來存儲(chǔ)“50”這個(gè)內(nèi)容,并且把a(bǔ)ge指向這個(gè)地址。因此我們可以知道:
我們聲明并初始化基本數(shù)據(jù)類型的局部變量時(shí),變量名以及字面量值都是存儲(chǔ)在棧中,而且是真實(shí)的內(nèi)容。
我們?cè)賮砜础癷nt weight=50;”,按照剛才的思路:字面量為50的內(nèi)容在棧中已經(jīng)存在,因此weight是直接指向這個(gè)地址的。由此可見: 棧中的數(shù)據(jù)在當(dāng)前線程下是共享的 。
那么如果再執(zhí)行下面的代碼呢?
1
weight=
40
;
當(dāng)代碼中重新給weight變量進(jìn)行賦值時(shí),JVM會(huì)去棧中尋找字面量為40的內(nèi)容,發(fā)現(xiàn)沒有,就會(huì)開辟一塊內(nèi)存空間存儲(chǔ)40這個(gè)內(nèi)容,并且把weight指向這個(gè)地址。由此可知:
基本數(shù)據(jù)類型的數(shù)據(jù)本身是不會(huì)改變的,當(dāng)局部變量重新賦值時(shí),并不是在內(nèi)存中改變字面量內(nèi)容,而是重新在棧中尋找已存在的相同的數(shù)據(jù),若棧中不存在,則重新開辟內(nèi)存存新數(shù)據(jù),并且把要重新賦值的局部變量的引用指向新數(shù)據(jù)所在地址。
成員變量:顧名思義,就是在類體中定義的變量。
看下圖:
我們看per的地址指向的是堆內(nèi)存中的一塊區(qū)域,我們來還原一下代碼:
1
public
class
Person
{
2
private
int
age;
3
private
String name;
4
private
int
grade;
5
//篇幅較長,省略setter getter方法
6
static
void
run
()
{
7
System.out.println(
"run...."
);
8
};
9
}
10
11
//調(diào)用
12
Person per=
new
Person();
同樣是局部變量的age、name、grade卻被存儲(chǔ)到了堆中為per對(duì)象開辟的一塊空間中。因此可知: 基本數(shù)據(jù)類型的成員變量名和值都存儲(chǔ)于堆中,其生命周期和對(duì)象的是一致的。
前面提到 方法區(qū) 用來存儲(chǔ)一些共享數(shù)據(jù),因此 基本數(shù)據(jù)類型的靜態(tài)變量名以及值存儲(chǔ)于方法區(qū)的運(yùn)行時(shí)常量池中,靜態(tài)變量隨類加載而加載,隨類消失而消失
2. 引用數(shù)據(jù)類型的存儲(chǔ):
上面提到:堆是用來存儲(chǔ)對(duì)象本身和數(shù)組,而引用(句柄)存放的是實(shí)際內(nèi)容的地址值,因此通過上面的程序運(yùn)行圖,也可以看出,當(dāng)我們定義一個(gè)對(duì)象時(shí)
1
Person per=
new
Person();
實(shí)際上,它也是有兩個(gè)過程:
1
Person per;
//定義變量
2
per=
new
Person();
//賦值
在執(zhí)行Person per;時(shí),JVM先在虛擬機(jī)棧中的變量表中開辟一塊內(nèi)存存放per變量,在執(zhí)行per=new Person()時(shí),JVM會(huì)創(chuàng)建一個(gè)Person類的實(shí)例對(duì)象并在堆中開辟一塊內(nèi)存存儲(chǔ)這個(gè)實(shí)例,同時(shí)把實(shí)例的地址值賦值給per變量。因此可見:
對(duì)于引用數(shù)據(jù)類型的對(duì)象/數(shù)組,變量名存在棧中,變量值存儲(chǔ)的是對(duì)象的地址,并不是對(duì)象的實(shí)際內(nèi)容。
前面已經(jīng)介紹過形參和實(shí)參,也介紹了數(shù)據(jù)類型以及數(shù)據(jù)在內(nèi)存中的存儲(chǔ)形式,接下來,就是文章的主題:值傳遞和引用的傳遞。
值傳遞:
在方法被調(diào)用時(shí),實(shí)參通過形參把它的內(nèi)容副本傳入方法內(nèi)部,此時(shí)形參接收到的內(nèi)容是實(shí)參值的一個(gè)拷貝,因此在方法內(nèi)對(duì)形參的任何操作,都僅僅是對(duì)這個(gè)副本的操作,不影響原始值的內(nèi)容。
來看個(gè)例子:
1
public
static
void
valueCrossTest
(
int
age,
float
weight)
{
2
System.out.println(
"傳入的age:"
+age);
3
System.out.println(
"傳入的weight:"
+weight);
4
age=
33
;
5
weight=
89.5f
;
6
System.out.println(
"方法內(nèi)重新賦值后的age:"
+age);
7
System.out.println(
"方法內(nèi)重新賦值后的weight:"
+weight);
8
}
9
10
//測試
11
public
static
void
main
(String[] args)
{
12
int
a=
25
;
13
float
w=
77.5f
;
14
valueCrossTest(a,w);
15
System.out.println(
"方法執(zhí)行后的age:"
+a);
16
System.out.println(
"方法執(zhí)行后的weight:"
+w);
17
}
輸出結(jié)果:
1
傳入的age:
25
2
傳入的weight:
77.5
3
4
方法內(nèi)重新賦值后的age:
33
5
方法內(nèi)重新賦值后的weight:
89.5
6
7
方法執(zhí)行后的age:
25
8
方法執(zhí)行后的weight:
77.5
從上面的打印結(jié)果可以看到:
a和w作為實(shí)參傳入valueCrossTest之后,無論在方法內(nèi)做了什么操作,最終a和w都沒變化。
這是什么造型呢?!!
下面我們根據(jù)上面學(xué)到的知識(shí)點(diǎn),進(jìn)行詳細(xì)的分析:
首先程序運(yùn)行時(shí),調(diào)用mian()方法,此時(shí)JVM為main()方法往虛擬機(jī)棧中壓入一個(gè)棧幀,即為當(dāng)前棧幀,用來存放main()中的局部變量表(包括參數(shù))、操作棧、方法出口等信息,如a和w都是mian()方法中的局部變量,因此可以斷定,a和w是躺著mian方法所在的棧幀中
如圖:
引用傳遞:
”引用”也就是指向真實(shí)內(nèi)容的地址值,在方法調(diào)用時(shí),實(shí)參的地址通過方法調(diào)用被傳遞給相應(yīng)的形參,在方法體內(nèi),形參和實(shí)參指向通愉快內(nèi)存地址,對(duì)形參的操作會(huì)影響的真實(shí)內(nèi)容。
舉個(gè)栗子:
先定義一個(gè)對(duì)象:
1
public
class
Person
{
2
private
String name;
3
private
int
age;
4
5
public
String
getName
()
{
6
return
name;
7
}
8
public
void
setName
(String name)
{
9
this
.name = name;
10
}
11
public
int
getAge
()
{
12
return
age;
13
}
14
public
void
setAge
(
int
age)
{
15
this
.age = age;
16
}
17
}
我們寫個(gè)函數(shù)測試一下:
1
public
static
void
PersonCrossTest
(Person person)
{
2
System.out.println(
"傳入的person的name:"
+person.getName());
3
person.setName(
"我是張小龍"
);
4
System.out.println(
"方法內(nèi)重新賦值后的name:"
+person.getName());
5
}
6
//測試
7
public
static
void
main
(String[] args)
{
8
Person p=
new
Person();
9
p.setName(
"我是馬化騰"
);
10
p.setAge(
45
);
11
PersonCrossTest(p);
12
System.out.println(
"方法執(zhí)行后的name:"
+p.getName());
13
}
輸出結(jié)果:
1
傳入的person的name:我是馬化騰
2
方法內(nèi)重新賦值后的name:我是張小龍
3
方法執(zhí)行后的name:我是張小龍
可以看出,person經(jīng)過personCrossTest()方法的執(zhí)行之后,內(nèi)容發(fā)生了改變,這印證了上面所說的 “引用傳遞” ,對(duì)形參的操作,改變了實(shí)際對(duì)象的內(nèi)容。
那么,到這里就結(jié)題了嗎?
不是的,沒那么簡單,
能看得到想要的效果
是因?yàn)閯偤眠x對(duì)了例子而已?。。?/p>
下面我們對(duì)上面的例子稍作修改,加上一行代碼,
1
public
static
void
PersonCrossTest
(Person person)
{
2
System.out.println(
"傳入的person的name:"
+person.getName());
3
person=
new
Person();
//加多此行代碼
4
person.setName(
"我是張小龍"
);
5
System.out.println(
"方法內(nèi)重新賦值后的name:"
+person.getName());
6
}
輸出結(jié)果:
1
傳入的person的name:我是馬化騰
2
方法內(nèi)重新賦值后的name:我是張小龍
3
方法執(zhí)行后的name:我是馬化騰
`
為什么這次的輸出和上次的不一樣了呢?
看出什么問題了嗎?
按照上面講到JVM內(nèi)存模型可以知道,對(duì)象和數(shù)組是存儲(chǔ)在Java堆區(qū)的,而且堆區(qū)是共享的,因此程序執(zhí)行到main()方法中的下列代碼時(shí)
1
Person p=
new
Person();
2
p.setName(
"我是馬化騰"
);
3
p.setAge(
45
);
4
PersonCrossTest(p);
JVM會(huì)在堆內(nèi)開辟一塊內(nèi)存,用來存儲(chǔ)p對(duì)象的所有內(nèi)容,同時(shí)在main()方法所在線程的棧區(qū)中創(chuàng)建一個(gè)引用p存儲(chǔ)堆區(qū)中p對(duì)象的真實(shí)地址,如圖:
1
person=
new
Person();
JVM需要在堆內(nèi)另外開辟一塊內(nèi)存來存儲(chǔ)new Person(),假如地址為“xo3333”,那此時(shí)形參person指向了這個(gè)地址,假如真的是引用傳遞,那么由上面講到: 引用傳遞中形參實(shí)參指向同一個(gè)對(duì)象,形參的操作會(huì)改變實(shí)參對(duì)象的改變 。
可以推出:實(shí)參也應(yīng)該指向了新創(chuàng)建的person對(duì)象的地址,所以在執(zhí)行PersonCrossTest()結(jié)束之后,最終輸出的應(yīng)該是后面創(chuàng)建的對(duì)象內(nèi)容。
然而實(shí)際上,最終的輸出結(jié)果卻跟我們推測的不一樣,最終輸出的仍然是一開始創(chuàng)建的對(duì)象的內(nèi)容。
由此可見: 引用傳遞,在Java中并不存在。
但是有人會(huì)疑問:為什么第一個(gè)例子中,在方法內(nèi)修改了形參的內(nèi)容,會(huì)導(dǎo)致原始對(duì)象的內(nèi)容發(fā)生改變呢?
這是因?yàn)椋? 無論是基本類型和是引用類型,在實(shí)參傳入形參時(shí),都是值傳遞,也就是說傳遞的都是一個(gè)副本,而不是內(nèi)容本身。
有圖可以看出,方法內(nèi)的形參person和實(shí)參p并無實(shí)質(zhì)關(guān)聯(lián),它只是由p處copy了一份指向?qū)ο蟮牡刂?,此時(shí):
p和person都是指向同一個(gè)對(duì)象 。
因此在第一個(gè)例子中,對(duì)形參p的操作,會(huì)影響到實(shí)參對(duì)應(yīng)的對(duì)象內(nèi)容。而在第二個(gè)例子中,當(dāng)執(zhí)行到new Person()之后,JVM在堆內(nèi)開辟一塊空間存儲(chǔ)新對(duì)象,并且把person改成指向新對(duì)象的地址,此時(shí):
p依舊是指向舊的對(duì)象,person指向新對(duì)象的地址。
所以此時(shí) 對(duì)person的操作,實(shí)際上是對(duì)新對(duì)象的操作,于實(shí)參p中對(duì)應(yīng)的對(duì)象毫無關(guān)系 。
因此可見:在Java中所有的參數(shù)傳遞,不管基本類型還是引用類型,都是值傳遞,或者說是副本傳遞。
只是在傳遞過程中:
如果是對(duì)基本數(shù)據(jù)類型的數(shù)據(jù)進(jìn)行操作,由于原始內(nèi)容和副本都是存儲(chǔ)實(shí)際值,并且是在不同的棧區(qū),因此形參的操作,不影響原始內(nèi)容。
如果是對(duì)引用類型的數(shù)據(jù)進(jìn)行操作,分兩種情況,一種是形參和實(shí)參保持指向同一個(gè)對(duì)象地址,則形參的操作,會(huì)影響實(shí)參指向的對(duì)象的內(nèi)容。一種是形參被改動(dòng)指向新的對(duì)象地址(如重新賦值引用),則形參的操作,不會(huì)影響實(shí)參指向的對(duì)象的內(nèi)容。
以上為小編關(guān)于“值傳遞和引用傳遞”問題的思考和論證,對(duì)于這個(gè)問題,歷來都是多有爭論,在此希望和讀者一起探討和學(xué)習(xí),有不同意見或者建議請(qǐng)假小編微信:sisi-ceo。理性評(píng)論,不喜勿噴。