lambda是java8的新特性,基本使用比較容易理解,但有一個環節遇到了坎兒,那就是方法引用,尤其是類的實例方法引用,燒腦之後總結一下。
在需要函數參數的方法中,我們可以把另一個同類型的方法直接傳入,這稱爲方法引用的綁定。類似於C語言中的函數指針。
lambda表達式可以替代方法引用;或者說方法引用是lambda的一種特例,方法引用不可以控制傳遞參數。
4.1) 構造器引用
private Person construntorRef(Supplier<Person> sup){
Person p=sup.get();
return p;
}
@Test
public void testConstructorRef(){
Person p=construntorRef(Person::new);
System.out.println(p);
}
需要有無參的構造器。
4.2) 靜態方法引用
private static void print(String s){
System.out.println(s);
}
@Test
public void testStaticRef(){
Arrays.asList("aa","bb","cc").forEach(TestMethodReference::print);
}
so easy,只要靜態方法的參數列表和FI需要的參數一致就可以。
4.3) 成員方法引用
@Test
public void testMemberMethodRef(){
Arrays.asList("aa","bb","cc").forEach(System.out::println);
}
so easy,只要成員方法的參數列表和FI需要的參數一致就可以。
4.4) 類的任意對象的實例方法引用(很怪異)
@Test
public void testClassMemberMethodRef(){
String[] strs={"zzaa","xxbb","yycc"};
Arrays.sort(strs,String::compareToIgnoreCase);//OK
System.out.println(Arrays.asList(strs));
File[] files = new File("C:").listFiles(File::isHidden); // OK
}
��,前方高能,請關掉耳機的音樂,認真思考,小心行事。
傳統的java開發中,是不允許使用類名去調用成員方法的,這是一個基本原則,那麼這裏的這種寫法就有點不太容易理解了。還是用實例說明:
用到的內部類:
import lombok.Data;
@Data
public static class Person{
private String name;
private Integer age;
public int mycompare(Person p1){
return p1.getAge()-this.getAge();
}
public void print(){
System.out.println(this);
}
public void println(Person p){
System.out.println(p);
}
public int compareByAge(Person a,Person b){
return a.getAge().compareTo(b.getAge());
}
}
public static class APerson{
public void print(){
System.out.println(this);
}
public void println(Person p){
System.out.println(p);
}
}
測試代碼:
@Test
public void testClassMemberMethodRef2() {
// R apply(T t);//要求一個參數
Function<String, String> upperfier1 = String::toUpperCase;
UnaryOperator<String> upperfier2 = (x) -> x.toUpperCase();//這裏沒有參數,即0個
/*
* 小結:如果方法引用表達式 "String::toUpperCase" 可以用lambda表達式中參數的指定成員方法(這個成員方法的參數比FI要求的參數少一個改類型的參數)改寫,
* 那麼就可以使用 "類的實例方法"來表示方法引用。
*
* 或者說:如果lambda表達式的lambda體中使用的方法是參數匹配的方法,那麼方法引用表達式就用"類引用對象的實例方法"。
*
* lambda的參數是方法的主體。
*/
class Print {
public void println(String s) {
System.out.println(s);
}
}
// void accept(T t);
Consumer<String> sysout1 = new Print()::println;
Consumer<String> sysout2 = (x) -> new Print().println(x);
/*
* 小結:如果方法引用表達式 "new Print()::println" 可以用lambda表達式中參數的具體對象的參數匹配的成員方法改寫,
* 那麼就用 "對象的實例方法"來表示方法引用。
*
* 或者說:如果lambda表達式的lambda體中使用的方法來操作lambda的參數,那麼方法引用表達式就用"對象的實例方法"。
*
* lambda的參數是方法的參數。
*/
//有一個更讓人易混淆的例子,可以用上面的規則來驗證,Arrays.sort(T t,Comparator<? extends t> c)
class Person {
public int com1(Person p) {
return 1;
}
public int com2(Person p1, Person p2) {
return 1;
}
}
// int compare(T o1, T o2);//需要兩個參數
Person【】 ps = { new Person(), new Person() };
Arrays.sort(ps, Person::com1);
Arrays.sort(ps, (x,y)->x.com1(y));
Arrays.sort(ps, new Person()::com2);
Arrays.sort(ps, (x,y)->new Person().com2(x, y));
//按照以上規則驗證應該能說明清楚。
/*
* 但是一個接口爲什麼有兩種寫法?缺省的lambda會匹配FI方法,即"int compare(T o1, T o2);"
* 從上面的lambda表達式來分析,默認的使用lambda應該是:
*/
Comparator<Person> comparator1 = new Person()::com2;
/*
* 下面的方式又是怎麼回事呢?
*/
Comparator<Person> comparator2 = Person::com1;
System.out.println(comparator2);
/*
* 任一個兩個參數的FI接口方法(int compare(T o1, T o2)),都可以用引用減少一個參數的方法(int o1<T>.compare(T o2))來代替,而引用對象本身作爲另一個隱含參數,那麼方法引用的對象用類名,表示類的任意對象。
還是有點亂?我們來換一個角度來看一下:
首先,我們需要的是int compare(T o1, T o2)是兩個參數;
其次,先不考慮::前綴是類還是對象,你給了我一個compare(T o2),少一個參數?怎麼辦?
lambda機制爲了解決這個問題,它使用::前面的類名new一個對象,當做需要的缺少的那個參數,這就是類的實例方法。
*/
}
小結一下:
首先明確此處需要的方法參數列表,此處標記參數個數爲N,那麼:
1. 如果傳入的方法是一個類型的靜態方法,而且參數匹配,使用“類的靜態方法引用”;這應該不難理解。
2. 如果傳入的方法是一個實例的成員方法,而且參數匹配,使用“實例的成員方法”;這也應該不難理解。
3. 如果傳入的方法是一個類型T的實例的成員方法,而且參數爲N-1個,缺少了一個T類型的參數,那麼就使用“T類型的實例方法”。
燒腦分析類的實例方法省略了哪個參數
前面的例子,FI的兩個參數是同一個類型,如果類型不同呢?省略了哪個參數呢?
是按照位置省略了第一個,亦或者是省略了最後一個?
還是按照類型自動去對應,而不關心第幾個呢?
這個時候,我們能想到的辦法可能是去看源碼,但是一般看代碼沒有個把禮拜甚至更長,毛都看不出來。我們還是用一個例子來分析一下吧。
定義一個FI接口:
package com.pollyduan.fi;
public interface TestInterface {
//隨便什麼名字,lambda並不關心,因爲FI只有一個接口,並且根據參數來匹配
public void anyStringAsName(TestBean1 bean1,TestBean2 bean2);
}
編寫兩個用於參數的類:
TestBean1.java
package com.pollyduan.fi;
public class TestBean1 {
public void expect1(TestBean1 bean1){
}
public void expect2(TestBean2 bean2){
}
public void test1(TestInterface i){
}
}
TestBean2.java
package com.pollyduan.fi;
public class TestBean2 {
public void expect1(TestBean1 bean1){
}
public void expect2(TestBean2 bean2){
}
public void test1(TestInterface i){
}
}
二者區別不大。
編寫測試類:
package com.pollyduan.fi;
public class TestFIMain {
public static void main(String[] args) {
TestBean1 bean1=new TestBean1();
bean1.test1(TestBean1::expect1);//①
bean1.test1(TestBean1::expect2);//② ok
bean1.test1(TestBean2::expect1);//③
bean1.test1(TestBean2::expect2);//④
TestBean2 bean2=new TestBean2();
bean2.test1(TestBean1::expect1);//⑤
bean2.test1(TestBean1::expect2);//⑥ ok
bean2.test1(TestBean2::expect1);//⑦
bean2.test1(TestBean2::expect2);//⑧
}
}
測試方法中,除了標記OK的行正確,其他都報錯。
分析:
首先我們要明確FI需要的參數列表是:(TestBean1,TestBean2)
我們先看①行,我們傳入的”::”前導的類是TestBean1,而expect1方法匹配的是TestBean1類型的入參bean1,也就是說省略了TestBean2類型的參數bean2,FI中的最後一個參數。即便我們使用類TestBean1去new一個對象,也找不到TestBean2,因此這個錯誤。
我們先看②行,我們傳入的”::”前導的類是TestBean1,而expect2方法匹配的是TestBean2類型的入參bean2,也就是說省略了TestBean1類型的參數bean1,那麼lambda就可以使用”::”前導的TestBean1構建一個對象,作爲第一個參數,從而匹配FI的接口方法。ok。
我們先看③行,我們傳入的”::”前導的類是TestBean2,而expect1方法匹配的是TestBean1類型的入參bean1,也就是說省略了TestBean2類型的參數bean2,FI的最後一個參數。按照第二步的分析,我們用”::”前導的類TestBean2去new一個對象,應該可以湊足兩個參數。實際測試會發現這不靈。這就證明了只能省略第一個參數,而且,用”::”前導的類也必須是第一個參數的類型。
同第一步類似,第④行代碼,找不到TestBean1的參數,有錯誤可以理解。
至於⑤~⑧,只是替換了外層的test1的主體,沒有任何區別。這證明了,lambda的匹配與外層是什麼鬼沒有任何關係,它只關心外層需要的FI的參數列表。
請不要看下一步,在這裏停下來冷靜的思考一下,如果我們把TestInterface中FI方法的參數位置換一下,即
public void anyStringAsName(TestBean2 cat,TestBean1 dog);
,結果應該是哪兩行正確呢?認真思考一下,實在想不明白跑一下測試用例,也許對理解更有幫助。如果想明白了用這個思路驗證一下:參照參數列表
(TestBean2,TestBean1)
,可以確定只可以省略第一個參數即TestBean2,那麼”::”簽到必須是TestBean2,用於自動創建對象;而未省略的參數是TestBean1,那麼方法名爲expect1,結果爲xxx(TestBean2::expect1)
,即③和⑦,你答對了嗎?