Java8 lambda表達式詳解

目錄

 

函數式編程的概念

函數式編程的優缺點:

Lambda表達式

Lambda表達式的語法

lambda表達式的函數式接口

Lambda具體應用場景

函數式接口

意義

Java8內置的函數式接口


參考地址:https://blog.csdn.net/lcgoing/article/details/87886599

參考地址:https://www.jianshu.com/p/95b168bea405

函數式編程的概念

函數式編程是種編程方式,它將電腦運算視爲函數的計算。函數編程語言最重要的基礎是λ演算(lambda calculus),而且λ演算的函數可以接受函數當作輸入(參數)和輸出(返回值)。和指令式編程相比,函數式編程強調函數的計算比指令的執行重要。和過程化編程相比,函數式編程裏函數的計算可隨時調用。

函數式編程的優缺點:

優點:

  • 代碼簡潔,開發快速:函數式編程大量使用函數,減少了代碼的重複,因此程序比較短,開發速度較快。
  • 接近自然語言,易於理解:函數式編程的自由度很高,可以寫出很接近自然語言的代碼。將表達式(1 + 2) * 3 - 4,寫成函數式語言:subtract(multiply(add(1,2), 3), 4)
  • 更方便的代碼管理:函數式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果必定相同。因此,每一個函數都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模塊化組合。
  • 易於"併發編程":函數式編程不需要考慮"死鎖"(deadlock),因爲它不修改變量,所以根本不存在"鎖"線程的問題。不必擔心一個線程的數據,被另一個線程修改,所以可以很放心地把工作分攤到多個線程,部署"併發編程"(concurrency)。
  • 代碼的熱升級:函數式編程沒有副作用,只要保證接口不變,內部實現是外部無關的。所以,可以在運行狀態下直接升級代碼,不需要重啓,也不需要停機。

缺點:

  • 函數式編程常被認爲嚴重耗費在CPU和存儲器資源。早期的函數式編程語言實現時並無考慮過效率問題;有些非函數式編程語言爲求提升速度,不提供自動邊界檢查或自動垃圾回收等功能;惰性求值亦爲語言增加了額外的管理工作。
  • 因爲其靈活的語法控制不好程序的結構,帶來了語言學習難度高,代碼維護性差等缺點。

Lambda表達式

Lambda 表達式,也可稱爲閉包,它是推動 Java 8 發佈的最重要新特性。

Lambda 允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中)。

優點:

使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。

缺點:

  • 用Lambda充當匿名內部類、方法引用等場合效率低
  • Lambda的特點還在於開發成本高,並且異常難以排查。它的異常堆棧比匿名內部類還要難懂。如果你把stream的操作都寫在同一行,則問題更甚。
  • 代碼維護性差。

Lambda表達式的語法

Java8中Lambda表達式由三個部分組成:

第一部分爲一個括號內用逗號分隔的形式參數,參數是函數式接口裏面方法的參數 

第二部分爲一個箭頭符號: ->;

第三部分是方法體,可以是表達式和代碼塊。

1.方法體爲表達式時,該表達式的值作爲返回值返回

(parameters) -> expression
 
//求和
(int a,int b) -> return a + b;

2.方法體爲代碼塊,必須用{ }來包裹起來,且需要一個return返回值,但函數式接口裏面方法返回值是 void,無需返回值。

(parameters) -> { statements; }
 
 //求平方
(int a) -> {return a * a;}
 
//打印,無返回值
(int a) -> {System.out.println("a = " + a);}

lambda表達式的函數式接口

  1. 函數式接口(Functional Interface)是Java 8對一類特殊類型的接口的稱呼。這類接口只定義了唯一的抽象方法的接口(除了隱含的Object對象的公共方法,因此最開始也就做SAM類型的接口(Single Abstract Method)。
  2. 定義函數式接口的原因是在Java Lambda的實現中,開發組不想再爲Lambda表達式單獨定義一種特殊的Structural函數類型,稱之爲箭頭類型(arrow type,依然想採用Java既有的類型(class, interface, method等)。
  3. 增加一個結構化的函數類型會增加函數類型的複雜性,破壞既有的Java類型,並對成千上萬的Java類庫造成嚴重的影響。權衡利弊,因此最終還是利用SAM 接口作爲 Lambda表達式的目標類型。
  4. 對於函數式接口來說@FunctionalInterface並不是必須的,只要接口中只定義了唯一的抽象方法的接口那它就是一個實質上的函數式接口,就可以用來實現Lambda表達式。

Java8中已經定義了很多常用的函數式接口,他們都放在java.util.function包下面,一般有以下常用的四大核心接口:

     

函數式接口 參數類型 返回類型 用途
Consumer<T>(消費型接口) T void 對類型爲T的對象應用操作,void accept(T t)
Supplier<T>(供給型接口) T 返回類型爲T的對象 T get();
Function<T, R>(函數型接口) T R 對類型爲T的對象應用操作並返回R類型的對象。R apply(T t);
Predicate<T>(斷言型接口) T boolean 確定類型爲T的對象是否滿足約束,boolean test(T t);

Lambda具體應用場景

當我們新建一個線程的時候,以前我們可以這樣做:

       Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(666);
            }
        });

改爲lambda表達式後

Thread t1 = new Thread(() -> System.out.println("hello world"));

是不是覺異常簡單。

函數式接口

只有函數式接口,纔可以轉換爲lambda表達式,什麼是函數式接口呢?

  • 有且只有一個抽象方法的接口被成爲函數式接口!
  • 函數式接口可以顯式的被@FunctionalInterface所表示,當被標識的接口不滿足規定時,編譯器會提示報錯

我們所熟知的Runnable就是一個函數式接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

1、當函數式接口內的唯一抽象方法沒有參數時,可以使用如下方式

//如果方法體內只有一條語句
//1.無返回值
() -> System.out.println("test");
//2.有返回值,不需要顯示的return
() -> new Student();

//3.如果有多條語句,需要用{}包圍
//無返回值
() -> {
    int a = 1;
    int b = 2;
    System.out.println(a * b);
};
//4.有返回值
() -> {
    int a = 1;
    int b = 2;
    return a * b;
};

2、當函數式接口內的唯一抽象方法含有參數時,寫法與上面基本相同,只是需要把對應參數寫入到()裏面

//不需要指明參數類型,會根據實際方法中的類型自動推測
//只有一個參數可以省略括號
(arg1, arg2, arg3) -> System.out.println(arg1 * arg2 * arg3);

3、舉個例子

//自定義一個函數式接口
@FunctionalInterface
public interface MyInterface {
    void method(int a, int b);
}

 //使用lambda表達式
 public static void main(String[] args) {
    MyInterface myInterface = (a, b) -> System.out.println(a + b);
    myInterface.method(1, 2);//3
}

 

意義

首先毋庸置疑,lambda表達式十分簡潔。其次,在傳統的java面向對象編程中,我們在方法內只可以傳遞值或對象引用,無法傳遞方法。當我們想要在方法中傳遞方法,或者說是傳遞行爲的時候,如Java Swing或者Android開發中,經常需要我們實現監聽接口中的方法,這就是一種行爲的傳遞。

private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
             //代碼省略
        }
 };

這種行爲的傳遞需要依賴接口實現類實例,但是我們只關心方法內部的邏輯實現,所以使用lambda表達式可以實現這一目標

private Handler handler = msg -> {
    //代碼省略
};
//但實際上lambda並沒有脫離接口,仍然依附於接口實現類實例

Java8內置的函數式接口

Java8提供了一個java.util.function包,包含了很多函數式接口,我們來介紹最爲基本的4個(爲了節省篇幅,去掉了源碼中的註釋)

Function接口

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    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));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Function接口的唯一抽象方法是apply,作用是接收一個指定類型的參數,返回一個指定類型的結果

import java.util.function.Function;
public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest ft = new FunctionTest();
        //使用lambda表達式實現apply方法,返回入參+10。形式上如同傳遞了一個方法作爲參數
        int res = ft.compute(1, v -> v + 10);
        System.out.println(res);//11
    }

    public int compute(int a, Function<Integer, Integer> function){
        //使用者在使用本方法時,需要去編寫自己的apply,
        //傳遞的funtion是一個行爲方法,而不是一個值
        return function.apply(a);
    }

默認方法compose作用是傳入參數後,首先執行compose方法內的Function的apply方法,然後將其返回值作爲本Function方法的入參,調用apply後得到最後返回值

import java.util.function.Function;
public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest ft = new FunctionTest();
        //調用compose
        //先+8,然後將得到的值*3
        System.out.println(ft.compute(2, v -> v * 3, v -> v + 8));//30
    }
    public int compute(int a, Function<Integer, Integer> function1, Function<Integer, Integer> function2){
        //將function2先接收入參a,調用apply後,將返回值作爲新的入參,傳入function1,調用apply返回最後結果
        return function1.compose(function2).apply(a);
    }

默認方法andThen與compose正好相反,先執行本Function的apply,然後將結果作爲andThen方法參數內的Function的入參,調用apply後返回最後結果

import java.util.function.Function;
public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest ft = new FunctionTest();
        //調用andThen
        //先*3,然後將得到的值+8
        System.out.println(ft.compute(2, v -> v * 3, v -> v + 8));//14
    }
    public int compute(int a, Function<Integer, Integer> function1, Function<Integer, Integer> function2){
        //將function2先接收入參a,調用apply後,將返回值作爲新的入參,傳入function1,調用apply返回最後結果
        return function1.andThen(function2).apply(a);
    }

靜態方法identity的作用是傳入啥返回啥,這裏就不寫例子了

Consumer接口

package java.util.function;

import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

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

Consumer接口中accept方法的作用是接收指定參數類型,無返回值,重點在於內部消費

Consumer<String> consumer = s -> System.out.println("hello " + s);
consumer.accept("mike");// hello mike

默認方法andThen作用是連續消費,從本Consumer開始,從外到內,針對同一入參。

Consumer<String> consumer = s -> System.out.println("hello " + s);
Consumer<String> consumer2 = s -> System.out.println("nice to meet you " + s);
consumer.andThen(consumer2).accept("mike");
//hello mike
//nice to meet you mike

Predicate接口

package java.util.function;

import java.util.Objects;
@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);
    }
}

Predicate中的test方法,傳入指定類型參數,返回布爾類型的結果,用於判斷,斷言

//判斷一個數是否是偶數
Predicate<Integer> predicate = b -> n % 2 == 0;
System.out.println(predicate.test(3));
//false

默認方法and顧名思義,將本Predicate和and參數中的Predicate對同一入參進行test的結果進行【與】操作。
negate方法對test的結果進行【非】操作
or方法對兩個Predicate的test結果進行【或】操作

靜態方法isEqual將其入參與test方法的入參進行equals比較

System.out.println(Predicate.isEqual(1).test(1));//true

Supplier接口

package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier意爲供應,只有一個方法get,不接收任何參數,只返回指定類型結果

Supplier<String> sup = () -> "hello world";
System.out.println(sup.get());

總結

以上就是function包中最爲基礎的四個函數式接口,不管是接口名稱還是方法名稱,都是見名知意,十分形象。
當然funtion包下還有很多其他接口,但基本都與這四個接口有關。如BiFunction是接收兩個參數,DoubleConsumer是強制接收double類型的參數,更多函數式接口點進去源碼一看註釋便知道如何使用!

 

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