Java的值傳遞和引用傳遞

来源:https://www.cnblogs.com/YancyL/archive/2018/11/10/9937749.html
-Advertisement-
Play Games

本文旨在用最通俗的語言講述最枯燥的基本知識 學過Java基礎的人都知道:值傳遞和引用傳遞是初次接觸Java時的一個難點,有時候記得了語法卻記不得怎麼實際運用,有時候會的了運用卻解釋不出原理,而且坊間討論的話題又是充滿爭議:有的論壇帖子說Java只有值傳遞,有的博客說兩者皆有;這讓人有點摸不著頭腦,下 ...


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

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

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

是什麼原因?

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

1. 形參與實參

我們先來重溫一組語法:

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

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

舉個慄子:

1public static void func(int a){
2 a=20;
3 System.out.println(a);
4}
5public static void main(String[] args) {
int a=10;//變數
7 func(a);
8}

例子中
int a=10;中的a在被調用之前就已經創建並初始化,在調用func方法時,他被當做參數傳入,所以這個a是實參。
而func(int a)中的a只有在func被調用時它的生命周期才開始,而在func調用結束之後,它也隨之被JVM釋放掉,,所以這個a是形參。

2. Java的數據類型

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

數據類型實質上是用來定義編程語言中相同類型的數據的存儲形式,也就是決定瞭如何將代表這些值的位存儲到電腦的記憶體中。

所以,數據在記憶體中的存儲,是根據數據類型來劃定存儲形式和存儲位置的。
那麼
Java的數據類型有哪些?

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

4種整數類型:byte、short、int、long
2種浮點數類型:float、double
1種字元類型:char
1種布爾類型:boolean

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


介面
數組

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

3.JVM記憶體的劃分及職能

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


有圖可以看出:Java代碼被編譯器編譯成位元組碼之後,JVM開闢一片記憶體空間(也叫運行時數據區),通過類載入器加到到運行時數據區來存儲程式執行期間需要用到的數據和相關信息,在這個數據區中,它由以下幾部分組成:

1. 虛擬機棧
2. 堆
3. 程式計數器
4. 方法區
5. 本地方法棧

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


1. 虛擬機棧

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

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

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


棧幀:是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。

每個棧幀中包括:

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

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

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

  4. 方法返回地址:存儲方法執行完成後的返回地址。


2. 堆:

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


3. 方法區:

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

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


4. 本地方法棧:

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

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


5. 程式計數器:

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


4. 數據如何在記憶體中存儲?

從上面程式運行圖我們可以看到,JVM在程式運行時的記憶體分配有三個地方:

  • 靜態方法區

  • 常量區

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

  • 堆式:

  • 棧式

  • 靜態

我們已經知道:Java中的數據類型有基本數據類型和引用數據類型,那麼這些數據的存儲都使用哪一種策略呢?
這裡要分以下的情況進行探究:

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

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

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

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

2. 引用數據類型的存儲


1. 基本數據類型的存儲


我們分別來研究一下:

A.基本數據類型的局部變數
  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指向這個地址。由此可知:

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


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

成員變數:顧名思義,就是在類體中定義的變數。
看下圖:

我們看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對象開闢的一塊空間中。因此可知:基本數據類型的成員變數名和值都存儲於堆中,其生命周期和對象的是一致的。


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

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


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方法所在的棧幀中
如圖:


而當執行到valueCrossTest()方法時,JVM也為其往虛擬機棧中壓入一個棧,即為當前棧幀,用來存放valueCrossTest()中的局部變數等信息,因此age和weight是躺著valueCrossTest方法所在的棧幀中,而他們的值是從a和w的值copy了一份副本而得,如圖:
因而可以a和age、w和weight對應的內容是不一致的,所以當在方法內重新賦值時,實際流程如圖:
也就是說,age和weight的改動,只是改變了當前棧幀(valueCrossTest方法所在棧幀)里的內容,當方法執行結束之後,這些局部變數都會被銷毀,mian方法所在棧幀重新回到棧頂,成為當前棧幀,再次輸出a和w時,依然是初始化時的內容。
因此:
值傳遞傳遞的是真實內容的一個副本,對副本的操作不影響原內容,也就是形參怎麼變化,不會影響實參對應的內容。

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

舉個慄子:
先定義一個對象:

 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對象的真實地址,如圖:


當執行到PersonCrossTest()方法時,因為方法內有這麼一行代碼:

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中所有的參數傳遞,不管基本類型還是引用類型,都是值傳遞,或者說是副本傳遞。
只是在傳遞過程中:

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

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

 

PS:(轉載至公眾號Java團長,原文章的小錯誤已更正)如果覺得我的分享不錯,歡迎大家隨手點贊、轉發。

                                                                                              (完)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SpringMVC入門學習(一) ssm框架 spring SpringMVC是一個Java WEB框架,現在我們知道Spring了,那麼,何為MVC呢? MVC是一種設計模式,其分為3個方面 model 模型:dao層和service層 view 視圖:例如jsp和Html controller ...
  • 很不錯的Nginx入門書,裡面有教你怎麼去做日誌切割,怎麼去做高併發,怎麼去統計流量帶寬等等。不要998,不要98,免費下載!!! ...
  • 學了100多天java知道了要抓住本質、最底層學習,比如堆和棧啊,類是怎麼載入的啊,再深一點的話就要看編譯原理了,還是羡慕電腦專業的。 靜態成員不可調用非靜態成員,static稱為靜態,靜態成員的最根本作用是區分屬性,方法,內部類,初始化塊成員到底屬於類本身,還是屬於類的實例,有static修飾的 ...
  • 1、PHP 自帶函數 getallheaders() 目前 getallheaders() 只能用於 apache 中。如果想在 nginx 中也能使用,可以使用自定義函數。 2、自定義函數 ...
  • 創建項目 使用idea(版本2018.2),新建項目,使用spring initalizr,項目命名為名字為producer,選擇integration里的rabbitmq,確定。 配置rabbitmq服務 將預設生成的application.properties重命名為application.ym ...
  • 之前寫過一篇閉包的使用(點擊此處進入),這次深入彙總下php中匿名函數的深入用法和理解: php中的匿名函數 也叫閉包函數 允許指定一個沒有名稱的函數。把匿名函數賦值給變數,通過變數來調用,舉個簡單的例子: 技巧1: 將匿名函數放在普通函數中,也可以將匿名函數返回,:這樣就構成了一個簡單的閉包 技巧 ...
  • 公司的項目沒那麼忙了,而且可能項目組很快要去深圳了! 自己的引擎方面去掉了一些不必要的層級,儘量輕量級,簡單架構,模擬著一個新人接觸怎樣能上手來重新整理了一下! 游戲方面增加了C++類之後寫代碼熟多了,但是整體上大部分還是在藍圖中,有一些函數什麼的還在摸索! 女朋友還是一樣的每周末都住一下,好像一下 ...
  • HashMap的put操作源碼解析 [toc] 1、官方文檔 1.1、繼承結構 1.2、類型參數: 2、put(key, value) HashMap是一種以鍵——值對的形式來存儲數據的數據結構。HashMap允許使用 null 值和 null 鍵,它並不能保證你存放數據和取出的順序是一致的。 接下 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...