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


  在這篇“Java 8新特性教程”系列文章中,我們會深入解釋,並通過代碼來展示,如何通過流來遍歷集合,如何從集合和數組來創建流,以及怎么聚合流的值。

在之前的文章“遍歷、過濾、處理集合及使用Lambda表達式增強方法”中,我已經深入解釋並演示了通過lambda表達式和方法引用來遍歷集合,使用predicate接口來過濾集合,實現接口的默認方法,最后還演示了接口靜態方法的實現。

源代碼都在我的Github上:可以從 這里克隆。

內容列表

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

1. 使用流來遍歷集合

簡介:

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 系統。我可以給你的唯一建議是測試你的應用和數據。建立一個基准的、計時的操作。然后分別使用串行流和並行流,看哪一個更適合於你。

2、從集合或數組創建流

簡介

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 表達式、過濾、方法引用等功能了。

3、聚合流的值

簡介

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

如何工作?

我會在 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使用指南


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM