JAVA8 Lambda表達式完全解析

JAVA8 新特性

在學習JAVA8 Lambda之前,必須先了解一下JAVA8中與Lambda相關的新特性,不然對於一些概念會感到比較陌生。

1、 接口的默認方法和靜態方法
Java 8允許我們給接口添加一個默認方法,用default修飾即可。默認方法可以重寫。

public interface IMyInterface {
    void onMethond(String str);//這是一個抽象方法
    default String onDefalutMethod(){//這是一個默認方法 
        return "這是一個默認方法";
    }
}
//重寫默認方法
public class MyImpl1 implements IMyInterface {

    @Override
    public void onMethond(String str) {
        // TODO Auto-generated method stub

    }
    @Override
    public String onDefalutMethod() {
        return "重寫默認方法";
    }

}
//不重寫默認方法
public class MyImpl2 implements IMyInterface {

    @Override
    public void onMethond(String str) {
        // TODO Auto-generated method stub
    }
}


此外Java 8還允許我們給接口添加一個靜態方法,用static修飾即可。

public interface IMyInterface {

    void onMethond(String str);//這是一個抽象方法

    default String onDefalutMethod(){//這是一個默認方法 
        return "這是一個默認方法";
    }

    static String onStaticMethod(){
        return "這是一個靜態方法";
    }

}

2、 函數式接口(Functional Interface)
什麼叫函數式接口?他和普通接口有什麼區別?
“函數式接口”是指僅僅只包含一個抽象方法的接口(可以包含默認方法和靜態方法),其他特徵和普通接口沒有任何區別,Java中Runnalbe,Callable等就是個函數式接口;。我們可以給一個符合函數式接口添加@FunctionalInterface註解,這樣就顯式的指明該接口是一個函數式接口,如果不是,編譯器會直接提示錯誤。當然你也可以不用添加此註解。添加的好處在於,由於Lambda表達式只支持函數式接口,如果恰好這個接口被應用於Lambda表達式,某天你手抖不小心添加了個抽象方法,編譯器會提示錯誤。

//顯式指明該接口是函數式接口
@FunctionalInterface
public interface IMyInterface {

    void onMethond(String str);//這是一個抽象方法

    default String onDefalutMethod(){//這是一個默認方法 
        return "這是一個默認方法";
    }

    static String onStaticMethod(){
        return "這是一個靜態方法";
    }

}

3、方法與構造函數引用
Java 8 允許你使用::關鍵字來引用已有Java類或對象的方法或構造器。::的誕生和Lambda一樣都是來簡化匿名內部類的寫法的,所以::必須配合函數式接口一起用。使用::操作符後,會返回一個函數式接口對象,這個接口可以自己定義,也可以直接使用系統提供的函數式接口,系統提供的後面會單獨介紹。

假如有個Person類如下,以下的例子都以這個Person類爲基礎講解。

public class Person {
    private String name;
    private int age;

    public Person(){

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

   public static String show(String str){
        System.out.println("----->輸入"+str);
        return "----->返回"+str;
    }
}
  • 使用::關鍵字初始化構造函數,返回的是一個函數式接口,這個接口可以自己定義,也可以直接使用系統提供的函數式接口。現在先來定義一個函數式接口用於獲取Person實例。
@FunctionalInterface
public interface PersonSupply {
    Person get();
}

接下來寫一個接口的實現,如果按照常規寫法,寫出來的代碼一般會是如下形式。

PersonSupply sp=new PersonSupply{

  public Person get(){
      Person person=new Person();
      return person;
  }
}

Person person=sp.get();//獲取實例

而自從Java8引入::關鍵字後,就可以簡化爲一行代碼,簡直不要太簡潔。

PersonSupply sp=Person::new; //接口實現
Person person=sp.get();//獲取實例

當然爲了使這個接口更通用,我們可以定義成如下形式

@FunctionalInterface
public interface PersonSupply<T> {
    T get();
}

PersonSupply<Person> sp=Person::new;
Person person=sp.get();

java8自帶的Supplier接口就是這樣實現的,後面會做介紹。直接使用Supplier如下

Supplier<Person> sp=Person::new;
Person person=sp.get();

這種簡便寫法同樣支持帶參構造函數,首先寫一個函數式接口,由於需要參數所以get()裏面輸入參數,返回Person,如下。

@FunctionalInterface
public interface PersonSupply<P extends Person> {
    P get(String name, int age);
}

常規寫法如下:

PersonSupply<Person> sp=new PersonSupply<Person>{

  public Person get(String name, int age);{
      Person person=new Person(name,age);
      return person;
  }
}

Person person=sp.get("maplejaw",20);

簡便寫法如下:

PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw",20);
  • 使用::關鍵字引用靜態方法
    同樣,寫一個函數式接口,該靜態方法需要參數,所以get()裏傳入參數,需要返回參數,所以返回String。
@FunctionalInterface
public interface PersonFactory {
    String get(String str);
}
PersonFactory pf=Person::show;
pf.get("哈哈哈");

PersonFactory pf=Person::show;等價於下面

PersonFactory pf=new PersonFactory{

  public String get(String str){
      return Person.show(str);
  }
}
  • 使用::關鍵字引用普通方法比較特殊。
    如果要調用的方法沒有參數,可以用Class::method形式調用,但是這時需要傳入一個Person實例,這時函數式接口這樣寫,在get()中傳入Person類的實例,返回String。
@FunctionalInterface
public interface PersonFactory {
    String get(Person person);
}
 PersonSupply<Person> sp=Person::new;
 Person person=sp.get("maplejaw", 20);
 PersonFactory pf=Person::getName;      
 System.out.println("--->"+pf.get(person));

PersonFactory pf=Person::getName;等價於下面

PersonFactory pf=new PersonFactory{

  public String get(Person person){
      return person.getName();
  }
}

也可以以instance::method形式調用。這時get()不需要傳參。返回String。

@FunctionalInterface
public interface PersonFactory {
    String get();
}

 PersonSupply<Person> sp=Person::new;
 Person person=sp.get("maplejaw", 20);
 PersonFactory pf=person::getName;      
 System.out.println("--->"+pf.get());

PersonFactory pf=person::getName;等價於下面

Person person=new Person("maplejaw",20);
PersonFactory pf=new PersonFactory{

  public String get( ){
      return person.getName();
  }
}

如果要調用的方法有參數,必須用instance::method形式調用,這時函數式接口這樣寫,set傳入參數。

@FunctionalInterface
public interface PersonFactory {
    void set(String name);
}

PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw", 20);
PersonFactory pf=person::setName;
pf.set("maplejaw");

PersonFactory pf=person::setName;等價於下面

Person person=new Person("maplejaw",20);
PersonFactory pf=new PersonFactory{

  public void set(String name){
      return person.setName(name);
  }
}

4、JAVA8 API內建的函數式接口
還記得前面提到的Supplier函數式接口嗎,它就是API內建的函數式接口之一,下面將介紹一些常用的內建函數式接口。

  • Supplier
    Supplier 提供者,不接受參數,有返回值
    Supplier<Person> Supplier=new Supplier<Person>() {

            @Override
            public Person get() {
                return new Person();
            }

        };

    Supplier<Person> sp=Person::new;
  • Function 一個參數,一個返回值。常用於數據處理。
Function<String, Integer> function=new Function<String, Integer>(){

            @Override
            public Integer apply(String s) {

                return Integer.parseInt(s);
            }

        } ;
Function<String, Integer> function=Integer::parseInt;
  • Consumer 消費者,只有一個參數,沒有返回值
Consumer<String> consumer=new Consumer<String>() {

            @Override
            public void accept(String t) {

            }
        };
  • Comparator 比較類
        Comparator<Integer> comparator=new Comparator<Integer>() {

            @Override
            public int compare(Integer o1, Integer o2) {
                // TODO Auto-generated method stub
                return o1-o2;
            }


        };
  • Predicate
    Predicate 接口,抽象方法只有一個參數,返回boolean類型。該接口包含多種默認方法來將Predicate組合成其他複雜的邏輯(比如:與,或,非):
Predicate<String> predicate=new Predicate<String>() {

            @Override
            public boolean test(String t) {

                return t.startsWith("h");
            }
        };
boolean b=predicate.test("hahaha");//判斷是否符合條件
Predicate<String> predicate = String::isEmpty;
boolean b=predicate.test("hahaha");//判斷是否符合條件
  • UnaryOperator 接收一個參數,返回一個參數,且參數類型相同
    UnaryOperator<String> unaryOperator=new UnaryOperator<String>() {

            @Override
            public String apply(String s) {
                // TODO Auto-generated method stub
                return s;
            }
        };
  • BinaryOperator 接收兩個參數,返回一個參數,且參數類型相同
BinaryOperator<String> binaryOperator=new BinaryOperator<String>() {


            @Override
            public String apply(String t, String u) {
                // TODO Auto-generated method stub
                return t+u;
            }
        };

5、三個API

  • Optional
    Optional 這是個用來防止NullPointerException異常而引入的。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。
Optional<String> optional = Optional.of("給你一個值");
        optional.isPresent(); //判斷是否爲空值
        optional.get();      //獲取值 ,如果空值直接拋異常。
        optional.orElse("返回空值");  //獲取值 ,如果空值返回指定的值。
  • Stream(流)
    最新添加的Stream API(java.util.stream)把真正的函數式編程風格引入到Java中。這是目前爲止對Java類庫最好的補充,因爲Stream API可以極大提供Java程序員的生產力,讓程序員寫出高效率、乾淨、簡潔的代碼。
    接觸過RxJava的可能對下面的代碼風格比較眼熟,Stream 的創建需要指定一個數據源,比如 java.util.Collection的子類,List或者Set, Map不支持。Stream的操作可以串行執行或者並行執行。
    怎麼用Stream?Stream必須有數據源。那就給一個數據源。

    List<String> list = new ArrayList<>();
        list.add("ddd2");
        list.add("aaa2");
        list.add("bbb1");
        list.add("aaa1");
        list.add("aaa3");
        list.add("bbb3");
        list.add("ccc");
        list.add("bbb2");
        list.add("ddd1");
    • forEach list新增的for循環方法,forEach是一個最終操作(只能放在最後)

      //遍歷打印數據
      list.forEach(new Consumer<String>() {
      
                  @Override
                  public void accept(String t) {
                   System.out.println("---->"+t);
      
                  }
              });

      打印結果如下

      ---->ddd2
      ---->aaa2
      ---->bbb1
      ---->aaa1
      ---->aaa3
      ---->bbb3
      ---->ccc
      ---->bbb2
      ---->ddd1
      
    • filter 過濾

      list.stream()
         .filter(new Predicate<String>() {
      
                  @Override
                  public boolean test(String t) {
      
                      return t.startsWith("a");
                  }
              })
          .forEach(new Consumer<String>() {
      
                  @Override
                  public void accept(String t) {
                   System.out.println("---->"+t);
      
                  }
              });

      打印結果如下

      ---->aaa2
      ---->aaa1
      ---->aaa3
      
    • Sort 排序

       list.stream()
              .sorted()//排序,如果不實現Comparator接口,則按默認規則排序
               .filter(new Predicate<String>() {
      
                  @Override
                  public boolean test(String t) {
      
                      return t.startsWith("a");
                  }
              })
               .forEach(new Consumer<String>() {
      
                  @Override
                  public void accept(String t) {
                   System.out.println("---->"+t);
      
                  }
              });
      
          list.stream()
               .sorted(new Comparator<String>() {
      
                  @Override
                  public int compare(String o1, String o2) {
                      // TODO Auto-generated method stub
                      return o1.compareTo(o2);
                  }
              })
               .filter(new Predicate<String>() {
      
                  @Override
                  public boolean test(String t) {
      
                      return t.startsWith("a");
                  }
              })
               .forEach(new Consumer<String>() {
      
                  @Override
                  public void accept(String t) {
                   System.out.println("---->"+t);
      
                  }
              });
    • map

      
           list.stream()
              .sorted(new Comparator<String>() {
      
                  @Override
                  public int compare(String o1, String o2) {
                      // TODO Auto-generated method stub
                      return o1.compareTo(o2);
                  }
              })
               .filter(new Predicate<String>() {
      
                  @Override
                  public boolean test(String t) {
      
                      return t.startsWith("a");
                  }
              })
               .map(new Function<String, String>() {
      
                  @Override
                  public String apply(String t) {
                      // TODO Auto-generated method stub
                      return t+"--->被我處理過了";
                  }
              })
               .forEach(new Consumer<String>() {
      
                  @Override
                  public void accept(String t) {
                   System.out.println("---->"+t);
      
                  }
              });
      
      ---->aaa1--->被我處理過了
      ---->aaa2--->被我處理過了
      ---->aaa3--->被我處理過了
    • Match 匹配,是一個最終操作

      boolean b= list.stream()
                  .anyMatch(new Predicate<String>() {
      
                      @Override
                      public boolean test(String t) {
                          // TODO Auto-generated method stub
                          return t.startsWith("a");
                      }
                  });
      
          System.out.println("----->"+b);//true
          boolean b= list.stream()
                  .allMatch(new Predicate<String>() {
      
                      @Override
                      public boolean test(String t) {
                          // TODO Auto-generated method stub
                          return t.startsWith("a");
                      }
                  });
      
          System.out.println("----->"+b);//false
    • Count 計數,是一個最終操作

      long count = list.stream()
                    .filter(new Predicate<String>() {
      
                          @Override
                          public boolean test(String t) {
      
                              return t.startsWith("a");
                          }
                      })
                    .count();
              System.out.println(count);    // 3
      
    • Reduce 規約,最終操作

           Optional<String> optional = 
                      list
                          .stream()
                           .sorted()
                          .reduce(new BinaryOperator<String>() {
      
                              @Override
                              public String apply(String t, String u) {
      
                                  return t+u;
                              }
                          });
           System.out.println("---->"+optional.get());
           //---->aaa1aaa2aaa3bbb1bbb2bbb3cccddd1ddd2
    • findFirst 提取第一個,最終操作

       Optional<String> optional = 
                      list
                          .stream()
                           .sorted()
                          .findFirst();
           System.out.println("---->"+optional.get()); //aaa1
      
  • parallelStream(並行流)
    並行化之後和之前的代碼區別並不大。並且並行操作下,速度會比串行快。但是需要注意的是不用在並行流下排序,並行流做不到排序。

 list.parallelStream()
            .filter(new Predicate<String>() {

                @Override
                public boolean test(String t) {
                    // TODO Auto-generated method stub
                    return t.startsWith("a");
                }
            })
            .forEach(new Consumer<String>() {

                @Override
                public void accept(String t) {
                    // TODO Auto-generated method stub
                    System.out.println("--->"+t);
                }
            });

    list.parallelStream()
            .sorted()
            .forEach(new Consumer<String>() {

                @Override
                public void accept(String t) {
                    // TODO Auto-generated method stub
                    System.out.println("--->"+t);
                }
            });
            //打印出來的並沒有排序

Lambda 表達式

在上面你是不是覺得::有時候挺好用的?可以不用再new接口,再也不用寫煩人的匿名內部類了,比如

    Supplier<Person> sp=Person::new;

但是::的使用場景還是比較有限的。Lambda表達式的誕生就是爲了解決匿名內部類中飽受詬病的問題的。

  • 什麼是Lambda表達式
    Lambda表達式是Java8的一個新特性,它提供了一種更加清晰和簡明的方式使用函數式接口(以前被叫作單一方法接口)。使用Lambda表達式能夠更加方便和簡單的使用匿名內部類,比如對於集合的遍歷、比較、過濾等等。

  • Lambda表達式格式
    (type1 arg1, type2 arg2…) -> { body }
    每個lambda都包括以下三個部分:
    參數列表:(type1 arg1, type2 arg2…)
    箭頭: ->
    方法體:{ body }

    方法體既可以是一個表達式,也可以是一個語句塊:

    • 表達式:表達式會被執行然後返回執行結果。
    • 語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣,return語句會把控制權交給匿名方法的調用者。
      表達式函數體適合小型lambda表達式,它消除了return關鍵字,使得語法更加簡潔。

以下是一些例子

(int a, int b) -> {  return a + b; }
( a,  b) -> {  return a + b; }
( a,  b) -> a+b
() -> System.out.println("s")
(String s) -> { System.out.println(s); }
  • 一個 Lambda 表達式可以有零個或多個參數
  • 參數的類型既可以明確聲明,也可以根據上下文來推斷。例如:(int a)與(a)效果相同
  • 所有參數需包含在圓括號內,參數之間用逗號相隔。例如:(a, b) 或 (int a, int b)
  • 空圓括號代表參數集爲空。例如:()->{System.out.println(“s”);};
  • 當只有一個參數,且其類型可推導時,圓括號()可省略。
  • 如果 Lambda 表達式的主體只有一條語句,花括號{}可省略,且不能以分號結尾
  • 如果 Lambda 表達式的主體包含一條以上語句,則表達式必須包含在花括號{}中,每條語句必須以分號結尾。

Lambda表達式可以用來簡化內部類寫法,比如

   //常規代碼
   Runnable runable=new Runnable() {

        @Override
        public void run() {

            System.out.println("--->");
        }
    };

    //Lambda表達式
    Runnable runable=()->{System.out.println("--->");};
    //啓動一個線程
    new Thread(()->{System.out.println("--->");}).start();

還記得前面介紹的Predicate接口嗎?現在我們再來用Lambda表達式寫一遍。

//一般寫法
Predicate<String> predicate=new Predicate<String>() {

            @Override
            public boolean test(String t) {

                return t.startsWith("h");
            }

        };
//Lambda寫法
Predicate<String> predicate=(String s)->{s.startsWith("h");};

你現在是不是覺得Lambda表達式太神奇了?居然可以寫出這麼簡潔的代碼。
還記得內部類使用局部變量時需要把變量聲明爲final嗎,Lambda表達式則不需要。不過雖然不用聲明final,但也不允許改變值。

        String s="sss";
        new Thread(()->{
            System.out.println(s);
        }).start();

此外,內部類引用外部類也不用使用MainActivity.this,這種操蛋的代碼了。
在Lambda表達式中this,指的就是外部類,因爲根本就沒有內部類的概念啊。

  btn.setOnClickListener(()->{
            Toast.makeText(this,"xxx",Toast.LENGTH_SHORT).show();
        });

現在回過頭來把前面Stream中的代碼用Lambda表達式再寫一遍。

         list.stream()
             .sorted((s1,s2)->s1.compareTo(s2))
             .filter((s)->s.startsWith("a"))
             .map((s)->s+"被我處理過了")
             .forEach(s->System.out.println(s));

代碼簡潔的簡直讓人窒息。但是能不能更簡潔一點呢?當然是可以的,首先我們檢查一下哪裏可以替換成::關鍵字,然後作如下替換,是不是更簡潔了。關於替換規則,請看前面的介紹。

 list.stream()
            .sorted((s1,s2)->s1.compareTo(s2))
             .filter((s)->s.startsWith("a"))
             .map((s)->s+"被我處理過了")
             .forEach(System.out::println);

打印結果如下

aaa1被我處理過了
aaa2被我處理過了
aaa3被我處理過了
  • 顯示指定目標類型
    如果Lambda表達式的目標類型是可推導的,就不用指定其類型,如果Lambda表達式的參數類型是可以推導,就不用指定參數類型。因爲編譯器可以根據上下文自動推導出其類型,然後進行隱式轉換。但是,有些場景編譯器是沒法推導的。比如下面這樣的,如果不顯示指定類型,編譯器就會提示錯誤
//接口一
public interface IMyInterface1 {
    void opreate(String str);
}
//接口二
public interface IMyInterface2 {
    void opreate(int i);
}
//Person類
public class Person類 {
    private String name;
    private int age;

    public Test(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void opreate(IMyInterface1 inter){
        inter.opreate(name);
    }

    public void opreate(IMyInterface2 inter){
        inter.opreate(age);
    }
}


 //這樣寫是錯誤的,因爲編譯器無法推導出其目標類型
  new Test("maplejaw",20).opreate((name)->System.out.println(name));

解決辦法有兩個
一、指定參數類型,但是如果兩個接口的參數類型是一樣的,就只能顯示指定目標類型。

  new Test("maplejaw",20).opreate((String name)->System.out.println(name));

二、指定目標類型

  new Test("maplejaw",20).opreate((IMyInterface1) (s)->System.out.println(s));
  new Test("maplejaw",20).opreate((IMyInterface1) System.out::println);

由於目標類型必須是函數式接口,所以如果想賦值給Object對象時,也必須顯示轉型。

Object runnable=(Runnable)()->{System.out.print("--->");};

關於Lambda表達式的介紹到此爲止,想更深入瞭解推薦【深入理解Java 8 Lambda】這篇文章。

最後

當初學習Lambda表達式的時候,由於網上的資料比較零散,且直接用了JAVA8的新API來做演示,由於對新API不是很熟導致學習的時候走了一些彎路,看得一頭霧水。所以決定把我的學習路線給記錄下來,或許可以幫助部分人。
Lambda表達式是把雙刃劍,讓代碼簡潔的同時,降低了代碼的可讀性。但是作爲程序員,追逐新技術的腳步不能停下。


本文參考了【深入理解Java 8 Lambda】【JAVA8 十大新特性詳解】兩篇文章

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