Java基礎知識儲備一:Java的值傳遞和引用傳遞

本篇文章轉自微信公衆號:Java後端技術java

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

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

是什麼緣由?編程

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

1. 形參與實參

咱們先來重溫一組語法:數組

形參:方法被調用時須要傳遞進來的參數,如:func(inta)中的a,它只有在func被調用期間a纔有意義,也就是會被分配內存空間,在方法func執行完成後,a就會被銷燬釋放空間,也就是不存在了實參:方法被調用時是傳入的實際值,它在方法被調用前就已經被初始化而且在方法被調用時傳入。

舉個栗子:安全

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;//變量
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程序的執行過程,以下圖:

clipboard.png

有圖能夠看出:Java代碼被編譯器編譯成字節碼以後,JVM開闢一片內存空間(也叫運行時數據區),經過類加載器加到到運行時數據區來存儲程序執行期間須要用到的數據和相關信息,在這個數據區中,它由如下幾部分組成:

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

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

1. 虛擬機棧

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

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

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

clipboard.png

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

每一個棧幀中包括:

1,局部變量表:用來存儲方法中的局部變量(非靜態變量、函數形參)。當變量爲基本數據類型時,直接存儲值,當變量爲引用類型時,存儲的是指向具體對象的引用。
2,操做數棧:Java虛擬機的解釋執行引擎被稱爲"基於棧的執行引擎",其中所指的棧就是指操做數棧。
3,指向運行時常量池的引用:存儲程序執行時可能用到常量的引用。
4,方法返回地址:存儲方法執行完成後的返回地址。

2. 堆:

堆是用來存儲對象自己和數組的,在JVM中只有一個堆,所以,堆是被全部線程共享的。

3. 方法區:

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

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

4. 本地方法棧:

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

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

5. 程序計數器:

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

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

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

堆 棧 靜態方法區 常量區

相應地,每一個存儲區域都有本身的內存分配策略:

堆式: 棧式 靜態

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

1. 基本數據類型的存儲

咱們分別來研究一下:

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

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

clipboard.png

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

int age=50;
int weight=50;
int grade=6;

當咱們寫「int age=50;」,實際上是分爲兩步的:

int age;//定義變量
age=50;//賦值

首先JVM建立一個名爲age的變量,存於局部變量表中,而後去棧中查找是否存在有字面量值爲50的內容,若是有就直接把age指向這個地址,若是沒有,JVM會在棧中開闢一塊空間來存儲「50」這個內容,而且把age指向這個地址。所以咱們能夠知道:
咱們聲明並初始化基本數據類型的局部變量時,變量名以及字面量值都是存儲在棧中,並且是真實的內容。

咱們再來看「int weight=50;」,按照剛纔的思路:字面量爲50的內容在棧中已經存在,所以weight是直接指向這個地址的。因而可知:棧中的數據在當前線程下是共享的。

那麼若是再執行下面的代碼呢?

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

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

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

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

clipboard.png

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

public class Person{
   private int age;
   private String name;
   private int grade;
 //篇幅較長,省略setter getter方法
   static void run(){
      System.out.println("run...."); 
    };
 }
//調用
Person per=new Person();

一樣是局部變量的age、name、grade卻被存儲到了堆中爲per對象開闢的一塊空間中。所以可知:基本數據類型的成員變量名和值都存儲於堆中,其生命週期和對象的是一致的。

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

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

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

上面提到:堆是用來存儲對象自己和數組,而引用(句柄)存放的是實際內容的地址值,所以經過上面的程序運行圖,也能夠看出,當咱們定義一個對象時

Person per=new Person();
實際上,它也是有兩個過程:

Person per;//定義變量
per=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方法所在的棧幀中
如圖:

clipboard.png

而當執行到valueCrossTest()方法時,JVM也爲其往虛擬機棧中壓入一個棧,即爲當前棧幀,用來存放valueCrossTest()中的局部變量等信息,所以age和weight是躺着valueCrossTest方法所在的棧幀中,而他們的值是從a和w的值copy了一份副本而得,如圖:

clipboard.png

於是能夠a和age、w和weight對應的內容是不一致的,因此當在方法內從新賦值時,實際流程如圖:

clipboard.png

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

clipboard.png

當執行到PersonCrossTest()方法時,由於方法內有這麼一行代碼:
person=new Person();
JVM須要在堆內另外開闢一塊內存來存儲new Person(),假如地址爲「xo3333」,那此時形參person指向了這個地址,假如真的是引用傳遞,那麼由上面講到:引用傳遞中形參實參指向同一個對象,形參的操做會改變實參對象的改變。

能夠推出:實參也應該指向了新建立的person對象的地址,因此在執行PersonCrossTest()結束以後,最終輸出的應該是後面建立的對象內容。

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

因而可知:引用傳遞,在Java中並不存在。

可是有人會疑問:爲何第一個例子中,在方法內修改了形參的內容,會致使原始對象的內容發生改變呢?

這是由於:不管是基本類型和是引用類型,在實參傳入形參時,都是值傳遞,也就是說傳遞的都是一個副本,而不是內容自己。

clipboard.png

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

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

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

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

因此此時對person的操做,其實是對新對象的操做,於實參p中對應的對象毫無關係。

結語

所以可見:在Java中全部的參數傳遞,無論基本類型仍是引用類型,都是值傳遞,或者說是副本傳遞。
只是在傳遞過程當中:

若是是對基本數據類型的數據進行操做,因爲原始內容和副本都是存儲實際值,而且是在不一樣的棧區,所以形參的操做,不影響原始內容。

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

相關文章
相關標籤/搜索