聊聊Java 8 Lambda 表達式

 

    早在2014年oracle發佈了jdk 8,在裏面增長了lambda模塊。因而java程序員們又多了一種新的編程方式:函數式編程,也就是lambda表達式。我本身用lambda表達式也差很少快4年了,但在工做中卻鮮有看到同事使用這種編程方式,即便有些使用了,但感受好像對其特性也不是很瞭解。我看了一上網上的資料也很多,本身整理了一下順便寫下一些本身的見解,但願個人分享能帶給別人一些幫助。php

 

    函數式編程基本概念入門html

  •    什麼是函數式編程 

        函數式編程(英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程典範,它將電腦運算視爲數學上的函數計算,而且避免使用程序狀態以及易變對象。函數編程語言最重要的基礎是λ演算(lambda calculus)。並且λ演算的函數能夠接受函數看成輸入(引數)和輸出(傳出值)。比起指令式編程,函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。這是維基百科給出的定義。從這個咱們知道函數式編程是相對於指令式編程的一種編程典範,而且對而言具備一些優勢。java

  •   函數式編程的特性與優缺點

      特性程序員

      一、函數是"第一等公民" 算法

         什麼是"第一等公民"?所謂"第一等公民"(first class),指的是函數與其餘數據類型同樣,處於平等地位,它不只擁有一切傳統函數的使用方式(聲明和調用),能夠賦值給其餘變量(賦值),也能夠做爲參數,傳入另外一個函數(傳參),或者做爲別的函數的返回值(返回)。函數能夠做爲參數進行傳遞,意味咱們能夠把行爲"參數化",處理邏輯能夠從外部傳入,這樣程序就能夠設計得更靈活。express

     二、沒有"反作用"編程

     所謂"反作用"(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。函數式編程強調沒有"反作用",意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。api

    三、引用透明數組

    引用透明(Referential transparency),指的是函數的運行不依賴於外部變量或"狀態",只依賴於輸入的參數,任什麼時候候只要參數相同,引用函數所獲得的返回值老是相同的。這裏強調了一點"輸入"不變則"輸出"也不變,就像數學函數裏面的f(x),只要輸入的x同樣那獲得的結果也確定定是同樣的。數據結構

      優勢:

    一、代碼簡潔,開發快速。

     函數式編程大量使用函數,減小了代碼的重複,所以程序比較短,開發速度較快。Paul Graham在《黑客與畫家》一書中寫道:一樣功能的程序,極端狀況下,Lisp代碼的長度多是C代碼的二十分之一。若是程序員天天所寫的代碼行數基本相同,這就意味着,"C語言須要一年時間完成開發某個功能,Lisp語言只須要不到三星期。反過來講,若是某個新功能,Lisp語言完成開發須要三個月,C語言須要寫五年。"固然,這樣的對比故意誇大了差別,可是"在一個高度競爭的市場中,即便開發速度只相差兩三倍,也足以使得你永遠處在落後的位置。" 

    2. 接近天然語言,易於理解

       函數式編程的自由度很高,能夠寫出很接近天然語言的代碼。以java爲例把學生以性別分組:

       沒用labmda表達式:       

Map<String,List<Student>> studentsMap = new HashMap<>();
        for(Student student : students){
            List<Student> studentList = studentsMap.getOrDefault(student.getSex(), new ArrayList<>());
            studentList.add(student);
            studentsMap.put(student.getSex(),studentList);
        }

  用了lambda表達式:

        Map<String,List<Student>> studentsMap = students.stream().collect(Collectors.groupingBy(Student::getSex));

       這基本就是天然語言的表達了,你們應該一眼就能明白它的意思吧。 

      3. 更方便的代碼管理

      函數式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果一定相同。所以,每個函數均可以被看作獨立單元,頗有利於進行單元測試(unit testing)和除錯(debugging),以及模塊化組合。 

      4. 易於"併發編程"
      函數式編程不須要考慮"死鎖"(deadlock),由於它不修改變量,因此根本不存在"鎖"線程的問題。沒必要擔憂一個線程的數據,被另外一個線程修改,因此能夠很放心地把工做分攤到多個線程,部署"併發編程"(concurrency)。
     請看下面的代碼:
     var s1 = Op1();
     var s2 = Op2();
     var s3 = concat(s1, s2);
     因爲s1和s2互不干擾,不會修改變量,誰先執行是無所謂的,因此能夠放心地增長線程,把它們分配在兩個線程上完成。其餘類型的語言就作不到這一點,由於s1可能會修改系統狀態,而s2可能會用到這些狀態,因此必須保證s2在s1以後運行,天然也就不能部署到其餘線程上了。多核CPU是未來的潮流,因此函數式編程的這個特性很是重要。

      5. 代碼的熱升級

      函數式編程沒有反作用,只要保證接口不變,內部實現是外部無關的。因此,能夠在運行狀態下直接升級代碼,不須要重啓,也不須要停機。Erlang語言早就證實了這一點,它是瑞典愛立信公司爲了管理電話系統而開發的,電話系統的升級固然是不能停機的。

     缺點:

     一、函數式編程常被認爲嚴重耗費在CPU和存儲器資源。主因有二:

  • 早期的函數式編程語言實現時並沒有考慮過效率問題。
  • 有些非函數式編程語言爲求提高速度,不提供自動邊界檢查或自動垃圾回收等功能。
    惰性求值亦爲語言如 Haskell增長了額外的管理工做。
    
    二、語言學習曲線陡峭,難度高

    函數式語言對開發者的要求比較高,學習曲線比較陡,並且很容易由於其靈活的語法控制很差程序的結構。

 

     介紹完函數式編程的概念和優缺點以後,下面讓咱們來進入java8 lambda的編程世界~

 

      Lambda表達式的組成

       java 8 中Lambda 表達式由三個部分組成:第一部分爲一個括號內用逗號分隔的形式參數,參數是函數式接口裏面方法的參數;第二部分爲一個箭頭符號:->;第三部分爲方法體,能夠是表達式和代碼塊。語法以下

       一、方法體爲表達式,該表達式的值做爲返回值返回。

(parameters) -> expression
(int a,int b) -> return a + b; //求和

       二、方法體爲代碼塊,必須用 {} 來包裹起來,且須要一個 return 返回值,但若函數式接口裏面方法返回值是 void,則無需返回值。

(parameters) -> { statements; }
(int a) -> {System.out.println("a = " + a);} //打印,無返回值
(int a) -> {return a * a;} //求平方

     

      Lambda表達式的底層實現

      java 8 內部Lambda 表達式的實現方式在本質是以匿名內部類的形式的實現的,看下面代碼。代碼中咱們定義了一個叫binaryOperator的Lambda表達式,看返回值它是一個IntBinaryOperator實例。  

IntBinaryOperator binaryOperator = (int a, int b) -> {
    return a + b;
};
int result = binaryOperator.applyAsInt(1, 2);
System.out.println("result = " + result); //3

    咱們再看一下IntBinaryOperator的定義 

@FunctionalInterface
public interface IntBinaryOperator {
    /**
     * Applies this operator to the given operands.
     * @param left the first operand
     * @param right the second operand
     * @return the operator result
     */
    int applyAsInt(int left, int right);
}

       

     咱們得知IntBinaryOperator是一個接口而且上面有一個@FunctionalInterface的註解,@FunctionalInterface標註了這是一個函數式接口,因此咱們知道了(int a, int b) -> {return a + b;}返回的一個IntBinaryOperator的匿名實現類。

 

     Lambda表達式的函數式接口 

     上面提到了函數式接口,那這是一個什麼樣的概念呢?

      函數式接口(Functional Interface)是Java 8對一類特殊類型的接口的稱呼。這類接口只定義了惟一的抽象方法的接口(除了隱含的Object對象的公共方法,所以最開始也就作SAM類型的接口(Single Abstract Method)。定義函數式接口的緣由是在Java Lambda的實現中,開發組不想再爲Lambda表達式單獨定義一種特殊的Structural函數類型,稱之爲箭頭類型(arrow type,依然想採用Java既有的類型(class, interface, method等).緣由是增長一個結構化的函數類型會增長函數類型的複雜性,破壞既有的Java類型,並對成千上萬的Java類庫形成嚴重的影響。權衡利弊,所以最終仍是利用SAM 接口做爲 Lambda表達式的目標類型.另外對於函數式接口來講@FunctionalInterface並非必須的,只要接口中只定義了惟一的抽象方法的接口那它就是一個實質上的函數式接口,就能夠用來實現Lambda表達式。

       在java 8中已經爲咱們定義了不少經常使用的函數式接口它們都放在java.util.function包下面,通常有如下經常使用的四大核心接口:       

函數式接口 參數類型 返回類型 用途
Consumer<T>(消費型接口) T void 對類型爲T的對象應用操做。void accept(T t)
Supplier<T>(供給型接口) T 返回類型爲T的對象。 T get();
Function<T, R>(函數型接口) T R 對類型爲T的對象應用操做並返回R類型的對象。R apply(T t);
Predicate<T>(斷言型接口) T boolean 肯定類型爲T的對象是否知足約束。boolean test(T t);

 

    Lambda表達式的應用場景

     一、使用() -> {} 替代匿名類

 Thread t1 = new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("no use lambda");
              }
          });
         
Thread t2 = new Thread(() -> System.out.println("use lambda"));

  咱們看到相對而言Lambda表達式要比匿名類要優雅簡潔不少~。

     二、以流水線的方式處理數據

        List<Integer> integers = Arrays.asList(4, 5, 6,1, 2, 3,7, 8,8,9,10);

        List<Integer> evens = integers.stream().filter(i -> i % 2 == 0)
                .collect(Collectors.toList()); //過濾出偶數列表 [4,6,8,8,10]
List<Integer> sortIntegers = integers.stream().sorted() .limit(5).collect(Collectors.toList());//排序而且提取出前5個元素 [1,2,3,4,5] List<Integer> squareList = integers.stream().map(i -> i * i).collect(Collectors.toList());//轉成平方列表 int sum = integers.stream().mapToInt(Integer::intValue).sum();//求和 Set<Integer> integersSet = integers.stream().collect(Collectors.toSet());//轉成其它數據結構好比set Map<Boolean, List<Integer>> listMap = integers.stream().collect(Collectors.groupingBy(i -> i % 2 == 0)); //根據奇偶性分組 List<Integer> list = integers.stream().filter(i -> i % 2 == 0).map(i -> i * i).distinct().collect(Collectors.toList());//複合操做

  藉助stream api和Lambda表達式,以住須要定義多個變量,編寫數十行甚至數百行的代碼的集合操做,如今都基本簡化成了能夠在一行以內完成~

      三、更簡單的數據並行處理

        List<Integer> squareList = integers.stream().parallel().map(i -> i * i).collect(Collectors.toList());//轉成平方列表

    數據並行處理,只須要在原來的基礎上加一個parallel()就能夠開啓~。順便提一下這裏parallel()開啓的底層並行框架是fork/join,默認的並行數是Ncpu個。

      四、用內部迭代取代外部迭代

       外部迭代:描述怎麼幹,代碼裏嵌套2個以上的for循環的都比較難讀懂;只能順序處理List中的元素;

       內部迭代:描述要幹什麼,而不是怎麼幹;不必定須要順序處理List中的元素

List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
    System.out.println(feature); //外部迭代
}

List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
 "Date and Time API");
features.stream.forEach(n -> System.out.println(n)); //內部迭代

       五、重構現有臃腫代碼,更高的開發效率

      在Lambda表達式出現以前,咱們的處理邏輯只能是以命令式編程的方式來實現,須要大量的代碼去編寫程序的每一步操做,定義很是多的變量,代碼量和工做量都相對的巨大。若是用Lambda表達式咱們看到以往數十行甚至上百行的代碼均可以濃縮成幾行甚至一行代碼。這樣處理邏輯就會相對簡單,開發效率能夠獲得明顯提升,維護工做也相對容易。

 

       Lambda表達式中的Stream

        在java 8 中 Stream 不是集合元素,它不保存數據,它是有關算法和計算的,它更像一個高級版本的 Iterator。原始版本的 Iterator,用戶只能顯式地一個一個遍歷元素並對其執行某些操做;高級版本的 Stream,用戶只要給出須要對其包含的元素執行什麼操做,好比 「過濾掉長度大於 10 的字符串」、「獲取每一個字符串的首字母」等,Stream 會隱式地在內部進行遍歷,作出相應的數據轉換。

        Stream 就如同一個迭代器(Iterator),單向,不可往復,數據只能遍歷一次,遍歷過一次後即用盡了,就比如流水從面前流過,一去不復返。而和迭代器又不一樣的是,Stream 能夠並行化操做,迭代器只能命令式地、串行化操做。顧名思義,當使用串行方式去遍歷時,每一個 item 讀完後再讀下一個 item。而使用並行去遍歷時,數據會被分紅多個段,其中每個都在不一樣的線程中處理,而後將結果一塊兒輸出。Stream 的並行操做依賴於 Java7 中引入的 Fork/Join 框架(JSR166y)來拆分任務和加速處理過程。

        Stream能夠有限的也能夠是無限的,流的構造方式有不少能夠從經常使用的Collection(List,Array,Set and so on...),文件,甚至函數....

      由值建立流:

               Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

       由數組建立流: 

               int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();       

       由文件建立流

               Stream<String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())

       上面的這些Stream都是有限的,咱們能夠用函數來建立一個無限Stream

               Stream.iterate(0, n -> n + 2).forEach(System.out::println);

        Stream也很懶惰,它只會在你真正須要數據的時候纔會把數據給傳給你,在你不須要時它一個數據都不會產生。

 

       Lambda表達式的Best Practice

        一、保持Lambda表達式簡短和一目瞭然

values.stream()
  .mapToInt(e -> {     
    int sum = 0;
    for(int i = 1; i <= e; i++) {
      if(e % i == 0) {
        sum += i;
      }
    }   
    return sum;
  })
  .sum());  //代碼複雜難懂 
values.stream()
  .mapToInt(e -> sumOfFactors(e))
  .sum() //代碼簡潔一目瞭然

  長長的Lambda表達式一般是危險的,由於代碼越長越難以讀懂,意圖看起來也不明,而且代碼也難以複用,測試難度也大。

      二、使用@FunctionalInterface 註解

         若是你肯定了某個interface是用於Lambda表達式,請必定要加上@FunctionalInterface,代表你的意圖否則未來說不定某個不知情的傢伙好比你旁邊的好基友,在這個interface上面加了另一個抽像方法時,你的代碼就悲劇了。

      三、優先使用java.util.function包下面的函數式接口

         java.util.function 這個包下面提供了大量的功能性接口,能夠知足大多數開發人員爲lambda表達式和方法引用提供目標類型的需求。每一個接口都是通用的和抽象的,使它們易於適應幾乎任何lambda表達式。開發人員應該在建立新的功能接口以前研究這個包,避免重複定義接口。另一點就是,裏面的接口不會被別人修改~。

      四、不要在Lambda表達中執行有"反作用"的操做

        "反作用"是嚴重違背函數式編程的設計原則,在工做中我常常看到有人在forEach操做裏面操做外面的某個List或者設置某個Map這實際上是不對的。

      五、不要把Lambda表達式和匿名內部類同等對待

         雖然咱們能夠用匿名內部類來實現Lambda表達式,也能夠用Lambda表達式來替換內部類,但並不表明這二者是等價的。這二者在某一個重要概念是不一樣的:this指代的上下文是不同的。當您使用內部類時,它將建立一個新的範圍。經過實例化具備相同名稱的新局部變量,能夠從封閉範圍覆蓋局部變量。您還能夠在內部類中使用這個關鍵字做爲它實例的引用。可是,lambda表達式可使用封閉範圍。您不能在lambda的主體內覆蓋範圍內的變量

private String value = "Enclosing scope value";

public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";
 
        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");
 
    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");
 
    return "Results: resultIC = " + resultIC + 
      ", resultLambda = " + resultLambda;
}

   運行上面這段代碼咱們將到 resultIC = "Inner class value",resultLambda = "Enclosing scope value"。也就是說在匿名內部類中this指的是自身的引用,在Lambda表達式中this指的是外部。

       六、多使用方法引用

       在Lambda表達式中 a -> a.toLowerCase()和String::toLowerCase都能起到相同的做用,但二者相比,後者一般可讀性更高而且代碼會簡短。

       七、儘可能避免在Lambda的方法體中使用{}代碼塊

       優先使用

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

     而不是

Foo foo = parameter -> { String result = "Something " + parameter; 
    //many lines of code 
    return result; 
};

  八、不要盲目的開啓並行流

       Lambda的並行流雖好,但也要注意使用場景。若是日常的業務處理好比過濾,提取數據,沒有涉及特別大的數據和耗時操做,則真的不須要開啓並行流。我在工做中看到有些人一個只有幾十個元素的列表的過濾操做也開啓了並行流,其實這樣作會更慢。由於多行線程的開啓和同步這些花費的時間每每比你真實的處理時間要多不少。但一些耗時的操做好比I/O訪問,DB查詢,遠程調用,這些若是能夠並行的話,則開啓並行流是可提高很大性能的。由於並行流的底層原理是fork/join,若是你的數據分塊不是很好切分,也不建議開啓並行流。舉個例子ArrayList的Stream能夠開啓並行流,而LinkedList則不建議,由於LinkedList每次作數據切分要遍歷整個鏈表,這自己就已經很浪費性能,而ArrayList則不會。

相關文章
相關標籤/搜索