Java8新特性_lambda表達式和函數式接口最詳細的介紹


Lambda表達式

在說Lambda表達式之前我們了解一下函數式編程思想,在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是拿什么東西做什么事情

相對而言,面向對象過分強調必須通過對象的形式來做事情,而函數式思想則盡量忽略面向對象的復雜語法——強調做什么,而不是以什么形式做。 下面以匿名內部類創建線程的代碼案例詳細說明這個問題。

 

public class ThreadDemo {
public static void main(String[] args) {
//實現Runnable方式創建簡單線程--傳統匿名內部類形式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("開啟了一個線程----匿名內部類");
}
}).start();

//實現Runnable方式創建簡單線程--Lambda表達式形式
new Thread(()-> System.out.println("開啟了一個線程---Lambda表達式")).start();
}
}
運行結果:

開啟了一個線程----匿名內部類
開啟了一個線程---Lambda表達式

 

對以上代碼的分析:

對於 Runnable 的匿名內部類用法,可以分析出幾點內容:

Thread 類需要 Runnable 接口作為參數,其中的抽象 run 方法是用來指定線程任務內容的核心;
為了指定 run 的方法體,不得不需要 Runnable 接口的實現類;
為了省去定義一個 RunnableImpl 實現類的麻煩,不得不使用匿名內部類;
必須覆蓋重寫抽象 run 方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
而實際上,似乎只有方法體才是關鍵所在

傳統的寫法比Lambda表達式寫法顯而易見代碼繁瑣了許多,而且2者實現目的是相同的。

我們真的希望創建一個匿名內部類對象嗎?不。我們只是為了做這件事情而不得不創建一個對象。我們真正希望做的事情是:將 run 方法體內的代碼傳遞給 Thread 類知曉。

傳遞一段代碼——這才是我們真正的目的。而創建對象只是受限於面向對象語法而不得不采取的一種手段方式。

那,有沒有更加簡單的辦法?如果我們將關注點從怎么做回歸到做什么的本質上,就會發現只要能夠更好地達到目的,過程與形式其實並不重要。 

這時就要用到函數式編程思想了,只關注“做什么”,而不是以什么方式做!!

了解過函數式編程思想后,我們要嘗試着轉變思想,從面向對象的"怎么做"轉換為函數式編程思想的“做什么”,只有思想有了轉變,才能更好的了解和學習Lambda表達式。

什么是Lambda表達式?

Lambda 是一個匿名函數,我們可以把 Lambda表達式理解為是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞)。可以寫出更簡潔、更靈活的代碼。

作為一種更緊湊的代碼風格,使Java的語言表達能力得到了提升 。20143Oracle所發布的Java 8JDK 1.8)中,加入了Lambda表達式 

Lambda表達式語法:( ) ->  { }

Lambda 表達式在Java 語言中引入了一個新的語法元素和操作符。這個操作符為 “->” , 該操作符被稱為 Lambda 操作符或剪頭操作符。它將 Lambda 分為

兩個部分:

左側 (): 指定了 Lambda 表達式需要的所有參數

右側  {}: 指定了 Lambda 體,即 Lambda 表達式要執行的功能。 

Lambda表達式標准格式:(參數類型 參數名稱) ‐> { 代碼語句 }

格式進一步說明:

小括號內的語法與傳統方法參數列表一致:無參數則留空;多個參數則用逗號分隔。
-> 是新引入的語法格式,代表指向動作。
大括號內的語法與傳統方法體要求基本一致

Lambda表達式如何使用呢?

Lambda表達式的使用是有前提的,必須要滿足2個條件:1.函數式接口      2.可推導可省略。

函數式接口是指一個接口中只有一個必須被實現的方法。這樣的接口都滿足一個注解@FunctionalInterface

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

可推導可省略是指上下文推斷,也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的實例 。

下面我們自定義一個函數式接口,使用Lambda表達式完成功能。

public class Demo {
    public static void main(String[] args) {
        invokeCook(()->{
            System.out.println("做了一盤紅燒魚....");
        });

    }
    //需要有個以函數式接口為參數的方法
    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
//自定義函數式接口
@FunctionalInterface
interface Cook{
    void makeFood();
}

以上案例是函數式接口以及Lambda表達式最簡單的定義和用法。

針對Lambda表達式還可以做出進一步的省略寫法:

1.小括號內參數的類型可以省略;
2. 如果小括號內有且僅有一個參,則小括號可以省略;
3. 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。 

所以上面的代碼可以簡寫為:

invokeCook(()-> System.out.println("做了一盤紅燒魚...."));

 

Lambda表達式有多種語法,下面我們了解一下。(直接寫省略形式)

1.無參,無返回值,Lambda體只需一條語句

Runnable r  = ()->System.out.println("hell lambda");

2.Lambda表達式需要一個參數,無返回值

Consumer c = (str)-> System.out.println(args);

當Lambda表達式只有一個參數時,參數的小括號可以省略

 Consumer c = str-> System.out.println(args);

3.Lambda表達式需要2個參數,並且有返回值

BinaryOperator<Long> bo = (num1,num2)->{ return num1+num2;};

當Lambda體中只有一條語句時,return 和 大括號、分號可以同時省略。

BinaryOperator<Long> bo = (num1,num2)-> num1+num2;

有沒有發現我們沒寫參數類型,Lambda表達式依然可以正確編譯和運行,這是因為Lambda表達式擁有的類型推斷功能。

上述 Lambda 表達式中的參數類型都是由編譯器推斷得出的。 Lambda 表達式中無需指定類型,程序依然可以編譯,這是因為 javac 根據程序的上下文,

在后台推斷出了參數的類型。 Lambda 表達式的類型依賴於上下文環境,是由編譯器推斷出來的。這就是所謂的“類型推斷” 。

 

Lambda表達式還具有延遲執行的作用:改善了性能浪費的問題,代碼說明。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表達式";
        log(1,str1+str2+str3);
    }
    public static void log(int level,String str) {
        if (level == 1) {
            System.out.println(str);
        }
    }
}

在上面代碼中,存在的性能浪費問題是如果 輸入的level!=1,而str1+str2+str3作為log方法的第二個參數還是參與了拼接運算,但是我們的實際想法應該是不滿足level=1的條件就不希望str1+str2+str3進行拼接運算,下面通過Lambda表達式來實現這個功能。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表達式";
        log(1,()->str1+str2+str3);
    }
    public static void log(int level,Message message) {
        if (level == 1) {
            System.out.println(message.message());
        }
    }
}
@FunctionalInterface
interface Message {
    String message();
}

以上代碼功能相同,Lambda表達式卻實現了延遲,解決了性能浪費,下面我們來驗證一下:

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表達式";
        log(2,()->{
            System.out.println("lambda 執行了");
            return str1+str2+str3;
        });
    }
    public static void log(int level,Message message) {
        if (level == 1) {
            System.out.println(message.message());
        }
    }
}
@FunctionalInterface
interface Message {
    String message();
}

此時在輸入level=2的條件時,如果Lambda不延遲加載的話會執行輸出語句輸出lambda 執行了,而實際是控制台什么也沒輸出,由此驗證了Lambda表達式的延遲執行。

在Lambda表達式的應用過程中還有一種比較常用的方式:方法引用。方法引用比較難以理解,而且種類也較多,需要多費腦筋去理解。

Lambda表達式應用之 :方法引用

方法引用也是有前提的,分別為:

1.前后的參數名一致,

2.Lambda表達式的方法體跟對應的方法的功能代碼要一模一樣



方法引用種類可以簡單的分為4+2種,4種跟對象和類有關,2種跟構造方法有關。下面一一說明。

跟對象和類有關的方法引用:

1.對象引用成員方法

  格式:對象名 :: 成員方法名      (雙冒號 :: 為引用運算符,而它所在的表達式被稱為方法引用

  原理:將對象的成員方法的參數和方法體,自動生成一個Lambda表達式。

 1 public class Demo {
 2     public static void main(String[] args) {
 3         Assistant assistant = new Assistant();
 4         work(assistant::dealFile);//對象引用成員方法(注意是成員的方法名,沒有小括號)
 5     }
 6     //以函數式接口為參數的方法
 7     public static void work(WokerHelper wokerHelper) {
 8         wokerHelper.help("機密文件");
 9     }
10 }
11 //助理類,有個成員方法
12 class Assistant{
13     public void dealFile(String file) {
14         System.out.println("幫忙處理文件:"+file);
15     }
16 }
17 //函數式接口,有個需要實現的抽象方法
18 @FunctionalInterface
19 interface WokerHelper {
20     void help(String file);
21 }

 

2.類調用靜態方法

  格式:類名 :: 靜態方法名

  原理:將類的靜態方法的參數和方法體,自動生成一個Lambda表達式。

public class Demo {
    public static void main(String[] args) {
        methodCheck((str)->StringUtils.isBlank(str),"   ");//非省略模式
        methodCheck(StringUtils::isBlank,"  ");//省略模式  類名調用靜態方法
    }
    //
    public static void methodCheck(StringChecker stringChecker,String str) {
        System.out.println(stringChecker.checkString(str));
    }
}
//定義一個類包含靜態方法isBlank方法
class StringUtils{
    public static boolean isBlank(String str) {
        return str==null || "".equals(str.trim());//空格也算空
    }
}
//函數式接口,有個需要實現的抽象方法
@FunctionalInterface
interface StringChecker {
    boolean checkString(String str);
}

 

3.this引用本類方法

  格式:this :: 本類方法名

  原理:將本類方法的參數和方法體,自動生成一個Lambda表達式。

public class Demo {
    public static void main(String[] args) {
        new Husband().beHappy();
    }
}
class Husband{
    public void buyHouse() {
        System.out.println("買套房子");
    }

    public void marry(Richable richable) {
        richable.buy();
    }
    public void beHappy() {
        marry(this::buyHouse);//調用本類中方法
    }
}
//函數式接口,有個需要實現的抽象方法
@FunctionalInterface
interface Richable {
    void buy();
}

 

4.super引用父類方法

  格式:super :: 父類方法名

  原理:將父類方法的參數和方法體,自動生成一個Lambda表達式。

public class Demo {
    public static void main(String[] args) {
       new Man().sayHello();
    }
}
//子類
class Man extends Human{
    public void method(Greetable greetable) {
        greetable.greet();
    }
    @Override
    public void sayHello() {
        method(super::sayHello);
    }
}
//父類
class Human{
  public void sayHello() {
      System.out.println("Hello");
  }
}
//函數式接口,有個需要實現的抽象方法
@FunctionalInterface
interface Greetable {
    void greet();
}

 

跟構造方法有關的方法引用:

5.類的構造器引用

  格式:  類名  :: new

  原理:將類的構造方法的參數和方法體自動生成Lambda表達式。

public class Demo {
    public static void main(String[] args) {
        printName("張三",(name)->new Person(name));
        printName("張三",Person::new);//省略形式,類名::new引用
    }

    public static void printName(String name, BuildPerson build) {
        System.out.println(build.personBuild(name).getName());
    }
}

//函數式接口,有個需要實現的抽象方法
@FunctionalInterface
interface BuildPerson {
    Person personBuild(String name);
}
//實體類
class Person{
    String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

 

6.數組的構造器引用

  格式: 數組類型[] :: new

  原理:將數組的構造方法的參數和方法體自動生成Lambda表達式。

public class Demo {
    public static void main(String[] args) {
        int[] array1 = method(10, (length) -> new int[length]);
        int[] array2 = method(10, int[]::new);//數組構造器引用
    }

    public static int[] method(int length, ArrayBuilder builder) {
       return builder.buildArray(length);
    }
}

//函數式接口,有個需要實現的抽象方法
@FunctionalInterface
interface ArrayBuilder {
    int[] buildArray(int length);
}

 

到此,Lambda表達式的基本知識就算學完了。

有人可能會提出疑問,Lambda表達式使用前要定義一個函數式接口,並在接口中有抽象方法,還要創建一個以函數式接口為參數的方法,之后調用該方法才能使用Lambda表達式,感覺並沒有省很多代碼!!哈哈,之所以有這樣的想法,那是因為是我們自定義的函數式接口,而JDK1.8及更高的版本都給我們定義函數式接口供我們直接使用,就沒有這么繁瑣了。接下來我們學習一下JDK為我們提供的常用函數式接口。

常用的函數式接口

1.Supplier<T> 供給型接口

  

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
用來獲取一個泛型參數指定類型的對象數據。由於這是一個函數式接口,這也就意味着對應的Lambda表達式需要對外提供一個符合泛型類型的對象數據。 

 如果要定義一個無參的有Object返回值的抽象方法的接口時,可以直接使用Supplier<T>,不用自己定義接口了。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "lambda";
        String s = method(() -> str1 + str2);
        System.out.println("s = " + s);
    }

    public static String method(Supplier<String> supplier) {
       return supplier.get();
    }
}

2.Consumer<T> 消費型接口

@FunctionalInterface
public interface Consumer<T> {

void accept(T t);
  
  //合並2個消費者生成一個新的消費者,先執行第一個消費者的accept方法,再執行第二個消費者的accept方法   default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
Consumer<T> 接口則正好相反,它不是生產一個數據,而是消費一個數據,其數據類型由泛型參數決定 。
如果要定義一個有參的無返回值的抽象方法的接口時,可以直接使用Consumer<T>,不用自己定義接口了。
public class Demo {
    public static void main(String[] args) {
       consumerString(string -> System.out.println(string));
       consumerString(System.out::println);//方法引用形式
    }

    public static void consumerString(Consumer<String> consumer) {
       consumer.accept("fall in love!");
    }
}

3.Predicate<T> 斷定型接口

@FunctionalInterface
public interface Predicate<T> {

 //用來判斷傳入的T類型的參數是否滿足篩選條件,滿足>true
boolean test(T t);

//合並2個predicate成為一個新的predicate---->並且&&
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

 //對調用的predicate原來的結果進行取反---->取反 !
default Predicate<T> negate() {
return (t) -> !test(t);
}

//合並2個predicate成為一個新的predicate---->或||
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

}

 

  Predicate<T>接口主要是對某種類型的數據進行判斷,返回一個boolean型結果。可以理解成用來對數據進行篩選

  當需要定義一個有參並且返回值是boolean型的方法時,可以直接使用Predicate接口中的抽象方法

  

 1 //1.必須為女生;
 2 //2. 姓名為4個字。
 3 public class Demo {
 4     public static void main(String[] args) {
 5         String[] array = { "迪麗熱巴,女", "古力娜扎,女", "馬爾扎哈,男", "趙麗穎,女" };
 6         List<String> list = filter(array,
 7                 str-> "女".equals(str.split(",")[1]),
 8                 str->str.split(",")[0].length()==3);
 9         System.out.println(list);
10     }
11     private static List<String> filter(String[] array, Predicate<String> one, Predicate<String> two) {
12         List<String> list = new ArrayList<>();
13         for (String info : array) {
14             if (one.and(two).test(info)) {
15                 list.add(info);
16             }
17         }
18         return list;
19     }
20 }

 

4.Function<T,R> 函數型接口  

@FunctionalInterface
public interface Function<T, R> {

   //表示數據轉換的實現。T--->R
    R apply(T t);

   //合並2個function,生成一個新的function,調用apply方法的時候,先執行before,再執行this
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
  //合並2個function,生成一個新的function,調用apply方法的時候,先執行this,再執行after
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
}

 

  Function<T,R> 接口用來根據一個類型的數據得到另一個類型的數據,前者稱為前置條件,后者稱為后置條件。有進有出,所以稱為函數Function”

   該接口可以理解成一個數據工廠,用來進行數據轉換,將一種數據類型的數據轉換成另一種數據.   泛型參數T:要被轉換的數據類型(原料),泛型參數R:想要裝換成的數據類型(產品)。

public class Demo {
    public static void main(String[] args) {
        String str = "趙麗穎,20";
        int age = getAgeNum(str,
                string ->string.split(",")[1],
                Integer::parseInt,//str->Integer.parseInt(str);
                n->n+=100);
        System.out.println(age);
    }
    //實現三個數據轉換 String->String, String->Integer,Integer->Integer
    private static int getAgeNum(String str, Function<String, String> one,
                                Function<String, Integer> two,
                                Function<Integer, Integer> three) {
        return one.andThen(two).andThen(three).apply(str);
    }
}

至此,常用的四個函數式接口已學習完畢。

總結一下函數式表達式的延遲方法與終結方法:

延遲方法:默認方法都是延遲的。

終結方法:抽象方法都是終結的。

接口名稱 方法名稱 抽象方法/默認方法 延遲/終結
Supplier get 抽象 終結
Consumer accept 抽象   終結
  andThen 默認 延遲
Predicate test 抽象 終結
  and 默認 延遲
  or 默認 延遲
  negate 默認 延遲
Function apply 抽象 終結
  andThen   默認   延遲

 

函數式接口在Stream流中的應用較為廣泛,其中Stream流中的過濾Filter方法使用到了Predicate的判定,map方法使用到了Function的轉換,將一個類型的流轉換為另一個類型的流。

 


免責聲明!

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



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