1. 程式人生 > >JAVA8 Lambda表示式完全解析

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表示式是把雙刃劍,讓程式碼簡潔的同時,降低了程式碼的可讀性。但是作為程式設計師,追逐新技術的腳步不能停下。