原文地址: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改成函式的引用要遵循一定的規則,具體可以分為下面的四種形式:
- 靜態方法的引用
如果函式式介面的實現恰好可以通過呼叫一個靜態方法來實現,那麼就可以使用靜態方法引用
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 --------
可以看出:
- reduce是一個迴圈,有兩個引數
- 第一次執行的時候x是第一個值,y是第二個值。
- 在第二次執行的時候,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())));