Java中lambda表示式詳解

付威的網路部落格發表於2019-07-21

原文地址:http://blog.laofu.online/2018/04/20/java-lambda/

為什麼使用lambda

在java中我們很容易將一個變數賦值,比如int a =0;int b=a;

但是我們如何將一段程式碼和一個函式賦值給一個變數?這個變數應該是什麼的型別?

在javascript中,可以用一個物件來儲存。   

 

var t=function()
{
  int a=1;
  a=a+1;
  alert(a);
} 

 

在java中,直到java8的lambda的特性問世,才有辦法解決這個問題

什麼是lambda

什麼是lambda? lambda在程式中到底是怎樣的一個存在? 首先看程式碼:

interface eat 
{
  void eatFood();
}

public static void main(String[] args) 
{

  eat e = () -> System.out.printf("hello\n");
  e.eatFood();
  
  eat e1 = new eat() 
  {
      @Override
      public void eatFood() 
      {
          System.out.printf("anoymous class\n");
      }
  };
  e1.eatFood();
}

 

上面的程式碼中,e是一個lambda的物件,根據java的繼承的特性,我們可以說e物件的型別是繼承自eat介面。而e1是一個正常的匿名類的物件.

通過對比, 可以說 lambda的表示式其實是介面的實現的“另一種方式”。這種方式更加簡潔,更容易閱讀。除了程式碼層面的簡潔外,在編譯的結果時候lambda也不會產生一個多餘的匿名類。

對於eat這個特殊的介面,稱之為:函式式介面

lamda的優點

  • 程式碼縮減

  • Option的使用簡化程式碼

    假如我們有個方法,能夠產生一個Option物件std   

Option<Person> std=getStudent();

 

       1、是否為空的判斷

         

     2、返回不為空的物件

          

  3、多重if else的簡化

           

函式式介面

什麼是函式式介面?這個是我們理解Lambda表示式的重點,也是產生lambda表示式的“母體”,這裡我們引用一個比較容易理解的說法:

函式式介面是 一個只有一個抽象方法(不包含object中的方法)的介面。

這個需要說明一點,就是在Java中任何一個物件都來自Object 所有介面中自然會繼承自Object中的方法,但在判斷是否是函式式介面的時候要排除Object中的方法,下面舉幾個例子如下:

//這個是函式式介面
interface eat 
{  
   void eatFood();    
}


//這個不是是函式式介面
interface eat
{  
  default void eatFood()
  {
     System.out.println("hello");
  };    
}



//這個是一個函式式介面
interface eat 
{  
 void eatFood();    
 String toString();
}

 

 對於是否是函式式介面,java8中也提供了一個專用的註解:@FunctionalInterface。通過這個註解,可以確定是否是函式式介面:

//此處會報編譯錯誤
@FunctionalInterface
interface eat 
{  
  default void eatFood()
  {
    System.out.println("hello");
  };    
}

 

   下面我們寫一段lambda簡單的程式碼,找出指定的身份證號碼,並列印出來。

      

最終的呼叫:

      

 

對於上面的程式碼實現,在我們呼叫excutor方法前,並不知道findName的實現方法,直到在最後把一個方法作為引數傳入到excutor方法中。

反思:函式式介面NameCheckInterface,是不是可以用來表示所有返回值為bool型別的,有兩個形參(型別是passager 和String型別)的lambda表示式?

如果我們再配合泛型的話,是不是我們只需要定義一個通用的函式式介面?下面我們改寫下程式碼:

@FunctionalInterface
public interface NameCheckInterface<T,T1,T2> 
{
   T2 findName(T passager,T1 name);
}

@FunctionalInterface
public interface PrintInterface<T> 
{
  void printName(T name);
}

private void excutor(List<passager> passagerList, NameCheckInterface<Boolean,passager,String> checker, PrintInterface<String> printer) 
{
  for (passager p : passagerList) {
      if (checker.findName(p,"李四")){
        printer.printName(p.getPassagerNo());
      }
  }
}

 

對應的呼叫方法

@Test
public void simpTest() 
{
  List<passager> passagerList = new ArrayList<>();
  passagerList.add(new passager("李四", "123456789"));
  passagerList.add(new passager("張三", "123456789"));
  passagerList.add(new passager("王二", "123456789"));

  excutor(passagerList,(p,str)->p.getName().equals(str),str-> System.out.println(str));
}

 

對於這段程式碼,可以得出lambda中的函式式介面是可以公用的,而jdk中也已經定義了很多通用的函式式介面。  


 

常用的函式式介面

在jdk中通用的函式式介面如下(都在java.util.function包中):

Runnable r = () -> System.out.printf("say hello");//沒有輸入引數,也沒有輸出
Supplier<String> sp = () -> "hello";//只有輸出訊息,沒有輸入引數
Consumer<String> cp = r -> System.out.printf(r);//有一個輸入引數,沒有輸出
Function<Integer, String> func = r -> String.valueOf(r);//有一個輸入引數 有一個輸出引數
BiFunction<Integer, Integer, String> biFunc = (a, b) -> String.valueOf(a + b);//有兩個輸入引數 有一個輸出引數
BiConsumer<Integer, Integer> biCp = (a, b) -> System.out.printf(String.valueOf(a + b));//有兩個輸入引數 沒有輸出引數

 

PS:上面是基本的方法,其他的都是基於這幾個擴充套件而來

如果上面的程式碼使用jdk中的函式式介面的話,就不用額外的定義NameCheckInterface和PrintInterface 介面了。根據上面的引數和返回值的形式,可以使用BiFunction和Consumer直接改寫excutor方法:

private void excutor(List<passager> passagerList, BiFunction<passager,String,Boolean> checker, Consumer<String> printer) {
        for (passager p : passagerList) {
            if (checker.apply(p,"李四")){
                printer.accept(p.getPassagerNo());
            }
        }
        
    }

 

 

函式的引用

從上面的demo中,使用通用的函式表示式能夠減少自定義函式式介面,為了進一步簡化程式碼,lambda表示式可以改寫成函式的引用的形式

函式的引用是lambda表示式的更簡潔的一種寫法,也是更能體現出函數語言程式設計的一種形式,讓我們更能理解lambda終歸也是一個“函式的物件”。 下面我們改寫一個例子:

Consumer<String> c1 = r -> System.out.printf(r); 
 c1.accept("1");
 Consumer<String> c2 =System.out::printf; 
 c1.accept("2");

 

在上面的demo中lambda表示式被我們改寫成System.out::printf這個形式,等於我們把一個函式直接賦值給了一個c2物件,這裡我們可以俗稱(非官方)c2為java函式的一個物件,這個也結局填補了java中一個空白。


函式引用的規則

對於Java中lambda改成函式的引用要遵循一定的規則,具體可以分為下面的四種形式:

  1. 靜態方法的引用

          如果函式式介面的實現恰好可以通過呼叫一個靜態方法來實現,那麼就可以使用靜態方法引用

Consumer<String> c1 = r -> Integer.parseInt(r);
     c1.accept("1");
     Consumer<String> c2 =Integer::parseInt;
     c1.accept("2");

 

    2.例項方法引用

     如果函式式介面的實現恰好可以通過呼叫一個例項方法來實現,那麼就可以使用例項方法引用

        

 Consumer<String> ins1 = r -> System.out.print(r);
 c1.accept("1");
 Consumer<String> ins2 =System.out::print;
 c1.accept("2");

  

  3.物件方法引用

     抽象方法的第一個引數型別剛好是例項方法的型別,抽象方法剩餘的引數恰好可以當做例項方法的引數。如果函式式介面的實現能由上面說的例項方法呼叫來實現的話,那麼就可以使用物件方法的引用。

 

  Function<BigDecimal,Double> fuc1=t->t.doubleValue();
  fuc1.apply(new BigDecimal("1.025"));
  Function<BigDecimal,Double> fuc2=BigDecimal::doubleValue;
  fuc2.apply(new BigDecimal("1.025"));


  BiFunction<BigDecimal, BigDecimal, BigDecimal> func3 = (x, y) -> x.add(y);
  func3.apply(new BigDecimal("1.025"), new BigDecimal("1.254"));
  BiFunction<BigDecimal, BigDecimal, BigDecimal> func4 = BigDecimal::add;
  func4.apply(new BigDecimal("1.025"), new BigDecimal("1.254"));

 

   4.構造方法引用

      如果函式式介面的實現恰好可以通過呼叫一個類的構造方法來實現,那麼就可以使用構造方法引用。

     

 Consumer<String> n1 = r ->new BigDecimal(r);
  c1.accept("1");
  Consumer<String> n2 =BigDecimal::new;
  c1.accept("2");

  

stream API的引用

Stream是處理陣列和集合的API,Stream具有以下特點:

  • 不是資料結構,沒有內部儲存
  • 不支援索引訪問
  • 延遲計算
  • 支援過濾,查詢,轉換,彙總等操作

對於StreamAPI的學習,首先需要弄清楚lambda的兩個操作型別:中間操作和終止操作。 下面通過一個demo來認識下這個過程。

Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
           System.out.print(x);
           return  x>3;
       });

 

當我們執行這段程式碼的時候,發現並沒有任何輸出,這是因為lambda表示式需要一個終止操作來完成最後的動作。 我們修改程式碼:

Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
           System.out.print(x);
           return  x>3;
       });
       
   st.forEach(t-> System.out.print(t));

對應的輸出結果是:

1234455

  

為什麼會有這個輸出呢?因為在filter函式的時候並沒有真正的執行,在forEach的時候才開始執行整個lambda表示式,所以當執行到4的時候,filter輸出之後,forEach也執行了,最終結果是1234455


對於Java中的lambda表示式的操作,可以歸類和整理如下:

中間操作:

  • 過濾 filter
  • 去重 distinct
  • 排序 sorted
  • 擷取 limit、skip
  • 轉換 map/flatMap
  • 其他 peek

終止操作

  • 迴圈 forEach
  • 計算 min、max、count、 average
  • 匹配 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny
  • 匯聚 reduce
  • 收集器 toArray collect

常用的幾個lambda

下面我們對這幾個常用的lambda表示式寫幾個demo,首先定義公共的Student類:

public class Student {
      
      public Student(String stuName, int age, BigDecimal score, int clazz) {
          this.stuName = stuName;
          this.age = age;
          this.score = score;
          this.clazz = clazz;
      }
      
      private String stuName;
      private int age;
      private BigDecimal score;
      private int clazz;
      
      public String getStuName() {
          return stuName;
      }
      public void setStuName(String stuName) {
          this.stuName = stuName;
      }
      public int getAge() {
          return age;
      }
      public void setAge(int age) {
          this.age = age;
      }
      public BigDecimal getScore() {
          return score;
      }
      public void setScore(BigDecimal score) {
          this.score = score;
      }
      public int getClazz() {
          return clazz;
      }
      public void setClazz(int clazz) {
          this.clazz = clazz;
      }
  }
  
  List<Student> studentList = new ArrayList<>();
  studentList.add(new Student("黎  明", 20, new BigDecimal(80), 1));
  studentList.add(new Student("郭敬明", 22, new BigDecimal(90), 2));
  studentList.add(new Student("明  道", 21, new BigDecimal(65.5), 3));
  studentList.add(new Student("郭富城", 30, new BigDecimal(90.5), 4));
  studentList.add(new Student("劉詩詩", 20, new BigDecimal(75), 1));
  studentList.add(new Student("成  龍", 60, new BigDecimal(88), 5));
  studentList.add(new Student("鄭伊健", 60, new BigDecimal(86), 1));
  studentList.add(new Student("劉德華", 40, new BigDecimal(81), 1));
  studentList.add(new Student("古天樂", 50, new BigDecimal(83), 2));
  studentList.add(new Student("趙文卓", 40, new BigDecimal(84), 2));
  studentList.add(new Student("吳奇隆", 30, new BigDecimal(86), 4));
  studentList.add(new Student("言承旭", 50, new BigDecimal(68), 1));
  studentList.add(new Student("鄭伊健", 60, new BigDecimal(86), 1));
  studentList.add(new Student("黎  明", 20, new BigDecimal(80), 1));
  studentList.add(new Student("李連杰", 65, new BigDecimal(86), 4));
  studentList.add(new Student("周潤發", 69, new BigDecimal(58), 1));
  studentList.add(new Student("徐若萱", 28, new BigDecimal(88), 6));
  studentList.add(new Student("許慧欣", 26, new BigDecimal(86), 8));
  studentList.add(new Student("陳慧琳", 35, new BigDecimal(64), 1));
  studentList.add(new Student("關之琳", 45, new BigDecimal(50), 9));
  studentList.add(new Student("溫碧霞", 67, new BigDecimal(53), 2));
  studentList.add(new Student("林青霞", 22, new BigDecimal(56), 3));
  studentList.add(new Student("李嘉欣", 25, new BigDecimal(84), 1));
  studentList.add(new Student("彭佳慧", 26, new BigDecimal(82), 5));
  studentList.add(new Student("陳紫涵", 39, new BigDecimal(88), 1));
  studentList.add(new Student("張韶涵", 41, new BigDecimal(90), 6));
  studentList.add(new Student("梁朝偉", 58, new BigDecimal(74), 1));
  studentList.add(new Student("梁詠琪", 65, new BigDecimal(82), 7));
  studentList.add(new Student("范瑋琪", 22, new BigDecimal(83), 1));    

 

  

forEach


forEach:代表迴圈當前的list ,下面的例子是迴圈列印出student的名字

studentList.stream().forEach(x -> System.out.println(x.getStuName()));

 

filter

根據條件過濾當前的資料,獲得分數大於80的學生名稱

studentList.stream().filter(t -> t.getScore().compareTo(new BigDecimal(80)) > 0).forEach(x -> System.out.println(x.getStuName()));

distinct、sorted 、group

 1.去除重複資料

studentList.stream().distinct().forEach(x -> System.out.println(x.getStuName()));

  

2.單條件排序和多條件排序

  
   studentList.stream().sorted(Comparator.comparing(Student::getScore)).forEach(x -> System.out.println(x.getStuName()));
   
   //多條件排序
  studentList.stream().sorted(Comparator.comparing(Student::getScore).thenComparing(Student::getStuName)).forEach(x -> System.out.println(x.getStuName()));

 

3.group 的使用

  System.out.println(studentList.stream().collect(Collectors.groupingBy(x->x.getAge(),Collectors.counting())));

limit、skip

跳過多少,取多少個元素,可以根據當前的資料進行分頁

studentList.stream().skip(10).limit(5).forEach(x -> System.out.println(x.getStuName())); 

//具體的分頁
int pageIndex=1;
int pageSize=5;
studentList.stream().skip((pageIndex-1)*pageSize).limit(pageSize).forEach(x -> System.out.println(x.getStuName())); 

map/flatMap

map是一個轉換的工具,提供很多轉換的方法,mapToInt,mapToDouble

studentList.stream().map(Student::getScore).forEach(x -> System.out.println(x));

  

上面的結果是輸出當前的所有同學的得分。

flatMap是一個可以把子陣列的值放到陣列裡面, 下面的例項是把所有的名字都拆開成一個新的陣列

studentList.stream().flatMap(x-> Arrays.stream(x.getStuName().split(""))).forEach(x -> System.out.println(x));

min、max、count、 average

一組常用的統計函式:

studentList.stream().max(Comparator.comparing(x -> x.getAge())).ifPresent(x-> System.out.println(x.getAge()));
  studentList.stream().min(Comparator.comparing(x -> x.getAge())).ifPresent(x-> System.out.println(x.getAge()));
  System.out.println(studentList.stream().count());
  studentList.stream().mapToDouble(x -> x.getScore().doubleValue()).average().ifPresent(x-> System.out.println(x));

  

anyMatch、noneMatch、 allMatch、 findFirst、 findAny

  • anyMatch: 操作用於判斷Stream中的是否有滿足指定條件的元素。如果最少有一個滿足條件返回true,否則返回false。

  • noneMatch: 與anyMatch相反。allMatch是判斷所有元素是不是都滿足表示式。

  • findFirst: 操作用於獲取含有Stream中的第一個元素的Optional,如果Stream為空,則返回一個空的Optional。若Stream並未排序,可能返回含有Stream中任意元素的Optional。

  • findAny: 操作用於獲取含有Stream中的某個元素的Optional,如果Stream為空,則返回一個空的Optional。由於此操作的行動是不確定的,其會自由的選擇Stream中的任何元素。在並行操作中,在同一個Stram中多次呼叫,可能會不同的結果。在序列呼叫時,都是獲取的第一個元素, 預設的是獲取第一個元素,並行是隨機的返回。

 

System.out.println(studentList.stream().anyMatch(r -> r.getStuName().contains("偉")));
System.out.println(studentList.stream().allMatch(r -> r.getStuName().contains("偉")));
System.out.println(studentList.stream().noneMatch(r -> r.getStuName().contains("偉")));
System.out.println(studentList.stream().findFirst());
System.out.println(studentList.stream().findAny());

for (int i=0;i<10;i++)
{
   System.out.println(studentList.stream().parallel().findAny().get().getStuName());
}

 

reduce

對於reduce的使用,應該在js中也有接觸到,但也是比較小眾的功能,但使用起來功能卻非常的強大,先看一個正常的demo:

Stream.of(1, 5, 10, 8).reduce((x, y) -> {
        System.out.println("x : " + x);
        System.out.println("y : " + y);
        System.out.println("x+y : " +x);
        
        System.out.println("--------");
        return x + y;
    });

 

列印結果:

x : 1
y : 5
x+y : 1
--------
x : 6
y : 10
x+y : 6
--------
x : 16
y : 8
x+y : 16
--------

可以看出:

  1. reduce是一個迴圈,有兩個引數
  2. 第一次執行的時候x是第一個值,y是第二個值。
  3. 在第二次執行的時候,x是上次返回的值,y是第三個值
    …. 直到迴圈結束為止。

再修改程式碼如下:

//指定了初始值

Stream.of(1, 5, 10, 8).reduce(100,(x, y) -> {
	System.out.println("x : " + x);
	System.out.println("y : " + y);
	System.out.println("x+y : " +x);
	System.out.println("--------");
	return x + y;
});

 

x : 100
y : 1
x+y : 100
--------
x : 101
y : 5
x+y : 101
--------
x : 106
y : 10
x+y : 106
--------
x : 116
y : 8
x+y : 116
--------

  

toArray、collect

toArray和collect是兩個收集器,toArray是把資料轉換成陣列,collect是轉成其他的型別。這裡就不在討論了。

System.out.println(studentList.stream().collect(Collectors.groupingBy(x->x.getAge(),Collectors.counting())));

  

  

 


  

 

相關文章