函數式接口 & 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);
    }

通過類名去找到對象的實例方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM