Java核心教程5: 流式編程

本次課程的標(biāo)題不像之前那樣易懂,是一個(gè)陌生的概念,“流式編程”是個(gè)什么東西?

在了解流式編程之前先思考一下“流”,水流、電流、人流,這些都是流。而流式編程則是讓集合中的一個(gè)一個(gè)對象像水流一樣流動(dòng),分別進(jìn)行去重、過濾、映射等操作,就和批量化生產(chǎn)線一樣。利用流,我們無需迭代集合中的元素,就可以提取和操作它們,這些操作通常被組合在一起,在流上形成一條操作管道。

流的一個(gè)核心好處是,它使得程序更加短小并且更易理解,讓我們來看看上次作業(yè)中的“生成 50 個(gè) 1 到 100 之間的不重復(fù)的隨機(jī)數(shù)并輸出”,如果用流來解決的話代碼是怎樣的:

new Random().ints(0,100)   //生成0到100的隨機(jī)數(shù)
    .distinct()   //去除重復(fù)的值
    .limit(50)    //只取前50個(gè)數(shù)
    .forEach(System.out::println);   //對每個(gè)元素調(diào)用println函數(shù)

這顯然非常簡單和直觀,而且你甚至都不需要寫任何一句循環(huán)!讓我們趕緊進(jìn)入流式編程的樂園吧。


一、創(chuàng)建流

下面用一組代碼來展示各種創(chuàng)建流的方法:

//產(chǎn)生從0到200的隨機(jī)浮點(diǎn)數(shù)的流
DoubleStream randomStream = new Random().doubles(0, 200);

//將數(shù)組轉(zhuǎn)換成流,可以產(chǎn)生基本數(shù)據(jù)類型的流,
//如IntStream、FloatStream、DoubleStream等,運(yùn)行效率比較高
DoubleStream arrayStream= Arrays.stream(new double[]{1,3,4,5,2,11});

//根據(jù)一組對象產(chǎn)生流,但不能產(chǎn)生基本數(shù)據(jù)類型的流,只能產(chǎn)生對應(yīng)包裝類的流
Stream<String> stream = Stream.of("happy", "sad", "bad", "yes", "no");

//產(chǎn)生從0到9的整數(shù)流
IntStream rangeStream=IntStream.range(0,10);

//以第一個(gè)參數(shù)為種子,迭代產(chǎn)生后面對象的流
Stream<Integer> iterateStream = Stream.iterate(0, i->2*i);

//大部分集合都有stream()方法用來產(chǎn)對應(yīng)的流
Stream<String> collectionStream = new ArrayList<String>().stream();
Stream<String> parallelStream = new ArrayList<String>().parallelStream();
//通過parallelStream()方法可以產(chǎn)生一個(gè)并行流,Java會(huì)將操作在多個(gè)核心上運(yùn)行



二、中間操作

中間操作具體包括去重、過濾、映射等操作,值得說明的是,在執(zhí)行中間操作的代碼的時(shí)候并不會(huì)執(zhí)行這些操作,而只會(huì)把這些操作保存在流里面,每次中間操作都會(huì)產(chǎn)生一個(gè)新的流對象,保存從開始到現(xiàn)在要進(jìn)行的所有操作序列,在執(zhí)行結(jié)束操作的時(shí)候才會(huì)真正執(zhí)行這些操作,這叫做懶加載。

跟蹤和調(diào)試

peek() 操作的目的是幫助調(diào)試。它允許你無修改地查看流中的元素。代碼示例:

// streams/Peeking.java
class Peeking {
    public static void main(String[] args) throws Exception {
        Stream.of("Will I eat the apple?".split(" "))
        .map(w -> w + " ")
        .peek(System.out::print)
        .map(String::toUpperCase)
        .peek(System.out::print)
        .map(String::toLowerCase)
        .forEach(System.out::print);
    }
}

輸出結(jié)果:

Will WILL will I I i eat EAT eat the THE the apple APPLE apple

可以看出流是對每個(gè)元素都分別進(jìn)行同樣的操作,就和流水線一樣。

流元素排序

在最開始的代碼中,我們熟識了 sorted() 的無參數(shù)方法。其實(shí)它還有另一種形式的實(shí)現(xiàn):傳入一個(gè) Comparator 參數(shù)。代碼示例:

// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
    public static void main(String[] args) throws Exception {
        //PS:這里的FileToWords是一個(gè)可以將文本文件轉(zhuǎn)換成單詞流的類
        FileToWords.stream("Cheese.dat")
        .skip(10)
        .limit(10)
        .sorted(Comparator.reverseOrder())
        .map(w -> w + " ")
        .forEach(System.out::print);
    }
}

輸出結(jié)果:

you what to the that sir leads in district And

sorted() 預(yù)設(shè)了一些默認(rèn)的比較器。這里我們使用的是反轉(zhuǎn)“自然排序”。你當(dāng)然也可以把 Lambda 函數(shù)作為參數(shù)傳遞給 sorted()。

移除元素

  • distinct():最開始的代碼中的 distinct() 可用于消除流中的重復(fù)元素。相比創(chuàng)建一個(gè) Set 集合,該方法的效率要高很多。
  • filter(Predicate):過濾操作則會(huì)留下使過濾器方法返回值為 true的元素。

在下例中,isPrime() 作為過濾器函數(shù),用于檢測質(zhì)數(shù)。

import java.util.stream.*;

public class Prime {
    public static Boolean isPrime(long n) {
        return LongStream.rangeClosed(2, (long)Math.sqrt(n))
            .noneMatch(i -> n % i == 0);
            //如果流中所有元素調(diào)用上述方法都返回false,則noneMatch()返回true
    }
    
    public LongStream numbers() {
        return LongStream.iterate(2, i -> i + 1)
            .filter(Prime::isPrime);
    }
    
    public static void main(String[] args) {
        new Prime().numbers()
            .limit(10)
            .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
            .skip(90)
            .limit(10)
            .forEach(n -> System.out.format("%d ", n));
    }
}

輸出結(jié)果:

2 3 5 7 11 13 17 19 23 29
467 479 487 491 499 503 509 521 523 541

range()是左閉右開區(qū)間不同,rangeClosed() 是閉區(qū)間,左右的值都包括。如果不能整除,即余數(shù)不等于 0,則 noneMatch() 操作返回 true,如果出現(xiàn)任何等于 0 的結(jié)果則返回 false

應(yīng)用函數(shù)到元素

  • map(Function):將原來流中的每個(gè)元素都調(diào)用參數(shù)里的方法,其返回值匯總起來產(chǎn)生一個(gè)新的流。
  • mapToInt(ToIntFunction):操作同上,但結(jié)果是IntStream。
  • mapToLong(ToLongFunction):操作同上,但結(jié)果是 LongStream。
  • mapToDouble(ToDoubleFunction):操作同上,但結(jié)果是 DoubleStream。

之前的代碼中就多次用到了map方法,只需要知道它可以將流里的所有元素都變成與其對應(yīng)的新元素就可以了,這里就不進(jìn)行代碼展示了。

最后需要注意的一點(diǎn)是,同一個(gè)流只能進(jìn)行一次操作,例如下面的代碼就會(huì)報(bào)錯(cuò):

//以第一個(gè)參數(shù)為種子,迭代產(chǎn)生后面對象的流
Stream<Integer> iterateStream= Stream.iterate(0, i->2*i).limit(10);
iterateStream.map(Integer::doubleValue);
iterateStream.map(Integer::byteValue);  //這句語句會(huì)報(bào)錯(cuò)



三、結(jié)束操作

這些操作接收一個(gè)流并產(chǎn)生一個(gè)最終結(jié)果;它們不會(huì)向后面的流提供任何東西。因此,結(jié)束操作總是你在管道中做的最后一個(gè)操作。

轉(zhuǎn)化為數(shù)組

  • toArray():將流轉(zhuǎn)換成適當(dāng)類型的數(shù)組。
  • toArray(generator):在特殊情況下,生成器用于分配自定義的數(shù)組存儲。

遍歷元素

  • forEach(Consumer):你已經(jīng)看到過很多次 System.out::println 作為 Consumer 函數(shù)。
  • forEachOrdered(Consumer): 確保按照原始流的順序執(zhí)行。

看著這兩種形式,似乎forEach方法并不會(huì)按順序輸出,但其實(shí)在沒有調(diào)用parallel()方法之前這兩個(gè)方法的輸出結(jié)果都是一樣的。

這里稍微簡單介紹下 parallel():可實(shí)現(xiàn)多處理器并行操作。實(shí)現(xiàn)原理是將流分割為多個(gè)(通常數(shù)目為 CPU 核心數(shù))并在不同處理器上分別執(zhí)行操作。而進(jìn)行并行操作的時(shí)候,forEach操作無法保證元素按原來的順序輸出,而forEachOrdered則可以確保按原來的順序輸出。

收集

  • collect(Collector):使用 Collector 收集流元素到結(jié)果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):收集流元素到結(jié)果集合中,第一個(gè)參數(shù)用于創(chuàng)建一個(gè)新的結(jié)果集合,第二個(gè)參數(shù)用于將下一個(gè)元素加入到現(xiàn)有結(jié)果合集中,第三個(gè)參數(shù)用于將兩個(gè)結(jié)果合集合并。

第一種形式中的的Collector參數(shù),Java核心庫為我們提供了很多Collector實(shí)現(xiàn)類,都在Collectors這個(gè)工具類里面,例如Colletors.toList、Collectos.toMap、Collections.toCollection等等,基本上都是故名思義,就不介紹了。當(dāng)然也可以自己寫寫一個(gè)類來繼承自Collector來實(shí)現(xiàn)自定義的收集要求

但對于自定義的元素收集要求,最好的辦法還是采用第二種形式,讓我們來看一個(gè)示例代碼:

import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
    public static void main(String[] args) throws Exception {
        ArrayList<String> words = FileToWords.stream("Cheese.dat")
            .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        words.stream()
            .filter(s -> s.equals("cheese"))
            .forEach(System.out::println);
    }
}

匹配

  • allMatch(Predicate) :如果流的每個(gè)元素根據(jù)提供的 Predicate 都返回 true 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) false 之后短路,也就是不會(huì)在發(fā)生 false 之后繼續(xù)執(zhí)行計(jì)算。
  • anyMatch(Predicate):如果流中的任意一個(gè)元素根據(jù)提供的 Predicate 返回 true 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) true 之后短路,也就是不會(huì)在發(fā)生 true 之后繼續(xù)執(zhí)行計(jì)算。
  • noneMatch(Predicate):如果流的每個(gè)元素根據(jù)提供的 Predicate 都返回 false 時(shí),最終結(jié)果返回為 true。這個(gè)操作將會(huì)在第一個(gè) true 之后短路,也就是不會(huì)在發(fā)生 true 之后繼續(xù)執(zhí)行計(jì)算。

元素查找

  • findFirst():返回一個(gè)含有第一個(gè)流元素的 Optional類型的對象,如果流為空返回 Optional.empty。
  • findAny():返回含有任意流元素的 Optional類型的對象,如果流為空返回 Optional.empty

findFirst() 無論流是否為并行化的,總是會(huì)選擇流中的第一個(gè)元素。對于非并行流,findAny()會(huì)選擇流中的第一個(gè)元素(但從定義上來看是選擇任意元素)。

統(tǒng)計(jì)信息

  • average() :求取流元素平均值。
  • max()min():求元素的最大值和最小值,對于非數(shù)字流則要多一個(gè)Comparator參數(shù)。
  • sum():對所有流元素進(jìn)行求和。
  • count():流中的元素個(gè)數(shù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容