對于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 {
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對比分析的第一篇文章,由于調研分析的時間有限,難免有疏忽之處,歡迎各位指正。