1. 程式人生 > >[Java]“語法糖”系列(二)之Lambda表示式/匿名函式(Lambda Expression)

[Java]“語法糖”系列(二)之Lambda表示式/匿名函式(Lambda Expression)

>什麼是Lambda表示式

    簡短的說,Lambda表示式是一種用於取代匿名類,把函式行為表述為函數語言程式設計風格的一種匿名函式。

    匿名類大家自然熟悉,上面那句話提到了“函式行為”,那麼什麼是函式行為?

>函式行為

    假設有這樣一個應用場景:計算機要通過給定的某工人的薪水計算出該工人要交納多少稅。我們有以下程式碼:

@FunctionalInterface
interface TaxFunction{
	double tax(double salary);
}

class Computer{
	public double taxing(double salary,TaxFunction func){
		return func.tax(salary);
	}
}

    計算機計算稅收,應當有一個計算方式,這個計算方式就是TaxFunction介面類中給出的抽象方法tax。之所以要設計成抽象的方法,因為針對不同的收入階層或是企業型別,可能會有不同的稅收方法。假設對於工人A,稅收為其總收入salary的10%:

Computer computer = new Computer();
double hisTax = computer.taxing(251, new TaxFunction() {
	@Override
	public double tax(double salary) {
		return salary*0.1;
	}
});

    這其中我們使用了一個匿名類TaxFunction並直接重寫了tax方法。這個tax方法就是一種函式行為,其描述了這樣一種行為:x->f(x)=x*0.1,也就是傳入一個狀態值x,返回經過函式f對映後的值0.1*x。

    這是一種函數語言程式設計的思想。關於函數語言程式設計,還可以檢視>這篇文章<中第一個例子輔助理解。

>短小精悍的Lambda表示式

   在上面那個例子中,很容易注意到我們只為了完成 x->f(x)=x*0.1 的對映,就要new 一個匿名類 TaxFunction寫上足足至少四行程式碼來完成這個函式行為。Java本來就以冗餘被人所詬病,這樣的寫法雖然看上去沒什麼問題,但是也暴露了Java程式碼“又臭又長”的缺點。

   而Java中的Lambda表示式就是JDK8中為了避免這種冗餘而出現的一種新特性。藉由Lambda表示式,上面那段程式碼要怎麼寫呢?

Computer computer = new Computer();
hisTax = computer.taxing(251, x -> x*0.1);
   一行就搞定了。要想知道Lambda表示式究竟是怎麼完成這一個操作的,我們需要先了解兩個東西:Lambda表示式的寫法函式式介面@FunctionalInterface

>函式式介面@FunctionalInterface

   在JDK8中提供了一個新包java.util.function,在這個包裡給出了很多常用的函式式介面。我們這裡以public interface Function<T, R>作講解:

@FunctionalInterface//1.函式介面註解
public interface Function<T, R> {

    //2.唯一的一個抽象方法
    R apply(T t);

    //下面這些方法不用管
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

    注意到,一個函式式介面有兩個要素:

  • 唯一的一個public抽象方法。(注意:除了這個唯一的抽象方法還允許存在從Object繼承下來的抽象方法,如toString()、equalTo())
  • @FunctionalInterface註解,這個註解可以顯著增加程式碼的可讀性,同時,當程式設計者不小心在該函式式介面中定義了多於一個的抽象方法時,IDE將會給出錯誤資訊,杜絕一些低階程式設計錯誤。如下所示:
        

>Lambda表示式的寫法

   我們先來想想一個函式需要哪些必須內容呢?

  • 傳入的引數
  • 返回的值
    也就是 x - >f(x) 的一個對映,只需要有傳入的引數(也稱為狀態)x和返回值(也稱為對映值)f(x)。至於這個函式叫什麼、返回值型別是什麼,都不重要。一個匿名函式只需要以上兩個必須要素。

     結合上面提到的函式介面@FunctionalInterface,之所以要求必須是【唯一】的一個抽象方法,就是為了方便在用Lambda表示式替代匿名類中的函式式時能夠方便地找到要替換的匿名函式。

    那麼Lambda表示式應該怎麼書寫呢?

    在Lambda表示式中,有兩種型別的短句:表示式(expression)和語句(statement)。表示式總是有返回值的,如3+2和3>2,一個返回int一個返回布林值;語句總是沒有返回值的,如System.out.println("身披白袍's部落格")。表示式和語句可以被一對花括號 { } 裹住,被稱作一個語句塊,語句塊可以類比於一般函式中public void func(int x){ $內容 } 中的{ $內容 }。

    一個Lambda表示式總是以以下的形式出現:

  • ($引數表) -> { $短句 }

   * 當且僅當$短句只有一句時,可以省略花括號 { };若此時唯一的短句為一個表示式,可以省略return關鍵詞。當左側引數只有一個時,可以省略一對圓括號();

   * 左側就是剛剛提到的兩個函式要素中的“傳入的引數”,-> 右側的內容就是“返回的值”(包括void)。

   我們來舉個例子,若在上文的收稅模型中,我們要先列印這個工人的工資再求稅收,需要這麼寫:

hisTax = computer.taxing(251, x -> { // 語句塊,用花括號括起來
	System.out.println("salary = " + x); // 列印工人的工資
	return x * 0.1; // 此時這裡多了一個return,因為已經不是唯一的表示式了
});

    看完這些內容,再回到我們上面計算稅收的例子:

//沒用Lambda表示式前
double hisTax = computer.taxing(251, new TaxFunction() {
	@Override
	//唯一的抽象函式,可被視為一個匿名函式
	public double tax(double salary) {
		return salary*0.1;
	}
});

//用Lambda表示式替代了介面中唯一的抽象函式tax()
hisTax = computer.taxing(251, x -> x*0.1);
    是不是就能明白Lambda表示式x -> x*0.1是怎麼完成匿名函式tax的功能了呢?

    如果還沒有體會到Lambda表示式的魔力,我們再舉個例子:

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 4);

LinkedList<Integer> list = nums.stream().
		filter(x -> x>1).
		map(n -> n+1 ).
		collect(Collectors.toCollection(LinkedList::new));
    這段程式碼的功能是,過濾出大於1的數字,然後把這些大於1的數字加一,再包裝成一個LinkedList

    我們一行一行來看:

LinkedList<Integer> list = nums.stream().
    第一行,把一個List<Interger>轉化為資料流。關於stream流,這裡有一篇非常棒的入門文章:https://www.ibm.......mapi/。可以暫時不看,你只需要知道它的功能是把nums裡的資料挨個排隊然後送往下一個函式filter():
filter(x -> x>1).
    第二行,這是一個Lambda表示式,直接給出了函式行為:輸入一個x狀態,然後判斷這個狀態是否大於一;如果大於1,送往下一個函式map:
map(n -> n+1 ).
    第三行,這是一個對映函式。在steam中很重要的概念就是對映/摺疊(map/reduce或fold)。它的功能可以理解成把資料流中的每一個狀態x進行一個對映操作,比如這裡就是把每個狀態加一後送往下一個函式collect:
collect(Collectors.toCollection(LinkedList::new));
    第四行,這是一個收集函式,是stream中的Terminal操作(終點操作)。所有資料被送到這裡後會被收集,並不再往下流動。Collectors.toCollection(LinkedList::new)是一個方法引用,目的是進一步簡寫Lambda函式,可以到這裡檢視相關的內容:http://blog.csdn.net/Shenpibaipao/article/details/78614280。此處只需要知道它把所有經過過濾、加一後的狀態收集起來並封裝成一個LinkedList就行了。

    如果我們不用Lambda表示式來書寫這段程式碼,會是怎麼樣的呢:

LinkedList<Integer> list2 = nums.stream().
		filter(new Predicate<Integer>() {
			@Override
			public boolean test(Integer integer) {
				if(integer>1)return true;
				return false;
			}
		}).
		map(new Function<Integer, Integer>() {
			@Override
			public Integer apply(Integer integer) {
				return integer+1;
			}
		}).
		collect(Collectors.toCollection(LinkedList::new));
    是不是非常非常長且非常不直觀?可以說,用了Lambda表示式來書寫函式行為,生產效率和程式碼美觀度將會得到極大的提高。

    看到這裡基本上對Lambda表示式有了基礎的瞭解。當然瞭解還是不夠的,多看一些實戰用例可以更好地理解Lambda表示式:http://www.importnew.com/16436.html