入門lambda表達式(二)

引文

  此次主要介紹Java 8的Stream以及如何與lambda配合使用。Stream做爲Java 8的一大亮點,它與java.io包裏的InputStream和OutputStream是徹底不一樣的概念。Java 8中的Stream是對集合對象功能的加強,它專一於對集合對象進行各類很是便利、高效的聚合操做,或者大批量數據操做。Stream API藉助於一樣新出現的lambda表達式,極大的提升編程效率和程序可讀性。能夠說,Stream的出現,徹底改變了處理集合的方式。但願你們在看完這篇文章後,能拋棄以前對集合用Iterator遍歷並完成相關的聚合操做那種笨拙的方式,改用流來處理。
  先用一個例子讓你們感覺一下Stream的便捷。假設有這樣一個Book類:java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {

    private Integer id;

    private String name;

    private String type;

    private Double price;
}
複製代碼

  如今有這樣一個業務場景:要發現種類爲「計算機」的全部圖書,而後返回以價格增序排序好的圖書ID集合:
傳統方式算法

public class StreamTest {

    public static void main(String[] args) {
        List<Book> bookList = new ArrayList<>();
        bookList.addAll(Arrays.asList(new Book(1, "Java核心技術", "計算機", 90.0),
                new Book(2, "Java編程思想", "計算機", 100.0),
                new Book(3, "浮生六記", "文學", 50.0)));
        List<Book> computerBooks = new ArrayList<>();
        for (Book b : bookList) {
            if (b.getType().equals("計算機")) {
                computerBooks.add(b);
            }
        }
        Collections.sort(computerBooks, new Comparator<Book>() {
            @Override
            public int compare(Book b1, Book b2) {
                return b1.getPrice().compareTo(b2.getPrice());
            }
        });
        List<Integer> bookIds = new ArrayList<>();
        for (Book b : computerBooks) {
            bookIds.add(b.getId());
        }
    }
}
複製代碼

使用Stream編程

public class StreamTest {

    public static void main(String[] args) {
        List<Book> bookList = new ArrayList<>();
        bookList.addAll(Arrays.asList(new Book(1, "Java核心技術", "計算機", 90.0),
                new Book(2, "Java編程思想", "計算機", 100.0),
                new Book(3, "浮生六記", "文學", 50.0)));
        List<Integer> bookIds = bookList.stream()
                .filter(b -> b.getType().equals("計算機"))
                .sorted(Comparator.comparing(Book::getPrice))
                .map(Book::getId)
                .collect(Collectors.toList());
    }
}
複製代碼

  能夠看到,原先繁瑣的操做,在使用了Stream後,只用一句話就解決了。那Stream到底是什麼呢?數據結構

Stream原理

  Stream不是集合元素或者數據結構,它並不保存數據,而是有關數據的算法和計算的。能夠把Stream理解成一個高級版本的Iterator。原始版本的Iterator,用戶只能顯式地一個一個遍歷元素並對其執行某些操做;高級版本的Stream,用戶只要給出須要對其包含的元素執行什麼操做,好比 「過濾掉長度大於10的字符串」、「獲取每一個字符串的首字母」等,Stream會隱式地在內部進行遍歷,作出相應的數據轉換(ps:也能夠把Stream理解成一種處理數據的風格,這種風格將要處理的元素集合看做一種流,流在管道中傳輸,而且能夠在管道的節點上進行處理,好比篩選、排序、聚合等)。
  可能上面說的有些抽象,下面給出一個具體的例子。有這樣一段代碼:多線程

List<Integer> nums = new ArrayList<>();
nums.addAll(Arrays.asList(1, null, 3, 4, null, 6));
nums.stream().filter(num -> num != null)).count();
複製代碼

  上面這段代碼的目的是獲取一個List中不爲null的元素的個數。經過這段代碼,咱們來剖析一下Stream的結構: 併發

圖片未加載成功
  上圖中三個方框,也就是咱們使用Stream的三個基本步驟。紅色框中的語句負責建立一個Stream實例;綠色框中的語句對集合元素進行數據轉換,每次轉換原有Stream對象不改變,返回一個新的Stream對象(能夠有屢次轉換),這就容許對其操做能夠像鏈條同樣排列,變成一個管道;藍色框中的語句把Stream裏包含的內容按照某種算法來匯聚成一個值。再形象一點,就是這樣的:
圖片未加載成功

  下面,我具體說一下這三個步驟:
  建立Stream有不少方法,你們感興趣的話能夠自行去查。這裏我重點說一下如何把一個Collection對象轉換成Stream。一般狀況下,調用Collection.stream()和Collection.parallelStream()分別產生序列化流(普通流)和並行流。並行和併發的概念,你們應該都清楚。併發是指多線程有競爭關係,在單核的狀況下某一時刻只有一個線程運行;而並行是指在多核的狀況下同時運行,單核談並行是無心義的。使用並行方式去遍歷時,數據會被分紅多個段,其中每個都在不一樣的線程中處理,而後將結果一塊兒輸出。Stream的並行操做依賴於Java 7中引入的Fork/Join框架來拆分任務和加速處理過程。須要注意的是,並行不必定快,尤爲在數據量很小的狀況下,可能比普通流更慢。只有在大數據量和多核的狀況下才考慮並行流。
  流的操做類型分爲兩種:

  • 中間操做(Intermediate operation):一個流能夠後面跟隨零個或多個intermediate操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法,並無真正開始流的遍歷。三個基本步驟中的轉換屬於中間操做,用於把一個Stream經過某些行爲轉換成一個新的Stream。
  • 最終操做(Terminal Operation):一個流只能有一個terminal操做,當這個操做執行後,流就被使用「光」了,沒法再被操做,因此這一定是流的最後一個操做。Terminal操做的執行,纔會真正開始流的遍歷,而且會生成一個結果。三個基本步驟中的聚合屬於最終操做,用於接受一個元素序列做爲輸入,反覆使用某個合併操做,把序列中的元素合併成一個彙總的結果。好比查找一個數字列表的總和或者最大值,或者把這些數合併成一個List對象。

  須要強調的是,在對一個Stream進行屢次Intermediate操做時,每次都對Stream的每一個元素進行轉換,但實質上並無作N(轉換次數)次for循環。轉換操做都是lazy的,多個轉換操做只會在Terminal操做的時候融合起來,一次循環完成。咱們能夠這樣簡單的理解,Stream裏有個操做函數的集合,每次轉換操做就是把轉換函數放入這個集合中,在Terminal操做的時候循環Stream對應的集合,而後對每一個元素執行全部的函數。
  至於有哪些轉換和聚合方法呢,你們能夠自行查找。推薦一篇文章:ifeve.com/stream/ ,裏面用示意圖說明了幾個典型的轉換和聚合方法,很形象。框架

Stream用法示例

map/flatMap

  map的做用是把input Stream的每個元素,映射成output Stream的另一個元素,例如:ide

// 大小寫轉換
List<String> wordList = Arrays.asList("a", "b", "c");
List<String> newWorldList = wordList.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());
複製代碼

  map生成的是1:1映射,每一個輸入元素,都按照規則轉換成爲另一個元素。還有一些場景,是一對多映射關係的,這時須要flatMap:函數

Stream<List<Integer>> inputStream = Stream.of(
        Arrays.asList(1),
        Arrays.asList(2, 3),
        Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream
        .flatMap((childList) -> childList.stream());
複製代碼

  flatMap把input Stream中的層級結構扁平化,就是將最底層元素抽出來放到一塊兒。最終output Stream裏面已經沒有List了,都是直接的數字。大數據

reduce

  這個方法的主要做用是把Stream元素組合起來。它提供一個起始值,而後依照運算規則(BinaryOperator),和前面Stream的第一個、第二個、第n個元素組合。從這個意義上說,字符串拼接、數值的sum、min、max、average都是特殊的reduce。例如Stream的sum就至關於如下兩種寫法:

Integer sum = integers.reduce(0, (a, b) -> a+b);   
Integer sum = integers.reduce(0, Integer::sum);
複製代碼

  reduce的用例以下:

// 字符串鏈接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 無起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 過濾、字符串鏈接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F")
        .filter(x -> x.compareTo("Z") > 0)
        .reduce("", String::concat);
複製代碼

match

  Stream有三個match方法,從語義上說:

  • allMatch:Stream中所有元素符合傳入的predicate,返回true
  • anyMatch:Stream中只要有一個元素符合傳入的predicate,返回true
  • noneMatch:Stream中沒有一個元素符合傳入的predicate,返回true

  它們都不是要遍歷所有元素才能返回結果。例如allMatch只要一個元素不知足條件,就skip剩下的全部元素,返回false。match的用例以下:

List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream()
        .allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream()
        .anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);
複製代碼

  emmm就先寫這幾個吧,其實Stream另外一個便捷的地方在於它的一些方法能夠直接根據方法名來判斷用途。固然,想要掌握Stream的用法,仍是要——多用。
  最後,說幾個lambda表達式須要注意的地方:

  • lambda表達式內可使用方法引用(即那個「::」),僅當該方法不修改lambda表達式提供的參數。然而,若對參數有任何修改,則不能使用方法引用,而需鍵入完整的lambda表達式。
  • lambda內部可使用靜態、非靜態和局部變量,但只能是final的。這就是說不能在lambda內部修改定義在域外的變量(ps:Java 8對這個限制作了優化,能夠不用顯式使用final修飾,可是編譯器隱式當成final來處理)。
  • 上次說到能夠用lambda表達式來代替匿名內部類,但這二者仍是有明顯區別的:一是this關鍵字,匿名類的this關鍵字指向匿名類,而lambda表達式的this關鍵字指向包圍lambda表達式的類;二是編譯方式不一樣,Java編譯器將lambda表達式編譯成類的私有方法。

  初識lambda表達式,可能有的人還會以爲很陌生,可是在掌握它的用法以後,必定能感覺到它的強大之處!

相關文章
相關標籤/搜索