Java 學習筆記(3)——函數

以前的幾篇文章中,總結了java中的基本語句和基本數據類型等等一系列的最基本的東西,下面就來講說java中的函數部分java

函數基礎

在C/C++中有普通的全局函數、類成員函數和類的靜態函數,而java中全部內容都必須定義在類中。因此Java中是沒有全局函數的,Java裏面只有普通的類成員函數(也叫作成員方法)和靜態函數(也叫作靜態方法)。這兩種東西在理解上與C/C++基本同樣,定義的格式分別爲:數組

public static void test(arglist){

}
public void test(arglist){

}

基本格式爲:修飾符 [static] 返回值 函數名稱 形參列表架構

修飾符主要是用來修飾方法的訪問限制,好比public 、private等等;若是是靜態方法須要加上static 若是是成員方法則不須要;後面是返回值,Java函數能夠返回任意類型的值;函數名用來肯定一個函數,最後形參列表是傳遞給函數的參數列表。函數

函數中的內存分佈

Java中函數的使用方式與C/C++中基本相同,這裏就再也不額外花費篇幅說明它的使用,我想將重點放在函數調用時內存的分配和使用上,更深一層瞭解java中函數的運行機制。3d

咱們說在X86架構的機器上,每一個進程擁有4GB的虛擬地址空間。Java程序也是一個進程,因此它也擁有4GB的虛擬地址空間。每當啓動一個Java程序的時候,由Java虛擬機讀取.class 文件,而後解釋執行其中的二進制字節碼。啓動java程序時,在進程列表中看到的是一個個的Java虛擬機程序。
java虛擬機在加載.class 文件時將它的4GB的虛擬地址空間劃分爲5個部分,分別是棧、堆、方法區、本地方法棧、寄存器區。其中重點須要關注前3個部分。指針

  • 棧:與C/C++中棧的做用相同,就是用來保存函數中的局部變量和實參值的。
  • 堆:與C/C++中堆的做用相同,用來存儲Java中new出來的對象
  • 方法區:用來保存方法代碼和方法名與地址的這麼一張表,相似於C/C++中的函數表

基本數據類型做爲函數的參數

class Demo{
    public static void main(String[] args){
        int n = 10;
        test(10);
        System.out.println(n);
    }

    public static void test(int i){
        System.out.println(i);
        i++;
    }
}

上述代碼在函數中改變了形參值,那麼在調用以後n的值會不會發生變化呢?答案是:不會變化,在C/C++中很好理解,形參i只是實參n的一個拷貝,i改變不會改變原來的n。這裏咱們從內存的角度來回答這個問題
內存分佈code

如上圖所示,方法區中存儲了兩個方法的相關信息,main和test,在調用main的時候,首先從方法區中查找main函數的相關信息,而後在棧中進行參數入棧等操做。而後初始化一個局部變量n,接着調用test函數,調用test函數時首先根據方法區中的函數表找到方法對應的代碼位置,而後進行棧寄存器的偏移爲函數test分配一個棧空間,接着進行參數入棧,這個時候會將n的值——10拷貝到i所在內存中。這個時候在test中修改了i的值,改變的是形參中拷貝的值,與n無關。因此這裏n的值不變對象

引用類型做爲函數參數

class Demo{
    public static void main(String[] args){
        String s = "Hello";
        test(s);
        System.out.println(s); //"Hello"
    }

    public static void test(String s){
        System.out.println(s);  //"Hello"
        s = "World";
    }
}

在C/C++中,常常有這麼一句話:「按值傳遞不能改變實參的值,按引用傳遞能夠改變實參的值」,咱們知道String 是一個引用,那麼這裏傳遞的是String的引用,咱們在函數內部改變了s的值,在外部s的值是否是也改變了呢?咱們首先估計會打印一個 "Hello"、一個"World"; 實際運行結果倒是打印了兩個 "Hello",那麼是否是有問題呢?Java中到底存不存在按引用傳遞呢?爲了回答這個問題,咱們仍是來一張內存圖:
內存分佈blog

從上面的內存圖來看,在函數中修改的仍然是形參的值,而對實參的值徹底沒有影響。若是想作到在函數中修改實參的值,請記住一點:拿到實參的地址,經過地址直接修改內存。進程

下面再來看一個例子:

class Demo{
    public static void main(String[] args){
        int[] array = new int[]{1, 2, 3, 4, 5};
        test(array);
        for(int i = 0; i < array.length; i++){
            System.out.print(array[i]);
        }
        
        System.out.println(); //98345
    }

    public static void test(int[] array){
        for(int i = 0; i < array.length; i++){
            System.out.print(array[i]);
        }
        
        System.out.println(); //12345
        array[0] = 9;
        array[1] = 8;
    }
}

運行這個實例,能夠看到這裏它確實改變了,那麼這裏它發生了什麼?跟上面一個字符串的例子相比有什麼不一樣呢?仍是來看看內存圖
內存分佈

這段代碼執行的過程當中經歷了3個主要步驟:

  • new一個數組對象,而且將數組對象的地址賦值給array 實參
  • 調用test函數時將array實參中保存的地址複製一份壓入函數的參數列表中
  • 在test函數中,經過這個地址值來修改對應內存中的內容

這段代碼與上面兩段本質上的區別在於,這段代碼經過引用類型中保存的地址值找到並修改了對應內存中內容,而上面的兩段代碼僅僅是在修改引用類型這個變量自己的值。

說到傳遞引用類型,那麼我就想到在C/C++中一個經典的漏洞——緩衝區溢出漏洞,那麼java程序中是否也存在這個問題呢?這裏我準備了這樣一段代碼:

class Demo{
    public static void main(String[] args){
        byte[] buf = new byte[7];
        test(buf);
    }

    public static void test(byte[] buf){
        for(int i = 0; i < 10; i++){
            buf[i] = (byte)i;
        }
    }
}

若是是在C/C++中,這段代碼能夠正常執行只是最後可能會報錯或者崩潰,可是賦值是成功的,這也就留給了黑客可利用的空間。

在Java中執行它會發現,它會報一個越界訪問的異常,也就說這裏賦值是失敗的,不能直接往內存裏面寫,也就不存在這個漏洞了。

返回引用類型

Java方法返回基本類型的狀況很簡單,也就是將函數返回值放到某塊內存中,而後進行一個複製操做。這裏重點了解一下它在返回引用類型時與C/C++不一樣的點

在C/C++中返回一個類對象的時候,會調用拷貝構造將須要返回的類對象拷貝到對應保存類對象的位置,而後針對函數中的類對象調用它的析構函數進行資源回收,那麼Java中返回類對象會進行哪些操做?

C/C++中返回一個類對象的指針時,外部須要本身調用delete或者其餘操做進行析構。java中的類對象都是引用類型,在函數外部爲什麼不須要額外調用析構呢?帶着這些問題,來看下面這段代碼:

class Demo{
    public static void main(String[] args){
        String s = test();
        System.out.println(s);
    }

    public static String test(){
//      return new String("hello world");
        return "Hello World";
    }
}

這段代碼 不論是用new也好仍是直接返回也好,效果實際上是同樣的,下面是對應的內存分佈圖
內存分佈圖

這段代碼首先在函數test中new一個對象,此時對應在堆內存中開闢一塊空間來保存"hello world" 值,而後保存內存地址在寄存器或者其餘某個位置,接着將這個地址值拷貝到main函數中的s中,最後回收test函數的棧空間。

這裏實質上是返回了一個堆中的地址值,這裏就回答了第一個問題:在返回類對象的時候其實返回的值對象所在的堆內存的地址。

接着來回答第二個問題:java中資源回收依賴與一個引用計數。每當對地址值進行一次拷貝時計數器加一,當回收拷貝值所在內存時計數器減一。這裏在返回時,先將地址值保存到某個位置(好比C/C++中是將返回值保存在eax寄存器中)。此時計數器 + 1;而後將這個值拷貝到 main 函數的s變量中,此時計數器的值再 + 1,變爲2,接着回收test函數棧空間,計數器 - 1,變爲1,在main函數指向完成以後,main的棧空間也被回收,此時計數器 - 1,變爲0,此時new出來的對象由Java的垃圾回收器進行回收。

相關文章
相關標籤/搜索