天天看點

Java的值傳遞和引用傳遞

  1. 形參與實參

我們先來重溫一組文法:

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

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

舉個栗子:

public static void func(int a){

a=20;

System.out.println(a);

}

public static void main(String[] args) {

int a=10;

func(a);

例子中

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

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

  1. 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程式的執行過程,如下圖:

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

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

我們接着來了解一下每部分的原理以及具體用來存儲程式執行過程中的哪些資料。

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

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

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

棧幀:是用于支援虛拟機進行方法調用和方法執行的資料結構,它是虛拟機運作時資料區中的虛拟機棧的棧元素。

每個棧幀中包括:

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

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

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

方法傳回位址:存儲方法執行完成後的傳回位址。

  1. 堆:

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

  2. 方法區:

    方法區是一塊所有線程共享的記憶體邏輯區域,在JVM中隻有一個方法區,用來存儲一些線程可共享的内容,它是線程安全的,多個線程同時通路方法區中同一個内容時,隻能有一個線程裝載該資料,其它線程隻能等待。

方法區可存儲的内容有:類的全路徑名、類的直接超類的權全限定名、類的通路修飾符、類的類型(類或接口)、類的直接接口全限定名的有序清單、常量池(字段,方法資訊,靜态變量,類型引用(class))等。

  1. 本地方法棧:

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

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

  1. 程式計數器:

    線程私有的。

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

  2. 資料如何在記憶體中存儲?

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

靜态方法區

常量區

相應地,每個存儲區域都有自己的記憶體配置設定政策:

堆式:

棧式

靜态

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

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

  1. 基本資料類型的存儲:

    A. 基本資料類型的局部變量

    B. 基本資料類型的成員變量

    C. 基本資料類型的靜态變量

  2. 引用資料類型的存儲
  3. 基本資料類型的存儲

    我們分别來研究一下:

A.基本資料類型的局部變量

定義基本資料類型的局部變量以及資料都是直接存儲在記憶體中的棧上,也就是前面說到的“虛拟機棧”,資料本身的值就是存儲在棧空間裡面。

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

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指向這個位址。由此可知:

基本資料類型的資料本身是不會改變的,當局部變量重新指派時,并不是在記憶體中改變字面量内容,而是重新在棧中尋找已存在的相同的資料,若棧中不存在,則重新開辟記憶體存新資料,并且把要重新指派的局部變量的引用指向新資料所在位址。

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

看下圖:

我們看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對象開辟的一塊空間中。是以可知:基本資料類型的成員變量名和值都存儲于堆中,其生命周期和對象的是一緻的。

前面提到方法區用來存儲一些共享資料,是以基本資料類型的靜态變量名以及值存儲于方法區的運作時常量池中,靜态變量随類加載而加載,随類消失而消失

  1. 引用資料類型的存儲:

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

1Person per=new Person();

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

1Person per;//定義變量

2per=new Person();//指派

在執行Person per;時,JVM先在虛拟機棧中的變量表中開辟一塊記憶體存放per變量,在執行per=new Person()時,JVM會建立一個Person類的執行個體對象并在堆中開辟一塊記憶體存儲這個執行個體,同時把執行個體的位址值指派給per變量。是以可見:

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

  1. 值傳遞和引用傳遞

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

值傳遞:

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

來看個例子:

public static void valueCrossTest(int age,float weight){

System.out.println("傳入的age:"+age);

System.out.println("傳入的weight:"+weight);

age=33;

weight=89.5f;

System.out.println("方法内重新指派後的age:"+age);

System.out.println("方法内重新指派後的weight:"+weight);

//測試

int a=25;

float w=77.5f;

valueCrossTest(a,w);

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

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

輸出結果:

傳入的age:25

傳入的weight:77.5

方法内重新指派後的age:33

方法内重新指派後的weight:89.5

方法執行後的age:25

方法執行後的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時,依然是初始化時的内容。

是以:

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

引用傳遞:

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

先定義一個對象:

public class Person {

public String getName() {
         return name;
     }
    public void setName(String name) {
         this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
       this.age = age;
    }           

我們寫個函數測試一下:

public static void PersonCrossTest(Person person){

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

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

System.out.println("方法内重新指派後的name:"+person.getName());

Person p=new Person();

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

p.setAge(45);

PersonCrossTest(p);

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

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

方法内重新指派後的name:我是張小龍

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

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

那麼,到這裡就結題了嗎?

不是的,沒那麼簡單,

能看得到想要的效果

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

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

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

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

`

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

看出什麼問題了嗎?

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

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

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

person=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中對應的對象毫無關系。

結語