lambda表達式、函數式接口、方法引用、高階函數:Core Java 6.3

方法參數:當要傳遞的參數爲一段代碼塊時,該如何傳遞

java 8 之前,參數爲代碼塊的應用場景及實現實例

當用Arrays::sort( T[] a, Comparator<? supter T> c)方法對數組中的對象進行排序時,需要傳遞一個比較器對象(Comparator接口的實例c)爲參數。

最終,是將比較器中實現的接口方法Comparator::compare(T first, T second)中的代碼塊傳遞給sort方法,在sort方法中,會調用c.compare(a,b)來對數組中的所有元素兩兩比較,以此確定元素的順序。

在jdk1.8版本之前,因爲Java的面向對象特性,要傳遞代碼塊作爲參數,只能傳遞一個其中定義了要傳遞的代碼塊的類的對象。具體步驟是:先創建一個類,類中定義一個包括了要傳遞的代碼塊的方法,然後創建一個此類的對象,將此對象作爲參數傳遞。

示例:如果我們想對字符串排序,但是不想按照字符串的字母順序對字符串進行排序,而是想依據是字符串的長度對字符串排序,長度越小越靠前、長度越長越靠後,如下代碼實現了這種排序。

import java.util.*;
public class SortTest{
	public static void main(String [] args){
		String [] strings = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus"};
		
		Arrays.sort(strings, new LengthCompare());
		
		System.out.println(strings);  // 按字符串長度順序排序的序列,串越短越靠前,串越長越靠後
	}
}

class LengthCompare implements Comparator<String>{

	public int compare(String first,String second){
		return first.length()-second.length();
	}
}

爲了傳遞一個代碼塊,需要創建一個類,再創建一個對象實例,這種方式讓人感覺有點冗餘,不夠方便、簡潔(不過Java好像就是如此??)。
java se 8提供了lambda表達式,可以使得這種場景下的代碼更簡潔。

lamada表達式

lambda即希臘字符λ的英文形式。

λ表達式的語法規範

個人理解:由於本身lamda表達式是用來替代方法參數的,因此其語法像是匿名方法(自創的名詞,不是JAVA術語),就像是將方法的定義省去了方法訪問權限限定符、返回值類型、方法名,只保留了方法參數和方法體。

λ表達式的常規語法格式

(參數類型1 參數名, 參數類型2 參數名…) -> { 代碼塊;}

對於引子中的代碼,可以使用Lambda表達式使之更簡潔:

import java.util.*;
public class  SortTest{
	String []  strings = {...};
	
	Arrays.sort(strings, 
					(String first,String second)-> {
						return first.length()-second.length();
					}
				);
	System.out.println(strings);  // 按字符串長度順序排序的序列,串越短越靠前,串越長越靠後
				
}

省略參數類型

當編譯器可以根據上下文確定參數類型時,λ表示式中的參數可以省略參數類型的聲明。

如上例中,Comparator接口只有一個抽象方法,方法中自然對參數類型做了確定地聲明。而根據Arrays.sort(T[] a,Comparator<? super T> c)方法的聲明,就可以確定接λ表示的參數類型爲? super T。T雖然是泛型,但是在實際運行時,卻可以根據實際傳入的第一個參數的類型來確定。

Arrays.sort(strings, 
				(first, second) -> { return first.length()- second.length(); } 
			);

省略參數外的()

當只有一個參數,且可以省略參數類型時,可以省略參數外的(),但必須同時省略參數的類型

省略{}

當lambda體只有一句代碼時,可以省略{},但必須同時省略return和語句結尾的分號;。因爲不在{}中時,不是代碼塊,只能算作一個表達式。

而當lamada體用花括號{}括起來時,{}中必須是語句,每句要有;,返回語句要加return 。

Arrays.sort(ss, 
	(first , second) -> { return first.length() - second.length(); }
)
// 可簡寫如下
Arrays.sort(ss, 
	(first , second) -> first.length() - second.length()
)

當沒有參數時,必須有()

即使λ表達式沒有參數,仍然要提供(),就像無參方法一樣。

	()->{
			for(int i = 100; i >= 0; i--)
				System.out.println(i);
		}

λ表達式的作用原理及意義:函數式接口

函數式接口

有且只有一個抽象方法的接口,稱爲函數式接口。

當需要函數式接口的對象時,就可以提供一個λ表達式。例如:

Arrays.sort(strings, 
			(first, second)-> { return first.length()-second.length();

在底層,Arrays.sort 方法會接收實現了 Comparator 的某個類的對象(底層是如何實現的??-是否運行時JDK動態地生成了接口的一個實現類A,直接將lamada表達式的體作爲類A對接口中抽象方法的實現?-待研),在這個對象上調用compare方法時,會執行lambda表達式內的體。

最好把lambda表達式看作一個函數,而不是一個對象。即運行時,是把lambda表達式傳遞到函數式接口。(但是在語法上,卻可以用一個變量引用它,此變量是對方法的引用,而不是對象的引用)
語法示例:

Comparator  c = 
	(first, second)->{ return  first.length() - second.length();}

在java中,lambda表達式的意義也只在於轉換爲函數式接口。

可以對函數式接口加註解:@FunctionalInterface,但這不是必須的。即便不加,只要接口有且只有一個抽象方法,JDK就認爲它是一個函數式接口。

方法引用

語法規範

類名::方法名

應用場景:

當函數式接口的實現類中的方法,僅僅是調用了其它類的一個方法時,就可以直接用方法引用來取代lambda表達式。例如:

Timer timer = new Timer(1000, event -> System.out.println(event));

可以看到,lambda體內部其實是直接調用了其它方法,並沒有任何自己的算法。這時可以寫成如下形式:

Timer timer = new Timer(1000,System.out::println);

表達式:System.out::println就是一個方法引用,它等價於lambda表達式 event -> System.out.println(event)

又如:要實現不分大小寫的字符串排序,String本身提供了一個不分大小寫的比較方法compareToIgnoreCase,我們可以如下實現:

String [] strings = {""};
Arrays.sort(strings, 
	(first,second)->{return first.compareToIgnoreCase(second);}
);

此時lambda表達式的體中,只是調用了JDK中String類的一個成員方法compareToIgnoreCase方法,我們可以作如下簡寫:

Arrays.sort(strings, String::compareToIgnoreCase)

compareToIgnoreCase是String類的一個實例方法,jvm會將第一個參數作爲方法的調用者,其餘的參數作爲被調用的方法的參數

同lambda表達式一樣,方法引用也只能轉化爲函數式接口的實例。

可以在方法引用中用this參數

如:this::equals等同於x -> this.equals(x)

構造器引用

和方法引用類似,不過方法名爲new,如:
Person::new相當於lambda表達式(String name) -> { return new Person(name); }
int[]::new相當於lambda表達式x -> new int[x]

當lambda方法需要訪問外圍方法或者類中的變量

例如:

public static void repeatMessage(String text ,int delay){ 
	ActionListener listener = event -> {
		System.out.println(text);
		Toolkit.getDefaultTookit.beep();
	}; //函數式接口
	new Timer(delay, listener).start();
}

如果我們這樣調用上面的方法:

String text = "hell";
repeatMessage(text,1000); // 每1000毫秒打印hello,並響鈴

那麼會如下問題:lambda表達式的代碼可能會在repeatMessage方法執行完畢很久之後纔會運行,而那時repeatMessage方法早已執行完,線程棧中並不會有這個方法調用的棧幀,text等方法的變量的作用域也早已隨着repeatMessage方法的執行完畢而消失。事實上,lambda表達式的代碼是在另一個線程中執行的。

對於此問題,這裏引入一個概念:lambda表達式中的自由變量。

自由變量

lambda表達式有3個部分:

  1. 一個代碼塊
  2. 參數
  3. 自由變量的值

關於lambda表達式中自由變量的語法規範

  1. lambda表達式必須存儲自由變量的值,在上例中就是"hello",我們說它被lambda變量捕獲(captured)。

關於代碼塊及自由變量值有一個術語叫做閉包(closure),lambda表達式就是一個閉包。

  1. lambda表達式捕獲的自由變量必須是最終變量(effectively final),也就是一旦初始化後就不能改變的。

在代碼實現上,要麼是類似String,LocalDate這種類,它們的值本身就是final的,要麼就直接把變量聲明爲final。

原因之一是如果lambda表達式中的代碼塊是併發的,那麼對自由變量的更新就是不安全的;另外一個原因是如果不是final的,那麼lambda表達式外部也能夠對此變量進行更新。

下例就是一個不合法示範:

public static repeat(String text,int count){
	for(int i = 0; i < count; i++){
		ActionListener listner = event ->
			{
				System.out.println(i + ":" + text);
			};
		new Timer(1000, listener).start();
	}
}

高階函數

如果一個函數,接受一個函數作爲參數,或者返回一個函數作爲返回值,那麼這個函數就叫高階函數。

這裏的函數,也可以擴展爲函數式接口。如果一個方法接收一個函數式接口的實例作爲參數,或者返回一個函數式接口的實例,也是高階函數。

Arrays::sort(Comparator c)就是一個高階函數。

通用的函數式接口 ( java.util.function.* )

java.util.function包中定義了一系列的類似 Comparator / Runnable 的函數式接口。

函數式接口的通用性

函數式接口本身,無非是定義了一個接口方法,包括方法的 參數個數、參數類型、返回值類型。

而在泛型機制的支持下,參數類型、返回值類型,也不是函數式接口的限定因素了。

至此,函數式接口的接口方法的定義的限定,只在於參數個數、有無返回值上。

因此,函數式接口的通用性大大提高。

例如:Runnable接口本身在JDK中承載的意義是多線程的線程操作。但是一旦我們按照函數式接口的通用性進行拆解,就會發現,Runnable接口的接口方法就是定義了一個無參數、無返回值的行爲。
如果我們有個場景,比如每10分鐘就響一次鬧鈴,那麼就可以將響鈴這個行爲作爲Runnable接口的實現類的行爲。

再如:Comparator接口本身在JDK中承載的意義是比較兩個對象。但是按照函數式接口的通用性進行拆解,這個接口就是定義了一個有 兩個相同類型的參數、返回int類型值 的接口方法。
如果我們有個應用場景是對兩個int型的數做減法,後再進行一系列其它操作,最後再返回運算結果,也可以通過實現Comparator接口來定義這個行爲。但是要確保正常情況下的運算結果一定是int類型。

也就是說,對於以下所有方法,由於他們的參數個數相同、且都是有返回值的,因此一個函數式接口就可以代替以下所有方法的定義:

public class Test{
	int add(int a,int b){return a+b;}
	int sub(int b,int b){return a-b;}
	long multi(long a,long b ){return a*b;}
	
	public static void main(String[] args){
		int a=2,b=3;
		int c = new Test().add(a,b);
		int d = new Test().sub(a,b);
		long e = new Test().multi(a,b);
	}
}	

用一個高階函數,替代以上三個方法的定義:

public class Test{
	T compute(T a,T b,BiOperator<T> operator){
		return operator.apply(a,b);	
	}
	public static void main(String[] args){
		int a=2,b=3;
		int c = new Test().compute(a,b,(a,b)->{return a+b});
		int d = new Test().compute(a,b,(a,b)->{return a-b});
		long e = new Test().compute(a,b,(a,b)->{return a*b});
	}
}	

在上邊的示例中,將三個方法的方法體作爲高階函數compute的一個參數,並將三個方法的參數,作爲高階函數compute的其它參數傳遞給它,而在compute中會將方法體應用到其它參數上去,變相實現了三個方法的執行(實際就是原來的方法的方法體作用於原來的方法的參數)。
只不過方法不用顯式地聲明和定義了,而是直接作爲高階函數的參數,在調用高階函數時再定義出來。
又如:

public  class  Example{
	boolean  startsWith(String s,String start){return s.startsWith(start);}
	boolean  lessThan(int a,int b){return a-b<0;}
	boolean  greaterThan(double a,double b){return a-b>0;}
	
	// 以上方法的定義可以由這一個高階函數替代
	boolean compute(T a,T b,BiPredicate<T,T> predicate){
		return predicate.test(a,b);
	}
	//實際的使用示例:
	public static void main(String [] args){
		boolean r1 = new Example.compute("fangfang","f",(a,b)->{return a.startsWith(b);});	
		boolean r1 = new Example.compute(3,4,(a,b)->{return a-b<0;});
		boolean r1 = new Example.compute(12.3,24.6,(a,b)->{return a-b>0;});	
	}
}

通用函數式接口

以下爲不超過一個參數的通用函數式接口:

接口名 方法定義 描述 釋義
Runnable void run() 無參數、無返回值
Supplier<T> T get() 無參數、有返回值。 Supplier,供給方,不需要參數,只供應出返回值
Consumer<T> void accept(T t) 一個參數、無返回值。 Consumer,消費者,只進不出,只接收參數,不返回
Function<T,R> R apply(T t) 一個參數、返回值的類型可以與參數類型不同 。 Function,典型的方法,參數+返回值
Predicate<T> boolean test(T t) 一個參數, 返回boolean值。 Predicate,斷言,是或者不是,返回boolean值
UnaryOperator<T> T apply(T t) Function接口的子接口,限定參數和返回值同類型 UnaryOperator,一元運算符,類似於運算符++或者–的函數,即只需要一個參數,且返回值類型與參數相同,因此取名爲Unary(一元)Operator(運算符)

以下爲兩個泛型參數的通用函數式接口,均以Bi開頭,即Binary(二元)函數:

接口名 方法定義 描述 釋義
BiConsumer<T,U> void accept(T t, U u) 二個參數、無返回值。 BiConsumer,消費者,二元函數,只進不出,只接收參數,不返回
BiFunction<T,U,R> R apply(T t, U u) 二個參數、返回值的類型可以與參數類型不同 。 BiFunction,二元方法,二個參數+返回值
BiPredicate<T,U> boolean test(T t, U u) 二個參數,返回boolean值。 BiPredicate,斷言二元函數,是或者不是,返回boolean值
BinaryOperator<T> T apply(T t, T u) BiFunciton的子接口,限定了二個參數的類型和返回值的類型全都相同 BianryOperator,二元運算符,類似於運算符+或者-的函數,需要二個操作數,且返回值與參數的類型相同,因此取名爲Binary(二元)Operator(運算符)

其它指定了確定類型的參數或者返回值的函數式接口:

接口名 方法定義 描述 釋義
BooleanSupplier boolean getAsBoolean() 指定返回值是boolean類型的供給者 BooleanSupplier(boolean供給者),即供出一個boolean類型的返回值,不需要原材料即不需要參數
IntSupplier
LongSupplier
DoubleSupplier
指定返回值類型爲 Int / Long / Double的供給者
IntConsumer
LongConsumer
DoubleConsumer
指定參數類型爲 Int / Long / Double的消費者
ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
兩個參數的消費者,第一個參數泛型,指定第二個參數類型爲 Int / Long / Double的消費者
IntPredicate
LongPredicate
DoublePredicate
指定參數類型爲 Int / Long / Double的斷言
ToIntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
指定返回值類型爲 Int / Long / Double的一元方法
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunciton
DoubleToLongFunction
指定參數類型爲 Int,返回值類型爲 Long / Double的一元方法
ToIntBiFunction<T, U>
ToLongBiFunction<T, U>
ToDoubleBiFunction<T, U>
指定返回值類型爲 Int / Long / Double的二元方法
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
指定了參數類型的一元函數,參數類和返回值類型同爲 Int / Long / Double的一元函數
IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
指定了參數類型的二元函數,參數類和返回值類型同爲 Int / Long / Double的一元函數
通用函數式接口中的默認方法和static 方法
接口名 默認方法 說明
Consumer<T> Consumer<T> andThen(Consumer<? superT> c) 可以充分利用andThen,將一個Consumer中的語句,寫成多個andThen的方法調用
BiConsumer<T, U> BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> c)
BiFunction<T, U> BiFunction<T, U> andThen(BiFunction<T, U> f)
BiPredicate<T, U> BiPredicate<T, U> and(BiFunction<T, U> other)
BiPredicate<T, U> BiPredicate<T, U> or(BiFunction<T, U> other)

default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

Predicate

此接口的抽象方法返回一個boolean值。聲明如下:

@FunctionalInterface
public interface Predicate<T>{
	boolean test(T t);
	...
}

有個方法ArrayList<E> :: removeIf( Predicate<? super E> filter ),實現的業務是if( filter.test(element)) 則最終移會除掉element,應用的示例如下:

import java.util.*;
public class Test
{
	public static void main(String [] args){
	
		String [] ss = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus"};

		ArrayList<String> list= new ArrayList<String>(Arrays.asList(ss));

		list.removeIf(
			(String e)->{ return e.length()>5; }  // 移除長度大於5的字符串
		);	
		// 可簡寫爲:list.removeIf(e -> e.length()>5)
	}
}

處理lambda表達式

使用lambda表達式的重點是延遲執行。畢竟,如果想要立即執行代碼,完全可以直接執行,而無需把它包裝到一個lambda表達式中。之所以希望以後再執行,這有很多原因(有很多需求場景下必須延遲執行),如:

  • 在一個單獨的線程中運行代碼
  • 多次運行代碼
  • 在算法的適當位置運行代碼
  • 發生某種情況時執行代碼
  • 只在必要時才運行代碼

以上這段文字出自Core Java 6.3。個人只能理解線程的延遲執行的,對於其它4種都不明白爲什麼叫延遲執行?照其它幾個來說,對任何方法的調用都是延遲執行。o(︶︿︶)o,如果有實踐經驗的大神看到,還請評論裏解惑一下:->

個人嘗試從其它角度分析什麼場景下要使用lambda表達式:既然lambda表達式或者方法引用僅能轉化爲函數式接口發揮作用,那麼使用函數式接口的原因就是使用lambda表達式的原因。試着舉例分析一下:在Arrays.sort( T [], Comparator<? super T> c)方法中,調用c.compare(T x,T y))時調用的是每種類自己的compare方法,因爲每種類的比較都有不同的邏輯,因此無法在sort方法中直接寫比較算法,只能調用每種類自己的比較方法,那麼就只能用統一的接口來規範方法參數、名稱、返回值。這跟“延遲執行”的概念好像並沒有什麼關係。)

在Core Java 6.7中,所謂處理lambda表達式,看起來就是當你根據需求寫出來了一段用到了lambda表達式的代碼時,你應該選擇什麼樣的函數式接口去接收它。

示例:
假設你想重複一個動作n次,將這個動作和重複的次數傳遞到一個repeat方法:

repeat(10, () -> System.out.println("hello world"));

要接受這個lambda表達式,需要選擇(偶爾可能需要你自己編寫提供)一個函數式接口。這個例子中,我們可以選擇Runnable接口,因爲Runnable接口的抽象方法沒有參數、沒有返回值,恰好符合我們的lambda表達式。因此,我們可以根據這個需求的代碼而定義以下方法:

public static viod repeat(int n, Runable action){
	for (int i = 0; i < n; i++){
		action.run();
	}
}

另一個例子:在上例的需求之上,我們希望告訴這個動作它現在是在第幾次的迭代中執行的,因此需要傳遞一個參數x。據此,我們需要選擇這樣一個函數式接口,它的抽象方法有一個int類型的參數,沒有返回值。我們可以選擇IntConsumer接口:

// JDK 的IntConsumer如下
@FunctionalInterface
public interface IntConsumer{
	void accept(int value);
	...
}

因此我們的lambda表達式應該這樣寫的:

repeat(int n ,i -> System.out.println("iterations "+i))

我們的方法應該這樣定義:

public static void repeat(int n ,IntConsumer c){
	for (int i = 0; i < n; i++){
		c.accept(i);
	}
}

當需要傳遞一段代碼塊時(比如以上5中情況下),最好使用java.util.function包下的函數式接口,比如當需要對滿足特殊條件的文件進行處理時,可以用FileFiler來對特殊條件進行定義,但是使用函數式接口Predicate也完全可以處理這種情況,這時我們應該優先選擇後者。

另外,大多數函數式接口都提供了非抽象方法來生成或者合併函數,例如Predicate.isEquals(a)等同於a::equals,不過前者中a爲null也能正常工作。已經提供了默認方法and、or和negate來合併謂謂詞,例如:Predicate.isEquals(a).or(Prediate.isEquals(b))
就等同於
x -> a.equals(x)|| b.equals(x)

有個疑問,Lambda是不是違背了java語言的初衷?

附:在實際的項目工作中,lambda到底受不受歡迎?

lambda表達式本身是個語法,如果討論性能,指的是用lambda充當匿名內部類、方法引用等場合。說這種場合效率低,我認爲沒有根據。可能別人想說的是在處理集合數據的時候,stream操作比結構化代碼效率低。這些性能差異多數情況下可以忽略。
lambda的特點還在於開發成本高,並且異常難以排查。它的異常堆棧比匿名內部類還要難懂。如果你把stream的操作都寫在同一行,則問題更甚。
另外,lambda目前還不是Java程序員必備技能,你留在項目裏的代碼可能會造成後續維護上的困難。

若魚1919
Bbs7 版主
性能不用擔心,大不了新版本的java繼續優化
可讀性和項目組成員的接受程度這個更重要

作者:hitsmaxft
鏈接:https://www.zhihu.com/question/37872003/answer/1062822405
來源:知乎

lambda 可以非常友好地替換掉冗餘大量老的 SAM(single abstract method) 匿名類,但是 lambda 始終需要一個完備的 ide 支持, 否則寫的時候爽, 後期維護就想殺人了.

// 寫的默認的lambda 代碼, 寫起來方便
future.then((r)-> { return r.json() } ;

// 經過 intellij 自動展開補充了類型信息的代碼,類似 scala 
future.then( ( r: HttpResponse) :CompletionStage<JSON>  -> { return r.json() } : Function<HttpResponse, CompletionStage<JSON> )
//經過自動補全後,很容易明白這是對http信息進行json格式化,而以上的默認代碼,看的讓人一頭霧水。

///明顯後者閱讀體驗會提高很多.類型信息對書寫者是噪音, 對於閱讀的人, 那是速效救心丸.然而還是要正視, java 的lambda 還是挺殘廢的, 畢竟只是個 sam 的編譯魔法. 簡單用用還行, 等到需要深度對lambda本身進行處理的時候, 比如反射/調試(比如用 arthas 之類的動態調試), 到時候就覺得這垃圾玩意真是個禍害.所以回過頭來看, java 的lambda是什麼 ? 就是一個不用寫文檔(類型信息)的匿名類.

作者:鄭曄
鏈接:https://www.zhihu.com/question/21563900/answer/18631625
來源:知乎

函數式編程是技術的發展方向,而Lambda是函數式編程最基礎的內容,所以,Java 8中加入Lambda表達式本身是符合技術發展方向的。
通過引入Lambda,最直觀的一個改進是,不用再寫大量的匿名內部類。事實上,還有更多由於函數式編程本身特性帶來的提升。比如:代碼的可讀性會更好、高階函數引入了函數組合的概念。
此外,因爲Lambda的引入,集合操作也得到了極大的改善。比如,引入stream API,把map、reduce、filter這樣的基本函數式編程的概念與Java集合結合起來。在大多數情況下,處理集合時,Java程序員可以告別for、while、if這些語句。
隨之而來的是,map、reduce、filter等操作都可以並行化,在一些條件下,可以提升性能。
不過,對大多數Java程序員來說,他們最熟悉的內容是面向對象,函數式編程是個陌生的概念,是一種“全新”的思維模式。對於喜歡墨守陳規的大多數而言,這無疑會增加Java的入門成本,以及向新版本遷移的成本。
還有一件事,Lambda本身是藉助invokedynamic實現的,這是這個Java 7加入的新指令第一次在Java語言層面上得到應用。因爲它的存在,我們在某種程度上可以繞過Java的類型系統,很難說這是好是壞。

https://www.v2ex.com/t/442573

一般編寫業務代碼沒必要用 lambda,做科學計算方面的用函數式編程就很多了。

不少Java程序員都覺得lambda很雞肋,它到底有何用呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章