java8新特性,使用流遍歷集合

  在這篇「Java 8新特性教程」系列文章中,咱們會深刻解釋,並經過代碼來展現,如何經過流來遍歷集合,如何從集合和數組來建立流,以及怎麼聚合流的值。html

在以前的文章「遍歷、過濾、處理集合及使用Lambda表達式加強方法」中,我已經深刻解釋並演示了經過lambda表達式和方法引用來遍歷集合,使用predicate接口來過濾集合,實現接口的默認方法,最後還演示了接口靜態方法的實現。java

源代碼都在個人Github上:能夠從 這裏克隆。git

內容列表github

  • 使用流來遍歷集合。
  • 從集合或數組建立流。
  • 聚合流中的值。

1. 使用流來遍歷集合

簡介:api

Java的集合框架,如List和Map接口及Arraylist和HashMap類,讓咱們很容易地管理有序和無序集合。集合框架自引入的第一天起就在 持續的改進。在Java SE 8中,咱們能夠經過流的API來管理、遍歷和聚合集合。一個基於流的集合與輸入輸出流是不一樣的。數組

如何工做?框架

它採用一種全新的方式,將數據做爲一個總體,而不是單獨的個體來處理。當你使用流時,你不須要關心循環或遍歷的細節。你能夠直接從一個集合建立一個流。然 後你就能用這個流來許多事件了,如遍歷、過濾及聚和。我將從項目 Java8Features 的 com.tm.java8.features.stream.traversing 包下的例子開始。代碼在一個SequentialStream 類中,Java SE 8 中有兩種集合流,即串行流和並行流。性能

List<person> people = new ArrayList<>();

people.add(new Person("Mohamed", 69));
people.add(new Person("Doaa", 25));
people.add(new Person("Malik", 6));

Predicate<person> pred = (p) -> p.getAge() > 65;

displayPeople(people, pred);

...........

private static void displayPeople(List<person> people, Predicate<person> pred) {

     System.out.println("Selected:");
     people.forEach(p -> {
         if (pred.test(p)) {
             System.out.println(p.getName());
         }
     });
}

在這兩種流中,串行流相對比較簡單,它相似一個迭代器,每次處理集合中的一個元素。可是語法與之前不一樣。在這段代碼中,我建立了 pepole 的數組列表,向上轉型爲List。它包含三個 Person 類的實例。而後咱們使用 Predicate 聲明一個條件,只有知足這個條件的 people 纔會顯示。在 displayPeople() 方法的48到52行循環遍歷該集合,挨個測試其中的每一項。運行這段代碼,你將得到以下的結果:測試

Selected:
Mohamed

我將會展現如何使用流來重構這段代碼。首先,我註釋了這段代碼。而後,在這段註釋的代碼下,我開始使用集合對象 people。而後我調用一個 stream() 方法。一個stream對象,相似集合,也要聲明泛型。若是你從一個集合獲取流,則該流中每一項的類型與集合自己是一致的。個人集合是 Person 類的實例,因此流中也使用一樣的泛型類型。設計

System.out.println("Selected:");
 //        people.forEach(p -> {
 //            if (pred.test(p)) {
 //                System.out.println(p.getName());
 //            }
 //        });

  people.stream().forEach(p -> System.out.println(p.getName()));
}

你能夠調用一個 stream() 方法來得到了一個流對象,而後能夠在該對象上進行一些操做。我簡單地調用了 forEach 方法,該方法須要一個Lamda表達式。我在參數中傳遞了一個Lamda表達式。列表中的每一項就是經過迭代器處理的每一項。處理過程是經過Lambda 操做符和方法實現來完成的。我簡單使用system output來輸出每一個人的名稱。保存並運行這段代碼,輸出結果以下。由於沒有過濾,因此輸出了列表中全部元素。

Selected:
Mohamed
Doaa
Malik

如今,一旦有了一個流對象,就能夠很容易使用 predicate 對象了。當使用 for each 方法處理每一項時,我不得不顯示調用 predicate 的 test 方法,可是使用流時,你能夠調用一個名爲 filter 的方法。該方法接收一個 predicate 對象,全部的 predicate 對象都有一個 test 方法,因此它已經知道怎樣去調用該方法。因此,我對該代碼作一點改動。我將.forEach()方法下移了兩行,而後在中間的空白行,我調用了 filter 方法。

people.stream()
     .filter(pred)
     .forEach(p -> System.out.println(p.getName()));

filter方法接收一個 predicate 接口的實例對象。我將 predicate 對象傳進去。filtr 方法返回一個過濾後的流對象,在這個對象上我就能夠去調用forEach()方法了。我運行這段代碼,此次我只顯示集合中知足預約義條件的項了。你能夠在 流對象上作更多的事情。去看看 Java SE 8 API 中流的doc文檔吧。

Selected:
Mohamed

你將會看到除了過濾,你還能夠作聚合、排序等其餘的事情。在我總結這段演示以前,我想向大家展現一下串行流和並行流以前的重要區別。Java SE 8 的一個重要目標就是改善多 CPU 系統的處理能力。Java 可在運行期自動協調多個 CPU 的運行。你須要作的全部事情僅僅是將串行流轉換爲並行流。

從語法上講,有兩種方法來實現流的轉換。我複製一份串行流類。在包視圖窗口,我複製並粘貼該類,而後對它重命名,ParallelStream,打開這個 新的類。在這個版本中,刪除了註釋的代碼。我再也不須要這些註釋了。如今就能夠經過兩種方式建立並行流。第一種方式是調用集合中的 parallelStream()方法。如今我就擁有一個能夠自動分配處理器的流了。

private static void displayPeople(List<person> people, Predicate<person> pred) {
     System.out.println("Selected:");
     people.parallelStream()
             .filter(pred)
             .forEach(p -> System.out.println(p.getName()));
 }

運行這段代碼,就能夠看到徹底一致的結果,過濾而後返回數據。

Selected:
Mohamed

第二種建立並行流的方式。再次調用 stream() 方法,而後在 stream 方法的基礎上調用 parallel() 方法,其本質上作的事情是同樣的。開始是一個串行的流,而後再將其轉換爲並行流。可是它仍然是一個流。能夠過濾,能夠用以前的同樣方式去處理。只是如今的 流能夠分解到多個處理起來處理。

people.stream()
      .parallel()
      .filter(pred)
      .forEach(p -> System.out.println(p.getName()));

總結

如今尚未一個明確的規定來講明在什麼狀況下並行流優於串行流。這個依賴於數據的大小和複雜性以及硬件的處理能力。還有你運行的多 CPU 系統。我能夠給你的惟一建議是測試你的應用和數據。創建一個基準的、計時的操做。而後分別使用串行流和並行流,看哪個更適合於你。

二、從集合或數組建立流

簡介

Java SE 8’s stream API 是爲了幫助管理數據集合而設計的,這些對象是指集合框架中的對象,例如數組列表或哈希表。可是,你也能夠直接從數組建立流。

如何工做?

在 Java8Features 項目中的 eg.com.tm.java8.features.stream.creating 包下,我建立了一個名爲ArrayToStream的類。在這個類的 main 方法中,我建立了一個包含三個元素的數組。每一個元素都是Person類的一個實例對象。

public static void main(String args[]) {

    Person[] people = {
        new Person("Mohamed", 69),
        new Person("Doaa", 25),
        new Person("Malik", 6)};
    for (int i = 0; i < people.length; i++) {
        System.out.println(people[i].getInfo());
    }
}

該類中爲私有成員建立了 setters 和 getters 方法,以及 getInfo() 方法,該方法返回一個拼接的字符串。

public String getInfo() {
    return name + " (" + age + ")";
}

如今,若是想使用流來處理這個數組,你可能認爲須要先將數組轉爲數組列表,而後從這個列表建立流。可是,實際上你能夠有兩種方式直接從數組建立流。第一方式,我不須要處理數據的那三行代碼,因此先註釋掉。而後,在這個下面,我聲明一個流類型的對象。

Stream 是 java.util.stream 下的一個接口。當我按下 Ctrl+Space 並選取它的時候,會提示元素的泛型,這就是流管理的類型。在這裏,元素的類型即爲Person,與數組元素自己的類型是一致的。我將我新的流對象命名爲 stream,全部的字母都是小寫的。這就是第一種建立流的方法,使用流的接口,調用 of() 方法。注意,該方法存在兩個不一樣版本。

第一個是須要單個對象,第二個是須要多個對象。我使用一個參數的方法,因此傳遞一個名爲 people 的數組,這就是我須要作的全部事情。Stream.of() 意思就是傳入一個數組,而後將該數組包裝在流中。如今,我就可使用 lambda 表達式、過濾、方法引用等流對象的方法。我將調用流的 for each 方法,並傳入一個 lambda 表達式,將當前的 person 對象和 lambda 操做符後傳入後,就能獲取到 person 對象的信息。該信息是經過對象的 getInfo() 方法獲取到的。

Person[] people = {
        new Person("Mohamed", 69),
        new Person("Doaa", 25),
        new Person("Malik", 6)};

//        for (int i = 0; i < people.length; i++) {
//            System.out.println(people[i].getInfo());
//        }
        Stream<Person> stream = Stream.of(people);
        stream.forEach(p -> System.out.println(p.getInfo()));

保存並運行這段代碼,就可獲取到結果。輸出的元素的順序與我放入的順序是一致的。這就是第一種方式:使用 Stream.of() 方法。

Mohamed (69)
Doaa (25)
Malik (6)

另外一種方式與上面的方式其實是相同的。複製上面的代碼,並註釋掉第一種方式。此次不使用 Stream.of() 方法,咱們使用名爲 Arrays 的類,該類位於 java.util 包下。在這個類上,能夠調用名爲 stream 的方法。注意,stream 方法能夠包裝各類類型的數組,包括基本類型和複合類型。

//      Stream<person> stream = Stream.of(people);

        Stream<person> stream = Arrays.stream(people);
        stream.forEach(p -> System.out.println(p.getInfo()));

保存並運行上面的代碼,流完成的事情與以前實質上是一致的。

Mohamed (69)
Doaa (25)
Malik (6)

結論

因此,不管是 Stream.of() 仍是 Arrays.stream(),所作的事情實質上是同樣的。都是從一個基本類型或者複合對象類型的數組轉換爲流對象,而後就可使用 lambda 表達式、過濾、方法引用等功能了。

三、聚合流的值

簡介

以前,我已經描述過怎麼使用一個流來迭代一個集合。你也可使用流來聚合集合中的每一項。如計算總和、平均值、總數等等。當你作這些操做的時候,弄明白並行流特性就很是重要。

如何工做?

我會在 Java8Features 項目的 eg.com.tm.java8.features.stream.aggregating 包下進行演示。首先咱們使用 ParallelStreams 類。在這個類的 main 方法中,我建立了一個包含字符串元素的數組列表。我簡單地使用循環在列表中添加了10000個元素。而後在35和36行,我建立了一個流對象,並經過 for each 方法挨個輸出流中每一項。

public static void main(String args[]) {

    System.out.println("Creating list");
    List<string> strings = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        strings.add("Item " + i);
    }
    strings.stream()
           .forEach(str -> System.out.println(str));
}

運行這段代碼後,就得到了一個我所預期的結果。在屏幕上輸出的順序與添加到列表中的順序是一致的。

.........
Item 9982
Item 9983
Item 9984
Item 9985
Item 9986
Item 9987
Item 9988
Item 9989
Item 9990
Item 9991
Item 9992
Item 9993
Item 9994
Item 9995
Item 9996
Item 9997
Item 9998
Item 9999

如今,讓咱們看一下當轉換成並行流後會發生什麼。正如我以前所描述的,我便可以調用parallelStream方法,也能夠在流上調用parallel方法。

我將採用第二種方法。如今,我就可使用並行流了,該流能夠根據負載分配到多個處理器來處理。

strings.stream()
       .parallel()
       .forEach(str -> System.out.println(str));

再次運行該段代碼,而後觀察會發生什麼。注意,如今最後打印的元素不是列表中最後一個元素,最後一個元素應該是9999。若是我滾動輸出結果,就能發現處理過程以某種方式在循環跳動。這是由於在運行時將數據劃分紅了多個塊。

.........
Item 5292
Item 5293
Item 5294
Item 5295
Item 5296
Item 5297
Item 5298
Item 5299
Item 5300
Item 5301
Item 5302
Item 5303
Item 5304
Item 5305
Item 5306
Item 5307
Item 5308
Item 5309
Item 5310
Item 5311

而後,將數據塊分配給合適的處理器去處理。只有當全部塊都處理完成了,纔會執行以後的代碼。本質上講,這是在調用 forEach() 方法時,將整個過程是根據須要來進行劃分了。如今,這麼作可能會提升性能,也可能不會。這依賴於數據集的大小以及你硬件的性能。經過這個例子,也能夠看 出,若是須要按照添加的順序挨個處理每一項,那麼並行流可能就不合適了。

串行流能保證每次運行的順序是一致的。但並行流,從定義上講,是一種更有效率的方式。因此並行流在聚合操做的時候很是有效。很適合將集合做爲一個總體考慮,而後在該集合上進行一些聚合操做的狀況。我將會經過一個例子來演示集合元素的計數、求平均值及求和操做。

咱們在這個類的 main 方法中來計數,開始仍是用相同的基礎代碼。建立10,000個字符串的列表。而後經過一個 for each 方法循環處理每一項。

public static void main(String args[]) {

    System.out.println("Creating list");
    List<string> strings = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        strings.add("Item " + i);
    }
    strings.stream()
           .forEach(str -> System.out.println(str));
}

在這個例子中,我想直接對集合元素進行計數,而不是挨個來處理。因此,我註釋掉原來的代碼,使用下面的代碼。由於不能準確的知道該集合到底有多少個元素。因此我使用長整型變量來存儲結果。

我將這個變量命名爲count,經過調用集合strings的.stream(), .count()方法,返回一個長整型的值。而後將這個值與「count:」拼接起來,再經過system的output來打印。

//      strings.stream()
//             .forEach(str -> System.out.println(str));
        long count = strings.stream().count();
        System.out.println("Count: " + count);

保存並運行該段代碼,下面是輸出結果。集合中元素數量的統計幾乎是瞬間完成。

Creating list
Count: 10000

如今對上面的代碼作一點小小的改動,增長兩個0。如今,開始處理1000,000個字符串。我再次運行這段代碼,也很快就返回結果了。

Creating list
Count: 1000000

如今,我使用並行流來處理,看會發生什麼。我在下面增長 parallel 方法:

//      strings.stream()
//             .forEach(str -> System.out.println(str));
        long count = strings.stream().parallel().count();
        System.out.println("Count: " + count);

而後我運行這段代碼,發現花費的時間更長一點了。如今,我作一個基準測試,經過抓取操做先後的時間戳來觀察發生了什麼。而後作一點數學的事情。不一樣的系統 上,獲得的結果可能不一樣。可是根據個人經驗來講,這種包含簡單類型的簡單集合,使用並行流並無太多的優點。不過,我仍是鼓勵你去本身作基準測試,雖然有 點麻煩。 不過這也要你是如何去作的。

再讓咱們看一下求和及求均值。我將使用 SumAndAverage 類。此次,我有一個包含三個 person 對象的列表,每一個 person 對象的有不一樣的年齡值。個人目的是求三個年齡的和及年齡的平均值。我在全部的 person 對象都加入到列表以後加入了一行新的代碼。而後,我建立了一個名爲sum的整型變量。

首先,我經過 pepole.stream() 方法獲取一個流。在這個流基礎上,我能夠調用 mapToInt() 方法。注意,還有兩個相似的 Map Method:mapToDouble() 和 mapToLong()。這些方法的目的就是,從複合類型中獲取簡單的基本類型數據,建立流對象。你能夠用 lambda 表達式來完成這項工做。因此,我選擇 mapToInt() 方法,由於每一個人的年齡都是整數。

關於 Lambda 表達式,開始是一個表明當前 person 的變量。而後,經過 Lambda 操做符和 Lambda 表達式(p.getAge())返回一個整數。這種返回值,咱們有時也叫作int字符串。也能夠返回double字符串或其它類型。如今,因爲已經知道它 是一個數字類型的值,因此我能夠調用 sum() 方法。如今,我就已經將全部集合中 person 對象的年齡值所有加起來了。經過一條語句,我就能夠用 System Output 來輸出結果了。我將求和的結果與「Total of ages」鏈接在一塊兒輸出。

List<person> people = new ArrayList<>();
        people.add(new Person("Mohamed", 69));
        people.add(new Person("Doaa", 25));
        people.add(new Person("Malik", 6));

        int sum = people.stream()
                  .mapToInt(p -> p.getAge())
                  .sum();
        System.out.println("Total of ages " + sum);

保存並運行上面的代碼。三個年齡的總和是100。

Total of ages 100

求這些值的平均值很是相似。可是,求平均值須要作除法操做,因此須要考慮除數爲0的問題,所以,當你求平均值的時候,能夠返回一個Optional的變量。

你可使用多種數據類型。在計算平均值的時候,我想得到一個 doule 類型的值。因此,我建立了一個 OptionalDouble 類型的變量。注意,還存在 Optional Int 和 Optional Long。我將平均值命名爲 avg,使用的代碼與求和的代碼也是一致的,開始用 people.stream()。在這個基礎上,再次使用 mapToInt()。而且傳遞了相同的 lambda 表達式,最後,調用 average 方法。

如今,得到了一個OptionalDouble類型的變量。在處理這個變量前,你能夠經過 isPresent() 來確保它確實是一個double值。因此,我使用了一段 if/else 的模板代碼來處理。斷定的條件是 avg.isPresent()。若是條件爲真,就使用 System Output 輸出「Average」標籤和平均值。在 else 子句中,我簡單地打印「average wasn’t calculated」。

OptionalDouble avg = people.stream()
                .mapToInt(p -> p.getAge())
                .average();
if (avg.isPresent()) {
    System.out.println("Average: " + avg);
} else {
    System.out.println("average wasn't calculated");
}

如今,在這個例子中,我知道能成功,由於我給三我的的年齡都賦值了。可是,狀況不老是這樣的。正如我前面說的,存在除0的狀況,這時你就不能獲取到一個 double 類型返回值。我保存並運行這段代碼,請注意 optional double 類,它是一個複合對象。

Total of ages 100
Average: OptionalDouble[33.333333333333336]

因此,真實的值被包含在該類型中,回到這段代碼,直接引用該對象,並調用 getAsDouble() 方法。

if (avg.isPresent()) {
    System.out.println("Average: " + avg.getAsDouble());
} else {
    System.out.println("average wasn't calculated");
}

如今,我就能夠得到 double 類型的值。我再次運行這段代碼,輸出結果以下:

Total of ages 100
Average: 33.333333333333336

結論

經過流和 lambda 表達式,你能夠用很是很是少的代碼就能夠完成集合的聚合計算。

關於Stream API,您也能夠閱讀這篇文章:Java 8中的Stream API使用指南

相關文章
相關標籤/搜索