這一次徹底解決Java的值傳遞和引用傳遞

      網友投稿 1076 2022-05-29

      本文旨在用最通俗的語言講述最枯燥的基本知識

      學過Java基礎的人都知道:值傳遞和引用傳遞是初次接觸Java時的一個難點,有時候記得了語法卻記不得怎么實際運用,有時候會的了運用卻解釋不出原理,而且坊間討論的話題又是充滿爭議:有的論壇帖子說Java只有值傳遞,有的博客說兩者皆有;這讓人有點摸不著頭腦,下面我們就這個話題做一些探討,對書籍、對論壇博客的說法,做一次考證,以得出信得過的答案。

      其實,對于值傳遞和引用傳遞的語法和運用,百度一下,就能出來可觀的解釋和例子數目,或許你看一下例子好像就懂,但是當你參加面試,做一道這個知識點的筆試題時感覺自己會,胸有成熟的寫了答案,卻發現是錯的,或者是你根本不會做。

      是什么原因?

      那是因為你對知識點沒有了解透徹,只知道其皮毛。要熟讀一個語法很簡單,要理解一行代碼也不難,但是能把學過的知識融會貫通,串聯起來理解,那就是非常難了,在此,關于值傳遞和引用傳遞,小編會從以前學過的基礎知識開始,從內存模型開始,一步步的引出值傳遞和引用傳遞的本質原理,故篇幅較長,知識點較多,望讀者多有包涵。

      1. 形參與實參

      我們先來重溫一組語法:

      形參:方法被調用時需要傳遞進來的參數,如:func(int a)中的a,它只有在func被調用期間a才有意義,也就是會被分配內存空間,在方法func執行完成后,a就會被銷毀釋放空間,也就是不存在了

      實參:方法被調用時是傳入的實際值,它在方法被調用前就已經被初始化并且在方法被調用時傳入。

      舉個栗子:

      1public?static?void?func(int?a){

      2?a=20;

      3?System.out.println(a);

      4}

      5public?static?void?main(String[]?args)?{

      6?int?a=10;//實參

      這一次,徹底解決Java的值傳遞和引用傳遞

      7?func(a);

      8}

      例子中

      int a=10;中的a在被調用之前就已經創建并初始化,在調用func方法時,他被當做參數傳入,所以這個a是實參。

      而func(int a)中的a只有在func被調用時它的生命周期才開始,而在func調用結束之后,它也隨之被JVM釋放掉,,所以這個a是形參。

      2. Java的數據類型

      所謂數據類型,是編程語言中對內存的一種抽象表達方式,我們知道程序是由代碼文件和靜態資源組成,在程序被運行前,這些代碼存在在硬盤里,程序開始運行,這些代碼會被轉成計算機能識別的內容放到內存中被執行。

      因此

      數據類型實質上是用來定義編程語言中相同類型的數據的存儲形式,也就是決定了如何將代表這些值的位存儲到計算機的內存中。

      所以,數據在內存中的存儲,是根據數據類型來劃定存儲形式和存儲位置的。

      那么

      Java的數據類型有哪些?

      基本類型:編程語言中內置的最小粒度的數據類型。它包括四大類八種類型:

      4種整數類型:byte、short、int、long

      2種浮點數類型:float、double

      1種字符類型:char

      1種布爾類型:boolean

      引用類型:引用也叫句柄,引用類型,是編程語言中定義的在句柄中存放著實際內容所在地址的地址值的一種數據形式。它主要包括:

      接口

      數組

      有了數據類型,JVM對程序數據的管理就規范化了,不同的數據類型,它的存儲形式和位置是不一樣的,要想知道JVM是怎么存儲各種類型的數據,就得先了解JVM的內存劃分以及每部分的職能。

      3.JVM內存的劃分及職能

      Java語言本身是不能操作內存的,它的一切都是交給JVM來管理和控制的,因此Java內存區域的劃分也就是JVM的區域劃分,在說JVM的內存劃分之前,我們先來看一下Java程序的執行過程,如下圖:

      我們接著來了解一下每部分的原理以及具體用來存儲程序執行過程中的哪些數據。

      1. 虛擬機棧

      虛擬機棧是Java方法執行的內存模型,棧中存放著棧幀,每個棧幀分別對應一個被調用的方法,方法的調用過程對應棧幀在虛擬機中入棧到出棧的過程。

      棧是線程私有的,也就是線程之間的棧是隔離的;當程序中某個線程開始執行一個方法時就會相應的創建一個棧幀并且入棧(位于棧頂),在方法結束后,棧幀出棧。

      下圖表示了一個Java棧的模型以及棧幀的組成:

      棧幀

      每個棧幀中包括:

      局部變量表:用來存儲方法中的局部變量(非靜態變量、函數形參)。當變量為基本數據類型時,直接存儲值,當變量為引用類型時,存儲的是指向具體對象的引用。

      操作數棧:Java虛擬機的解釋執行引擎被稱為"基于棧的執行引擎",其中所指的棧就是指操作數棧。

      指向運行時常量池的引用:存儲程序執行時可能用到常量的引用。

      方法返回地址:存儲方法執行完成后的返回地址。

      2. 堆:

      堆是用來存儲對象本身和數組的,在JVM中只有一個堆,因此,堆是被所有線程共享的。

      3. 方法區:

      方法區是一塊所有線程共享的內存邏輯區域,在JVM中只有一個方法區,用來存儲一些線程可共享的內容,它是線程安全的,多個線程同時訪問方法區中同一個內容時,只能有一個線程裝載該數據,其它線程只能等待。

      方法區可存儲的內容有:類的全路徑名、類的直接超類的權全限定名、類的訪問修飾符、類的類型(類或接口)、類的直接接口全限定名的有序列表、常量池(字段,方法信息,靜態變量,類型引用(class))等。

      4. 本地方法棧:

      本地方法棧的功能和虛擬機棧是基本一致的,并且也是線程私有的,它們的區別在于虛擬機棧是為執行Java方法服務的,而本地方法棧是為執行本地方法服務的。

      有人會疑惑:什么是本地方法?為什么Java還要調用本地方法?

      5. 程序計數器:

      線程私有的。

      記錄著當前線程所執行的字節碼的行號指示器,在程序運行過程中,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、異常處理、線程恢復等基礎功能都需要依賴計數器完成。

      4. 數據如何在內存中存儲?

      從上面程序運行圖我們可以看到,JVM在程序運行時的內存分配有三個地方:

      靜態方法區

      常量區

      相應地,每個存儲區域都有自己的內存分配策略:

      堆式:

      棧式

      靜態

      我們已經知道:Java中的數據類型有基本數據類型和引用數據類型,那么這些數據的存儲都使用哪一種策略呢?

      這里要分以下的情況進行探究:

      1. 基本數據類型的存儲:

      A. 基本數據類型的局部變量

      B. 基本數據類型的成員變量

      C. 基本數據類型的靜態變量

      2. 引用數據類型的存儲

      1. 基本數據類型的存儲

      我們分別來研究一下:

      定義基本數據類型的局部變量以及數據都是直接存儲在內存中的棧上,也就是前面說到的“虛擬機棧”,數據本身的值就是存儲在棧空間里面。

      如上圖,在方法內定義的變量直接存儲在棧中,如

      1int?age=50;

      2int?weight=50;

      3int?grade=6;

      當我們寫“int age=50;”,其實是分為兩步的:

      1int?age;//定義變量

      2age=50;//賦值

      首先JVM創建一個名為age的變量,存于局部變量表中,然后去棧中查找是否存在有字面量值為50的內容,如果有就直接把age指向這個地址,如果沒有,JVM會在棧中開辟一塊空間來存儲“50”這個內容,并且把age指向這個地址。因此我們可以知道:

      我們聲明并初始化基本數據類型的局部變量時,變量名以及字面量值都是存儲在棧中,而且是真實的內容。

      我們再來看“int weight=50;”,按照剛才的思路:字面量為50的內容在棧中已經存在,因此weight是直接指向這個地址的。由此可見:棧中的數據在當前線程下是共享的。

      那么如果再執行下面的代碼呢?

      1weight=40;

      當代碼中重新給weight變量進行賦值時,JVM會去棧中尋找字面量為40的內容,發現沒有,就會開辟一塊內存空間存儲40這個內容,并且把weight指向這個地址。由此可知:

      基本數據類型的數據本身是不會改變的,當局部變量重新賦值時,并不是在內存中改變字面量內容,而是重新在棧中尋找已存在的相同的數據,若棧中不存在,則重新開辟內存存新數據,并且把要重新賦值的局部變量的引用指向新數據所在地址。

      成員變量:顧名思義,就是在類體中定義的變量。

      看下圖:

      我們看per的地址指向的是堆內存中的一塊區域,我們來還原一下代碼:

      1public?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//調用

      12Person?per=new?Person();

      同樣是局部變量的age、name、grade卻被存儲到了堆中為per對象開辟的一塊空間中。因此可知:基本數據類型的成員變量名和值都存儲于堆中,其生命周期和對象的是一致的。

      前面提到方法區用來存儲一些共享數據,因此基本數據類型的靜態變量名以及值存儲于方法區的運行時常量池中,靜態變量隨類加載而加載,隨類消失而消失

      2. 引用數據類型的存儲:

      上面提到:堆是用來存儲對象本身和數組,而引用(句柄)存放的是實際內容的地址值,因此通過上面的程序運行圖,也可以看出,當我們定義一個對象時

      1Person?per=new?Person();

      實際上,它也是有兩個過程:

      1Person?per;//定義變量

      2per=new?Person();//賦值

      在執行Person per;時,JVM先在虛擬機棧中的變量表中開辟一塊內存存放per變量,在執行per=new Person()時,JVM會創建一個Person類的實例對象并在堆中開辟一塊內存存儲這個實例,同時把實例的地址值賦值給per變量。因此可見:

      對于引用數據類型的對象/數組,變量名存在棧中,變量值存儲的是對象的地址,并不是對象的實際內容。

      6. 值傳遞和引用傳遞

      前面已經介紹過形參和實參,也介紹了數據類型以及數據在內存中的存儲形式,接下來,就是文章的主題:值傳遞和引用的傳遞。

      值傳遞:

      在方法被調用時,實參通過形參把它的內容副本傳入方法內部,此時形參接收到的內容是實參值的一個拷貝,因此在方法內對形參的任何操作,都僅僅是對這個副本的操作,不影響原始值的內容。

      來看個例子:

      1public?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("方法內重新賦值后的age:"+age);

      7????System.out.println("方法內重新賦值后的weight:"+weight);

      8????}

      9

      10//測試

      11public?static?void?main(String[]?args)?{

      12????????int?a=25;

      13????????float?w=77.5f;

      14????????valueCrossTest(a,w);

      15????????System.out.println("方法執行后的age:"+a);

      16????????System.out.println("方法執行后的weight:"+w);

      17}

      輸出結果:

      1傳入的age:25

      2傳入的weight:77.5

      3

      4方法內重新賦值后的age:33

      5方法內重新賦值后的weight:89.5

      6

      7方法執行后的age:25

      8方法執行后的weight:77.5

      從上面的打印結果可以看到:

      a和w作為實參傳入valueCrossTest之后,無論在方法內做了什么操作,最終a和w都沒變化。

      這是什么造型呢?!!

      下面我們根據上面學到的知識點,進行詳細的分析:

      首先程序運行時,調用mian()方法,此時JVM為main()方法往虛擬機棧中壓入一個棧幀,即為當前棧幀,用來存放main()中的局部變量表(包括參數)、操作棧、方法出口等信息,如a和w都是mian()方法中的局部變量,因此可以斷定,a和w是躺著mian方法所在的棧幀中

      如圖:

      值傳遞傳遞的是真實內容的一個副本,對副本的操作不影響原內容,也就是形參怎么變化,不會影響實參對應的內容。

      引用傳遞:

      ”引用”也就是指向真實內容的地址值,在方法調用時,實參的地址通過方法調用被傳遞給相應的形參,在方法體內,形參和實參指向通愉快內存地址,對形參的操作會影響的真實內容。

      舉個栗子:

      先定義一個對象:

      1public?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}

      我們寫個函數測試一下:

      1public?static?void?PersonCrossTest(Person?person){

      2????????System.out.println("傳入的person的name:"+person.getName());

      3????????person.setName("我是張小龍");

      4????????System.out.println("方法內重新賦值后的name:"+person.getName());

      5????}

      6//測試

      7public?static?void?main(String[]?args)?{

      8????????Person?p=new?Person();

      9????????p.setName("我是馬化騰");

      10????????p.setAge(45);

      11????????PersonCrossTest(p);

      12????????System.out.println("方法執行后的name:"+p.getName());

      13}

      輸出結果:

      1傳入的person的name:我是馬化騰

      2方法內重新賦值后的name:我是張小龍

      3方法執行后的name:我是張小龍

      可以看出,person經過personCrossTest()方法的執行之后,內容發生了改變,這印證了上面所說的“引用傳遞”,對形參的操作,改變了實際對象的內容。

      那么,到這里就結題了嗎?

      不是的,沒那么簡單,

      能看得到想要的效果

      是因為剛好選對了例子而已!!!

      下面我們對上面的例子稍作修改,加上一行代碼,

      1public?static?void?PersonCrossTest(Person?person){

      2????????System.out.println("傳入的person的name:"+person.getName());

      3????????person=new?Person();//加多此行代碼

      4????????person.setName("我是張小龍");

      5????????System.out.println("方法內重新賦值后的name:"+person.getName());

      6????}

      輸出結果:

      1傳入的person的name:我是馬化騰

      2方法內重新賦值后的name:我是張小龍

      3方法執行后的name:我是馬化騰

      `

      為什么這次的輸出和上次的不一樣了呢?

      看出什么問題了嗎?

      按照上面講到JVM內存模型可以知道,對象和數組是存儲在Java堆區的,而且堆區是共享的,因此程序執行到main()方法中的下列代碼時

      1Person?p=new?Person();

      2????????p.setName("我是馬化騰");

      3????????p.setAge(45);

      4????????PersonCrossTest(p);

      JVM會在堆內開辟一塊內存,用來存儲p對象的所有內容,同時在main()方法所在線程的棧區中創建一個引用p存儲堆區中p對象的真實地址,如圖:

      1person=new?Person();

      JVM需要在堆內另外開辟一塊內存來存儲new Person(),假如地址為“xo3333”,那此時形參person指向了這個地址,假如真的是引用傳遞,那么由上面講到:引用傳遞中形參實參指向同一個對象,形參的操作會改變實參對象的改變。

      可以推出:實參也應該指向了新創建的person對象的地址,所以在執行PersonCrossTest()結束之后,最終輸出的應該是后面創建的對象內容。

      然而實際上,最終的輸出結果卻跟我們推測的不一樣,最終輸出的仍然是一開始創建的對象的內容。

      由此可見:引用傳遞,在Java中并不存在。

      但是有人會疑問:為什么第一個例子中,在方法內修改了形參的內容,會導致原始對象的內容發生改變呢?

      這是因為:無論是基本類型和是引用類型,在實參傳入形參時,都是值傳遞,也就是說傳遞的都是一個副本,而不是內容本身。

      有圖可以看出,方法內的形參person和實參p并無實質關聯,它只是由p處copy了一份指向對象的地址,此時:

      p和person都是指向同一個對象。

      因此在第一個例子中,對形參p的操作,會影響到實參對應的對象內容。而在第二個例子中,當執行到new Person()之后,JVM在堆內開辟一塊空間存儲新對象,并且把person改成指向新對象的地址,此時:

      p依舊是指向舊的對象,person指向新對象的地址。

      所以此時對person的操作,實際上是對新對象的操作,于實參p中對應的對象毫無關系。

      結語

      因此可見:在Java中所有的參數傳遞,不管基本類型還是引用類型,都是值傳遞,或者說是副本傳遞。

      只是在傳遞過程中:

      如果是對基本數據類型的數據進行操作,由于原始內容和副本都是存儲實際值,并且是在不同的棧區,因此形參的操作,不影響原始內容。

      如果是對引用類型的數據進行操作,分兩種情況,一種是形參和實參保持指向同一個對象地址,則形參的操作,會影響實參指向的對象的內容。一種是形參被改動指向新的對象地址(如重新賦值引用),則形參的操作,不會影響實參指向的對象的內容。

      以上為小編關于“值傳遞和引用傳遞”問題的思考和論證,對于這個問題,歷來都是多有爭論,在此希望和讀者一起探討和學習,有不同意見或者建議請假小編微信:sisi-ceo。理性評論,不喜勿噴。

      本文轉載自異步社區

      Web應用防火墻 WAF

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:自制小工具大大加速MySQL SQL語句優化(附源碼)
      下一篇:51開發板萬年歷基礎
      相關文章
      最新亚洲人成无码网www电影| 亚洲国产精品乱码在线观看97| 久久亚洲精品无码VA大香大香| 亚洲精品成人网站在线观看| 亚洲精品无码专区2| 无码亚洲成a人在线观看| 亚洲欧美不卡高清在线| 亚洲欧洲免费无码| 中文字幕在线日亚洲9| 在线aⅴ亚洲中文字幕| 亚洲中文字幕乱码熟女在线| 亚洲色一区二区三区四区| 亚洲自偷自偷在线成人网站传媒 | 亚洲真人日本在线| 亚洲成a人一区二区三区| 午夜亚洲福利在线老司机| 亚洲av无码av在线播放| 亚洲aⅴ无码专区在线观看| 99亚洲精品卡2卡三卡4卡2卡| 色噜噜噜噜亚洲第一| 国产精品手机在线亚洲| 亚洲?V无码成人精品区日韩| 亚洲国产精品13p| 国产午夜亚洲不卡| 国产亚洲精品观看91在线| 久久亚洲精品国产精品黑人| 亚洲黄色免费观看| 亚洲女人影院想要爱| 亚洲AV永久无码精品网站在线观看| 亚洲午夜无码久久久久小说 | 亚洲最大的成人网| 亚洲女子高潮不断爆白浆| 国产精品亚洲一区二区三区| 亚洲狠狠爱综合影院婷婷| 久久影视综合亚洲| 亚洲AV日韩精品久久久久久久| 亚洲精品国产情侣av在线| 亚洲a级在线观看| 亚洲第一成年网站视频| 亚洲成人影院在线观看| 亚洲综合精品香蕉久久网|