方法參數:當要傳遞的參數爲一段代碼塊時,該如何傳遞
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個部分:
- 一個代碼塊
- 參數
- 自由變量的值
關於lambda表達式中自由變量的語法規範
- lambda表達式必須存儲自由變量的值,在上例中就是"hello",我們說它被lambda變量捕獲(captured)。
關於代碼塊及自由變量值有一個術語叫做閉包(closure),lambda表達式就是一個閉包。
- 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的類型系統,很難說這是好是壞。
一般編寫業務代碼沒必要用 lambda,做科學計算方面的用函數式編程就很多了。