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

Lambda表達式

在說Lambda表達式以前咱們瞭解一下函數式編程思想,在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是拿什麼東西作什麼事情java

相對而言,面向對象過分強調必須經過對象的形式來作事情,而函數式思想則儘可能忽略面向對象的複雜語法——強調作什麼,而不是以什麼形式作。 下面以匿名內部類建立線程的代碼案例詳細說明這個問題。編程

 

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表達式數組

 

對以上代碼的分析:app

對於 Runnable 的匿名內部類用法,能夠分析出幾點內容:ide

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

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

咱們真的但願建立一個匿名內部類對象嗎?不。咱們只是爲了作這件事情而不得不建立一個對象。咱們真正但願作的事情是:將 run 方法體內的代碼傳遞給 Thread 類知曉。性能

傳遞一段代碼——這纔是咱們真正的目的。而建立對象只是受限於面向對象語法而不得不採起的一種手段方式。學習

那,有沒有更加簡單的辦法?若是咱們將關注點從怎麼作迴歸到作什麼的本質上,就會發現只要可以更好地達到目的,過程與形式其實並不重要。 ui

這時就要用到函數式編程思想了,只關注「作什麼」,而不是以什麼方式作!!

瞭解過函數式編程思想後,咱們要嘗試着轉變思想,從面向對象的"怎麼作"轉換爲函數式編程思想的「作什麼」,只有思想有了轉變,才能更好的瞭解和學習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的轉換,將一個類型的流轉換爲另外一個類型的流。

相關文章
相關標籤/搜索