對於golang一直存有覬覦之心,但一直苦於沒有下定決心去學習研究,最近開始接觸golang。就我個人來說,學習golang的原動力是因為想要站在java語言之外來審視java和其它語言的區別,再就是想瞻仰一下如此NB的語言。年前就想在2019年做一件事情,希望能從各個細節處做一次java和gola ...
對於golang一直存有覬覦之心,但一直苦於沒有下定決心去學習研究,最近開始接觸golang。就我個人來說,學習golang的原動力是因為想要站在java語言之外來審視java和其它語言的區別,再就是想瞻仰一下如此NB的語言。年前就想在2019年做一件事情,希望能從各個細節處做一次java和golang的對比分析,不評判語言的優劣,只想用簡單的語言和可以隨時執行的代碼來表達出兩者的區別和底層涉及到的原理。今天是情人節,饅頭媽媽在加班,送給自己一件貼心的禮物,寫下第一篇對比文章:java&golang的區別之:閉包。 關於閉包到底是啥,建議參考知乎上的解釋:https://www.zhihu.com/question/51402215/answer/556617311
- java8之前的閉包
1 public class ClosureBeforeJava8 { 2 int y = 1; 3 4 public static void main(String[] args) { 5 final int x = 0; 6 ClosureBeforeJava8 closureBeforeJava8 = new ClosureBeforeJava8(); 7 Runnable run = closureBeforeJava8.getRunnable(); 8 new Thread(run).start(); 9 } 10 11 public Runnable getRunnable() { 12 final int x = 0; 13 Runnable run = new Runnable() { 14 @Override 15 public void run() { 16
17 System.out.println("local varable x is:" + x); 18 //System.out.println("member varable y is:" + this.y); //error 19 } 20 }; 21 return 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 { 2 int y = 1; 3 4 public static void main(String[] args) throws Exception{ 5 final int x = 0; 6 ClosureInJava8 closureInJava8 = new ClosureInJava8(); 7 Runnable run = closureInJava8.getRunnable(); 8 Thread thread1 = new Thread(run); 9 thread1.start(); 10 thread1.join(); 11 new Thread(run).start(); 12 } 13 14 public Runnable getRunnable() { 15 final int x = 0; 16 Runnable run = () -> { 17
18 System.out.println("local varable x is:" + x); 19 System.out.println("member varable y is:" + this.y++); 20 }; 21 return 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() { 6 ch := make(chan int ,1) 7 ch2 := make(chan int ,1) 8 fn := closureGet() 9 go func() { 10 fn() 11 ch <-1 12 }() 13 go func() { 14 fn() 15 ch2 <-1 16 }() 17 <-ch 18 <-ch2 19 } 20 21 func closureGet() func(){ 22 x := 1 23 y := 2 24 fn := func(){ 25 x = x +y 26 fmt.Printf("local varable x is:%d y is:%d \n", x, y) 27 } 28 return 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對比分析的第一篇文章,由於調研分析的時間有限,難免有疏忽之處,歡迎各位指正。