第一章.java&golang的區別之:閉包
對於golang一直存有覬覦之心,但一直苦於沒有下定決心去學習研究,最近開始接觸golang。就我個人來說,學習golang的原動力是因為想要站在java語言之外來審視java和其它語言的區別,再就是想瞻仰一下如此NB的語言。年前就想在2019年做一件事情,希望能從各個細節處做一次java和golang的對比分析,不評判語言的優劣,只想用簡單的語言和可以隨時執行的程式碼來表達出兩者的區別和底層涉及到的原理。今天是情人節,饅頭媽媽在加班,送給自己一件貼心的禮物,寫下第一篇對比文章:java&golang的區別之:閉包。
關於閉包到底是啥,建議參考知乎上的解釋:https://www.zhihu.com/question/51402215/answer/556617311
- java8之前的閉包
在java8之前,java其實就已經對閉包有了一定層面的支援,實現的閉包方式主要是靠匿名類來實現的,下面是java程式設計師經常寫的一段程式碼:
1 public class ClosureBeforeJava8 { 2int y = 1; 3 4public static void main(String[] args) { 5final int x = 0; 6ClosureBeforeJava8 closureBeforeJava8 = new ClosureBeforeJava8(); 7Runnable run = closureBeforeJava8.getRunnable(); 8new Thread(run).start(); 9} 10 11public Runnable getRunnable() { 12final int x = 0; 13Runnable run = new Runnable() { 14@Override 15public void run() { 16 17System.out.println("local varable x is:" + x); 18//System.out.println("member varable y is:" + this.y); //error 19} 20}; 21return run; 22} 23 }
上段程式碼的輸出:local varable x is:0
在程式碼的第13行到第20行,通過匿名類的方式實現了Runnable介面的run()方法,實現了一部分操作的集合(run方法),並將這些操作對映為java的物件,在java中就可以實現將函式以變數的方式進行傳遞了,如果僅僅是傳遞函式指標,那還不能算是閉包,我們再注意第17行程式碼,在這段被封裝可以在不同的java物件間傳遞的程式碼,引用了上層方法的區域性變數,這個就有些閉包的意思在裡面了。但是第18行被註釋掉的程式碼在匿名類的情況下卻無法編譯通過,也就是封裝的函式裡面,無法引用上層方法所在物件的成員變數。總結一下,java8之前的閉包特點如下:
1.可以實現封裝的函式在jvm裡進行傳遞,可以在不同的物件裡進行呼叫;
2.被封裝的函式,可以呼叫上層的方法裡的區域性變數,但是此區域性變數必須為final,也就是不可以更改的(基礎型別不可以更改,引用型別不可以變更地址);
3.被封裝的函式,不可以呼叫上層方法所在物件的成員變數;
- java8裡對閉包的支援
java8裡對於閉包的支援,其實也就是lamda表示式,我們再來看一下上段程式碼在lamda表示式方式下的寫法:
1 public class ClosureInJava8 { 2int y = 1; 3 4public static void main(String[] args) throws Exception{ 5final int x = 0; 6ClosureInJava8 closureInJava8 = new ClosureInJava8(); 7Runnable run = closureInJava8.getRunnable(); 8Thread thread1 = new Thread(run); 9thread1.start(); 10thread1.join(); 11new Thread(run).start(); 12} 13 14public Runnable getRunnable() { 15final int x = 0; 16Runnable run = () -> { 17 18System.out.println("local varable x is:" + x); 19System.out.println("member varable y is:" + this.y++); 20}; 21return run; 22} 23 }
上面對程式碼輸出:
local varable x is:0
member varable y is:1
local varable x is:0
member varable y is:2
在程式碼的第16行到第20行,通過lamda表示式的方式實現了函式的封裝(關於lamda表示式的用法,大家可以自行google)。通過程式碼的輸出,大家可以發現,在lamda表示式的書寫方式下,封裝函式不但可以引用上層方法的effectively final型別(java8的特性之一,其實也是final型別)的區域性變數,還可以引用上層方法所在物件的成員變數,並可以在其它執行緒和方法中對此成員變數進行修改。總結一下:java8對於閉包支援的特點如下:
1.通過lamda表示式的方式可以實現函式的封裝,並可以在jvm裡進行傳遞;
2.lamda表示式,可以呼叫上層的方法裡的區域性變數,但是此區域性變數必須為final或者是effectively final,也就是不可以更改的(基礎型別不可以更改,引用型別不可以變更地址);
3.lamda表示式,可以呼叫和修改上層方法所在物件的成員變數;
由於還沒時間分析jdk和hotspot的原始碼,在此只能猜測推理,第2點和第3點的情況。關於第2點:上層方法的區域性變數必須是final修飾的,網上的文章大部分都是說因為多執行緒併發的原因,無法在lamda表示式裡進行修改上層方法的區域性變數,這點上我是不同意這個觀點的。我認為主要原因是:java在定義區域性變數時,對於基礎型別都是建立在stack frame上的,而一個方法執行完畢後,此方法所對應的stack frame也就沒有意義了,試想一下,lamda表示式所依賴的上層方法的區域性變數的儲存區(stack frame)都消失了,我們還怎麼能夠修改這個變數,這是毫無意義的,在java裡也很難實現這一點,除非像golang一下,在特定情況下,更改區域性變數的儲存區域(在heap裡儲存)。關於第3點:實現起來就比較容易,就是在lamda表示式的物件裡,建立一個引用地址,地址指向原上層方法所在物件的堆儲存地址即可。
- golang裡對閉包的支援
golang裡對於閉包的支援,理解起來就非常容易了,就是函式可以作為變數來傳遞使用,程式碼如下:
1 package main 2 3 import "fmt" 4 5 func main(){ 6ch := make(chan int ,1) 7ch2 := make(chan int ,1) 8fn := closureGet() 9go func() { 10fn() 11ch <-1 12}() 13go func() { 14fn() 15ch2 <-1 16}() 17<-ch 18<-ch2 19 } 20 21 func closureGet() func(){ 22x := 1 23y := 2 24fn := func(){ 25x = x +y 26fmt.Printf("local varable x is:%d y is:%d \n", x, y) 27} 28return fn 29 }
程式碼輸出如下:
local varable x is:3 y is:2
local varable x is:5 y is:2
程式碼的第24行到27行,定義了一個方法fn,此方法可以使用上層方法的區域性變數,總結一下:
1.golang的閉包在表達形式上,理解起來非常容易,就是函式可以作為變數,來直接傳遞;
2.golang的封裝函式可以沒有限制的使用上層函式裡的區域性變數,並且在不同的goroutine裡修改的值,都會有所體現。
關於第2點,大家可以參考文章:https://studygolang.com/articles/11627 中關於golang閉包的講解部分。
- 總結
golang的閉包從語言的簡潔性、理解的難易程度、支援的力度上來說,確實還是優於java的。本文作為java和golang對比分析的第一篇文章,由於調研分析的時間有限,難免有疏忽之處,歡迎各位指正。