關於finally的一些思考

對於try catch每一個開發者都很熟悉,那麼如果深究一下他們的執行順序,提前中斷,提前返回那麼是怎麼樣一個結果呢

前排提示不想看原理最後有結果總結

關於try catch比較淺顯一點的理解就是try包裹有可能出錯的代碼,catch對可能出現的錯誤進行捕獲,finally處理一些資源回收的操作。

那麼提出這樣一個問題,如果try的時候retuen了,finally的內容還會執行嗎。

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println("111");
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
111
222

可以看到finally裏面的內容執行了。
那麼我們提出第二個問題

finally的執行時在return之前還是return之後

首先是關於return返回的底層知識

java方法是在棧幀中執行,棧幀是線程私有棧的單位,執行方法的線程會爲每一個方法分配一小塊棧空間來作爲該方法執行時的內存空間,棧幀分爲三個區域:
1. 操作數棧,用來保存正在執行的表達式中的操作數,數據結構中學習過基於棧的多項式求值算法,操作數棧的作用和這個一樣
2. 局部變量區,用來保存方法中使用的變量,包括方法參數,方法內部聲明的變量,以及方法中使用到的對象的成員變量或類的成員變量(靜態變量),最後兩種變量會複製到局部變量區,因此在多線程 環境下,這種變量需要根據需要聲明爲volatile類型
3. 字節碼指令區,這個不用解釋了,就是方法中的代碼翻譯成的指令

Java官網的文檔說明
If the try clause executes a return, the compiled code does the following:
Saves the return value (if any) in a local variable.
Executes a jsr to the code for the finally clause.
Upon return from the finally clause, returns the value saved in the local variable.
翻譯:
如果try塊中有return,代碼會做以下工作:
1、將return的值保存在一個本地變量中;
2、執行finally塊中的Java Specification Requests(jsr,java標準提案)代碼;
3、執行完finally塊後(從finally中返回後),return剛纔保存在本地變量中的值。
先執行return return語句先把返回值寫入到內存中。然後停下來等待finally語句塊執行完,return再執行後面的一段。
這個是重中之重!!!敲黑板做筆記!!!

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

    public int fun()
    {
        int i = 10;
        try
        {
            //doing something
            return i;
        }catch(Exception e){
            return i;
        }finally{
            i = 20;
        }
    }
}

結果爲10,創建了一個方法fun,在方法裏使用try/catch語句,方法要求返回值類型爲int型。在try裏面放回i,這個時候是10,但是在finally裏面將i值修改爲20。我們看到結果是10,好像是return先執行。那麼接下來再看另一個例子:

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

    public StringBuilder fun()
    {
        StringBuilder s = new StringBuilder("Hello");
        try
        {
            //doing something
            s.append("Word");

            return s;
        }catch(Exception e){
            return s;
        }finally{
            string.append("finally");
        }
    }
}

輸出結果:HelloWordFinally
看結果似乎有點出乎意料了,因爲這次finally裏面修改的內容生效了。看代碼其實差別不大,只是把返回值類型修改爲StringBuilder了。那麼這是爲什麼呢?下面就爲大家解釋一下其中到底是怎麼執行的。
首先,拿第一個例子來說,可以在main方法裏實現這樣一條語句:int result = test.fun();我們知道這樣做是沒有問題的,但是大家都知道“=”號賦值是常量賦值。但是,方法的存放地址和常量的存放地址是不一樣的,方法的存放在代碼區的。上面我們把一個方法賦值給一個int型也沒有報錯。那是因爲在聲明方法是我們聲明瞭返回值類型。那麼編譯器就會在代碼的最前端預留一段返回值類型的內存。執行return的時候,就會把返回的內容寫入到這段內存中
在這裏插入圖片描述
這樣,執行“=”號賦值的時候,就能在內存中匹配到相同的類型。賦值便能成功。

弄清楚上面的道理之後,再來解釋最開始提出的問題就容易多了。在執行了return之後,返回的值已經被寫入到那段內存中了,finally再修改i的值,只是修改了後面代碼段的i值,對返回段內存沒有影響。至於第二個例子,內存地址是一樣的,在字符串後面追加了內容,在return返回的時候一併返回。
當返回值不是基本數據類型的時候,其是指向一段內存的,return將返回段指向一段內存,但是代碼段的s依然是指向的同一段內存地址,所以當s修改它指向內存中的值的時候,其實也就修改了返回段指向內存中的值,所以最終的值改變。
下面有一個栗子自行閱讀,會有助於理解return和finally執行的順序,return時候內存改變

public class Test {
    public static void main(String[] args) {
        String name1 = test1("王五");
        System.out.println("test1運行結果爲:" + name1);
        String name2 = test2("王五");
        System.out.println("test2運行結果爲:" + name2);
        String name3 = test3("王五");
        System.out.println("test3運行結果爲:" + name3);
    }

    public static String test1(String name){
        try {
            name = "張三";
            return name;
        }finally {
            name = "李四";
            System.out.println("test1方法finally語句塊name:"+name);
        }
    }

    public static String test2(String name){
        try {
            name = "張三";
        }finally {
            name = "李四";
            System.out.println("test2方法finally語句塊name:"+name);
            return name;
        }
    }

    public static String test3(String name){
        try {
            name = "張三";
            return name;
        }finally {
            name = "李四";
            System.out.println("test3方法finally語句塊name:"+name);
            return name;
        }
    }
}

執行結果

test1方法finally語句塊name:李四
test1運行結果爲:張三
test2方法finally語句塊name:李四
test2運行結果爲:李四
test3方法finally語句塊name:李四
test3運行結果爲:李四

那麼執行的時候究竟做了什麼呢
1、執行:expression,計算該表達式,結果保存在操作數棧頂;
2、執行:操作數棧頂值(expression的結果)複製到局部變量區作爲返回值;
3、執行:finally語句塊中的代碼;
4、執行:將第2步複製到局部變量區的返回值又複製回操作數棧頂;
5、執行:return指令,返回操作數棧頂的值;

我們觀察到finally都執行了,那麼提出一個問題,什麼情況下,finally不會執行呢

當然不能說斷電就不執行了,現在提出這個幾個觀點來驗證
1.報運行時異常的時候finally會執行嗎
2.手動讓程序結束System.exit(0);之前寫算法的時候粗淺的用過這個函數暴力讓程序停止
3.直接炸棧的運行時異常
ps:其實主要是第二個,1,3也是異常,會按照try catch處理異常的正常流程走的

//1.報運行時異常的時候finally會執行嗎
public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println(1/0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
222

可以看到,運行時異常的時候finally還是執行了

//2.手動讓程序結束System.exit(0);之前寫算法的時候粗淺的用過這個函數暴力讓程序停止
public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println("111");
			System.exit(0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
111

finally的內容並沒有被執行,這裏我們查一下System.exit()定義

    /**
     * Terminates the currently running Java Virtual Machine. The
     * argument serves as a status code; by convention, a nonzero status
     * code indicates abnormal termination.
     * <p>
     * This method calls the <code>exit</code> method in class
     * <code>Runtime</code>. This method never returns normally.
     * <p>
     * The call <code>System.exit(n)</code> is effectively equivalent to
     * the call:
     * <blockquote><pre>
     * Runtime.getRuntime().exit(n)
     * </pre></blockquote>
     *
     * @param      status   exit status.
     * @throws  SecurityException
     *        if a security manager exists and its <code>checkExit</code>
     *        method doesn't allow exit with the specified status.
     * @see        java.lang.Runtime#exit(int)
     */
    public static void exit(int status) {
        Runtime.getRuntime().exit(status);
    }
    /**

終止虛擬機,無論返回值是多少都會退出 0代表正常退出非0代表異常

System.exit(n)這個方法等價於Runtime.getRuntime().exit(n)
ps:這句好像是廢話。。。。源碼都有


虛擬機從那一句退出了,finally當然不會執行
//3.直接炸棧的運行時異常
在這裏插入圖片描述
出乎意料的是,就算炸棧了finally依然執行了
catch中return的實驗

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		System.out.println(t.fun());
	}
	public String fun() {
		try {
			System.out.println("111");
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}

}
111
222
return

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		System.out.println(t.fun());
	}
	public String fun() {
		try {
			System.out.println(1/0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}

}
222
catch

總結

  1. 程序錯誤(運行時異常,除0炸棧),try中return catch中return finally部分的代碼都會執行
  2. 程序提前終止System.exit(0),會直接停止虛擬機,後面的finally語句不會被執行
  3. 執行是先return送值入棧,然後執行finally操作值,最後return返回結果
  4. 這個是常識性的,還是提一下,try catch相當於一個if 如果程序沒異常就try裏面的執行,如果有異常則執行catch裏面的(返回值也是根據不同分支來的),finally只要沒有中斷程序就會執行到,再返回值

參考文章

https://blog.csdn.net/sinat_22594643/article/details/80509266
https://blog.csdn.net/baiyunshi/article/details/97375218
https://blog.csdn.net/weixin_41005006/article/details/80643681
https://blog.csdn.net/zoujian1993/article/details/45362931

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章