天天看點

函數式接口 & lambda表達式 & 方法引用

拉呱: 終于,學習jdk8的新特性了,初體驗帶給我的感覺真爽,代碼精簡的不行,可讀性也很好,而且,spring5也是把jdk8的融入到血液裡,總之一句話吧,說的打趣一點,學的時候自己難受,學完了寫出來的代碼,别人看着難受

開篇說一個問題,jdk8是如何把這些新的特性添加進來,并且相容jdk7及以前版本的?

大家都知道,java的體系的建立,和interface有着莫大的關系,先有接口确定出一套明确的體系,再有它的實作類實作這套體系,比如,超級典型的java裡面的集合體系;

新的需求來了,總不能去原有的接口裡面添加抽象方法吧? 那不是開玩笑? 接口一改,所有的實作類,全部不能用了! java8是怎麼做的呢? 允許接口中添加 default方法, 允許方法存在方法體

  • java的拓展接口的方法是default方法 + 函數式接口(更進一步說,是default方法的入參大多是該函數式接口的引用,函數體都是基于抽象方法的一套邏輯組合)
仔細想想,還真的是很精妙的,default方法雖然有方法體,但是它們的動作其實是動态傳遞進去的!!!

可以看一下下面的代碼

list.forEach(new Consumer<String>() {
    @Override
    public void accept(String integer) {
        System.out.println(integer);
    }
});

/**
 *  用lambda表達式的方法實作,得到Consumer的實作
 *  Consumer唯一未實作的抽象方法就是accept  -- 接受一個參數,不傳回任何值
 *  下面的i為什麼不寫類型? 可以看看上面匿名内部類的實作方式, 編譯器通過類型推斷可以推斷出 i 就是integer類型的
 */
list.forEach(i->System.out.println(i));
Consumer c1 = i->{};

/**
 *  通過方法引用建立 函數式接口的執行個體
 *  滑鼠放到 :: 上,點進去, 編譯器跳轉到了 Consumer函數式接口 , 同樣是通過類型推斷,内部疊代出每個元素
 */
list.forEach(System.out::println);
Consumer c  = System.out::println;      
  • java的拓展類的方法是類的話,直接添加新的方法就行
但是,相當一部分新增的方法,入參類型,依然是函數式接口

什麼是函數式接口呢?

位于 java.util.function包

函數式接口本質上就是個接口,性質如下:

  1. 如果一個接口隻有一個抽象方法,無論有沒有FunctionInterface注解,這個接口都是一個函數式接口
  2. 如果我們在接口上加上了FunctionInterface注解,那麼編譯器按照函數式接口的要求,處理我們的接口

進一步,對于函數式接口,如何實作它呢?

  1. lambda表達式
  2. 方法引用
  3. 構造方法引用實作對應的執行個體
  4. 寫個類,實作它, 不過沒人這麼做,傻裡吧唧的

再進一步lambda表達式是什麼?

  • lambda表達式其實是對象,但是這種對象必須依附于函數式接口
  • but,即便我們知道lambda是對象,它到底是什麼類型的對象? 隻能通過給定的特定的上下文得知
Consumer consumer = i->{};      

有啥用?

  • 它解決了,在java中我們無法将函數作為參數傳遞給一個方法,也不能聲明一個傳回函數的方法這樣一個問題
  • 像js這種函數程式設計語言,它當然可以做到,ajax向後端發送請求,得到的傳回結果就是一個 回調函數 callback(){}

常見的函數式接口:

jdk8新添加的函數式接口有幾十個,但是套路相似,通過下面集合常見的函數式接口,可以搞清楚它的來龍去脈

Consumer

// 接收一個參數,無傳回值
void accept(T t);      

Function

@FunctionalInterface
public interface Function<T, R> {

/**
 * 接受一個參數,傳回一個值
 * @param t the function argument
 * @return the function result
 */
R apply(T t);      

觀看下面四行代碼

/*
 *  下面分别用 方法引用 和 lmabda表達式 實作函數式接口Function
 *   Function的函數式方法是apply 接受一個參數,并傳回傳回值, 這兩部分的泛型是Function傳遞給它的
 *   右半部分,不管使用什麼方法,必須滿足兩件事,,編譯器才會任務他是對函數式方法apply的實作
 *      1. 方法傳回值必須是String(第二個泛型)
 *      2. 第一個String是使用方法的對象的類型,也必須是string

對比這兩行代碼,你就可以看到,動作是動态傳遞進去的!~~而不是使用預先定義的行為
 * */
Function<String,String> function2 = String::toLowerCase;
Function<String,String> function3 = i->i.toUpperCase();

// 錯誤執行個體
// 無傳回值
Function<String,String> function4 = i-> System.out.println(i);

// 傳回值是布爾類型
Function<String,String> function1 = String::contains;      

Function的其他兩個方法(詳細記錄第一個方法的使用)

  • 首先,compose()接受一個Function函數接口 before
  • 具體執行的過程是 this.apply(before.apply(v)), 也就說,先執行傳遞進來的這個函數式接口執行個體的apply方法
  • before執行apply(v),他的入參是V ==> ? super V ; 由他可知,這兩個apply處理的都是V類型的資料
  • 它的傳回值是 ? extends T , T就是這個Function唯一接受的參數的類型
  • 傳回去看apply方法 : R apply(T t); 正好before執行完事把T類型的結果扔給this.apply(),再一步執行apply()
  • 最後看完整的看一下 return的形式: return (V v) -> apply(before.apply(v));畫重點!!!,return 的這個結果,從形式上看,首先它是個lambda表達式,還可以把它了解成Function唯一的函數式接口的實作(隻不過他們接受的參數比較特别),這裡也可以直接把它了解成是一個Function類型的執行個體,或者是一套模闆,apply()的嵌套;但是别忘了,apply嵌套的再多,最終也是要處理V, V具體是幾? 我們要動态的傳遞給它,怎麼傳給他? 用compose()方法的傳回值調用apply()方法;
其實上面的流程是java8已經搭好的架子, 我們要做的其實是apply的具體實作,apply是函數式接口,如何實作? lambda表達式,方法引用,構造方法引用随便挑
/**
 * @param t the function argument
 * @return the function result
 */
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));
}      

使用的demo

public int compute(int a, Function<Integer,Integer> function1,Function<Integer,Integer> function2){
         Function<Integer,Integer> fun = function1.compose(function2);
         fun.apply(a);
        return function1.compose(function2).apply(a);
}


public static void main(String[] args) {
    FunctionText functionText = new FunctionText();

     System.out.println(functionText.compute(2,i->i*4,j->j*3));
}      

結果是兩個24;沒差

Bifurcation

  • 和Function相似,隻不過,它唯一的函數式接口可以接受兩個參數,傳回一個值
  • 它隻有andThen()這麼一個預設方法,andThen()和compose正好相反,它先執行this.apply,得到一個傳回值,傳遞非入參位置上的Function的apply() -- 因為它剛好接收一個參數,傳回一個結果, 具體對誰進行apply?和我上面的分析雷同
@FunctionalInterface
public interface BiFunction<T, U, R> {

/**
 *接受兩個參數,傳回一個值
 *
 * @param t the first function argument
 * @param u the second function argument
 * @return the function result
 */
R apply(T t, U u);

/**

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

使用Bifurcation的demo

public int add(Integer a, Integer b, BiFunction<Integer,Integer,Integer> biFunction){
    return biFunction.apply(a,b);
}
public int compute3(Integer a, Integer b,
                    BiFunction<Integer,Integer,Integer> biFunction,
                    Function<Integer,Integer> function){
   return biFunction.andThen(function).apply(a,b);
}

System.out.println(functionText.add(1,2,( a , b ) -> a + b));
System.out.println(functionText.compute3(3,4,(c,d)->c*d,i->i+1));      

Predicate

用于動态的判斷傳遞給他的類型是否相等

學習過上面那幾個函數式接口,再看它,應該是很容易蒙出怎麼玩了
  • 套路: 到現在看,他和上面幾個函數式接口的套路還是大同小異的, java8針對不同的使用情景設計出不同的函數式接口,Predicate意味,斷定,判相等
  • 下面的三個預設方法,入參全部是Predicate類型的形參,目的是和目前對象的text()結合形成多重判斷
  • 最後一個static靜态方法,使用的是靜态方法的引用
  • 關于我們: 我們能做的依舊是寫出lambda表達式,作為text的真正的業務邏輯
@FunctionalInterface
public interface Predicate<T> {

boolean test(T t);


default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}

default Predicate<T> negate() {
    return (t) -> !test(t);
}


default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}


static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
}
}      
不接受參數,但是傳回一個值

方法引用

方法引用其實是lambda的文法糖,當我們的lambda表達式隻有一行并且恰好有一已經存在的方法作用跟他相同,我們就可以用方法引用替換lambda表達式,讓代碼的風格更好看

四類方法引用

一方面編譯器會提示如何使用方法引用,另一方面,我們自己要根據方法的類型知道如何引用
  • 類名::靜态方法名
  • 引用名(對象名)::執行個體方法名
  • 類名::執行個體方法名
lambda表示的第一個參數是作為方法的調用者傳遞進去的
  • 類名::new --- 構造方法引用
編譯可以很智能的推斷出,你在使用哪個構造方法
public class text1 {

public String getString1(String str, Function<String,String> function){
    return function.apply(str);
}
public String getString2(String str, Supplier<String> function){
    return function.get();
}

public static void main(String[] args) {

    text1 text1 = new text1();
    String haha = text1.getString1("haha", String::new);
    System.out.println(haha);

    String hehe = text1.getString2("hehe", String::new);
    System.out.println(hehe);
    }