延遲計算和閉包

前言

一直覺得函數(shù)式編程中的閉包和延遲計算是很神奇的技術(shù),因為一直不知道原理,所以也不知道如何用好他們??催^幾遍介紹,但終究是沒有摸到什么頭腦,直到一個偶然的機會,突然明白了...

一個延遲計算的例子

List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().map(s->s.toUpperCase()).peek(System.out::println).collect(Collectors.toList());

這是一個Java8中運用stream計算的一個例子,意思是把stringList中的所有字符串轉(zhuǎn)換成大寫的,然后輸出出來,然后放到新的List中
??有意思的是,如果代碼寫成這樣

List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().map(s->s.toUpperCase()).peek(System.out::println);

它是不會進行System.out.println()操作的。而如果寫成這樣

List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
Stream<String> stream= stringList.stream().map(s->s.toUpperCase());

得到的stream里的字符串流還是小寫的,這就是所謂的延遲計算。
??其實我在這里挺討厭延遲計算的,之前很不明白為什么不能直截了當(dāng)?shù)慕o我計算結(jié)果,而需要進行終結(jié)操作,事實上終結(jié)操作并不是我想要的,只是為了應(yīng)對延遲計算不得已做的操作。這個問題先留在這,下面我們先看下閉包,因為這兩個技術(shù)的原理是都來自高階函數(shù)。

閉包

Java閉包的用法

public class FirstLambdaExpression {  
    public String variable = "Class Level Variable";  
    public static void main(String[] arg) {  
        new FirstLambdaExpression().lambdaExpression();  
    }  
    public void lambdaExpression(){  
        String variable = "Method Local Variable";  
        String nonFinalVariable = "This is non final variable";  
        new Thread (() -> {  
            //Below line gives compilation error  
            //String variable = "Run Method Variable"  
            System.out.println("->" + variable);  
            System.out.println("->" + this.variable);  
       }).start();  
    }  
} 

這是java8中的一個閉包的例子,用這個例子的主要目的就是演示下Java也可以用閉包,為什么使用閉包,一言以蔽之,就是為了在鏈?zhǔn)接嬎阒芯S持一個上下文,同時進行變量隔離,這么說有點抽象,再舉個例子

List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().reduce((s1,s2)->s1+s2).get();

輸出: <code>abccdeefgghiijk</code>
?? reduce()接收有兩個參數(shù)的函數(shù),它的作用是把上一次計算的結(jié)果作為第一個參數(shù),然后把這次要計算的量作為第二個參數(shù),然后進行計算。
如果不使用閉包呢,我們將會得到下面的代碼

List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
        //System.out.println( stringList.stream().reduce((s1,s2)->s1+s2).get());
String temp="";
for(String s: stringList){
    temp+=s;
    }
System.out.println(temp);

我們需要一個中間變量來維持這個計算能進行下去。好吧,我承認這沒有什么不可以接受的,我們之前就一直這樣寫。但是如果變成這樣了呢

List<String> stringList1=Arrays.asList("abc","cde","efg","ghi","ijk");
List<String> stringList2=Arrays.asList("abc","cde","efg","ghi","ijk");
//System.out.println( stringList.stream().reduce((s1,s2)->s1+s2).get());
String temp="";
for(String s: stringList1){
    temp+=s;
    }
String temp1="";
for(String s: stringList2){
    temp+=s;
}

 System.out.println(temp);
 System.out.println(temp1);

對應(yīng)是使用閉包的寫法

List<String> stringList1=Arrays.asList("abc","cde","efg","ghi","ijk");
List<String> stringList2=Arrays.asList("abc","cde","efg","ghi","ijk");
System.out.println( stringList1.stream().reduce((s1,s2)->s1+s2).get());
System.out.println( stringList2.stream().reduce((s1,s2)->s1+s2).get());

從這個例子中我們看到了使用中間變量的不便性,對java來說這個中間變量一般在方法里面,不會有多大影響,但是對應(yīng)javascript來說,太容易造成變量污染了,尤其是你用完這個字符串忘掉置空或者使用前忘記置空了,這就是為什么閉包的特性在javascript中是與生俱來的,而在java中直到第八個版本才出現(xiàn)的原因了(開玩笑的。JavaScript是從一開始就是一種可以進行函數(shù)式編程的語言,java第八版本才開始變得可以進行函數(shù)式編程,閉包是函數(shù)式編程語言必須提供的一種特性,正如例子中的reduce()函數(shù)一樣,能夠接收函數(shù)作為參數(shù)的語言,必然也天生的實現(xiàn)了閉包)。

好了到目前為止我們已經(jīng)對閉包和延遲計算有了一點點了解,那接下來我們就要探究下其實現(xiàn)原理了。在這我們先介紹一個概念高階函數(shù)

高階函數(shù)

定義

在數(shù)學(xué)和計算機科學(xué)中,高階函數(shù)是至少滿足下列一個條件的函數(shù):

  1. 接受一個或多個函數(shù)作為輸入
  2. 輸出一個函數(shù)
    ?? 在數(shù)學(xué)中它們也叫做算子(運算符)或泛函。微積分中的導(dǎo)數(shù)就是常見的例子,因為它映射一個函數(shù)到另一個函數(shù)。
    ?? 在無類型 lambda 演算,所有函數(shù)都是高階的;在有類型 lambda 演算(大多數(shù)函數(shù)式編程語言都從中演化而來)中,高階函數(shù)一般是那些函數(shù)型別包含多于一個箭頭的函數(shù)。在函數(shù)式編程中,返回另一個函數(shù)的高階函數(shù)被稱為Curry化的函數(shù)。
    ?? 在很多函數(shù)式編程語言中能找到的 map 函數(shù)是高階函數(shù)的一個例子。它接受一個函數(shù) f 作為參數(shù),并返回接受一個列表并應(yīng)用 f 到它的每個元素的一個函數(shù)。

范例

這是一個javascript 的例子, 其中函式 g() 有一引數(shù)以及回傳一函數(shù). 這個例子會打印 100 ( g(f,7)= (7+3)×(7+3) ).

function f(x){
    return x + 3
}
   
function g(a, x){
    return a(x) * a(x)
}
console.log(g(f, 7))

這是接收一個函數(shù)作為參數(shù)的例子,下面我們以一個返回一個函數(shù)的例子

function outer(){
    var a=1;
    var inner= function(){
    return a++;
    }
    return inner
}
var b=outer();
console.log(b());
console.log(b());

分析

我們從javascript語言入手進行分析是因為java語言沒有辦法定義高階函數(shù),高階函數(shù)是延遲計算和閉包的來源。順便提一句,高階函數(shù)的設(shè)計原理也并沒有多么復(fù)雜,以我了理解,高階函數(shù)實現(xiàn)起來大概來源于C語言的指向函數(shù)的指針,指向函數(shù)的指針也來源于匯編語言,對于這么底層的語言來講,沒有函數(shù)的概念只有代碼塊的概念,在代碼塊間跳來跳去,就實現(xiàn)了函數(shù),在這里我就不展開來說了。

延遲計算

我們拿上面的返回函數(shù)的例子來講

var b=outer();

此時,b是個什么?b是一個函數(shù),此時

var b=function(){
    return a++;
}

在這里<b>b只是函數(shù)定義,并沒有執(zhí)行,而函數(shù)執(zhí)行的地方在于 <key>console.log(b());</key></b>
只用這一句話,就說明了 <em>延遲計算</em> 的實質(zhì),只定義不使用。
所以回頭來看下我們前面的Java代碼里“延遲計算”,這里就比較明了了,map函數(shù)和reduce函數(shù)只是接收了函數(shù),并沒有立即執(zhí)行,這就是為什么需要一步終結(jié)操作了。

閉包

提到閉包不得不提另一個口號,那就是“在函數(shù)式編程中,函數(shù)是編程語言中的一等公民”,每個函數(shù)都可以當(dāng)做對象來使用,再舉一個例子

function a(){
    var i=1;
    return function () {
        return ++i;
    }
}
var b=a();

console.log(b());//2

var c=a();

console.log(b());//3

console.log(c());//2

可以看出b和c是隔離開的,互相不影響的,這里我們可以類比成Java中的代碼:

class Outter{

    int i=1;

    public int inner(){
  
        return this.i++;

    }

}
Outter b=new Outer();
Outter c=new Outer();
System.out.println(b.inner());
System.out.println(b.inner());
System.out.println(c.inner());

在javascript中的寫法也可以寫成:

function Outter(){
    var i=1;
    var inner=function(){
        return i++;
    }
    return inner;
}
var b=new Outter();//實際上返回一個inner對象
var c=new Outter();//實際上又返回一個inner對象
console.log(b());//1
console.log(b());//2
console.log(c());//1
console.log(c());//2

ok,到這里,基本上就能理解閉包如何使用了,在我看來,閉包實際上是函數(shù)式編程的面向?qū)ο缶幊?,或者函?shù)式編程中面向?qū)ο蟮囊环N實現(xiàn)方式。反正我是這么理解了閉包的,自從這樣想明白之后,我突然變得會使用閉包了
?? 以上

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容