java8系列-04 Stream

image

Stream英文直譯為“流”,其實這里也是對文件流、集合流等各種流的操作。(雖然我們最常用在Collection集合中進(jìn)行操作,但它的應(yīng)用可是很廣泛的)它是java8中最亮眼的特性之一,能極大的方便我們對于數(shù)據(jù)的操作,讓程序員們能寫出非常高效率又干凈的代碼。下面我們先看一個例子,從例子中進(jìn)行初步認(rèn)識,然后再帶大家慢慢的來揭露它的神秘面紗。

Stream基礎(chǔ)

先來感受一下Stream的神奇吧~

public class StreamDemo {
    /**
     * 這里有一個很簡單的需求:
     * 有一組用戶數(shù)據(jù)(List<User>),
     * 我們要從這組數(shù)據(jù)中篩選出年齡在18-22,并且性別為男的所有用戶
     */

    // java8以下的方法
    public static List<User> filterUser(List<User> users){
        Iterator<User> it = users.iterator();
        List<User> us = new ArrayList<>();
        while (it.hasNext()){
            User u = it.next();
            if(u.getAge()>=18 && u.getAge()<=22 && u.getSex().contentEquals("男")){
                us.add(new User(u.getName(),u.getAge(),u.getSex()));
            }
        }
        return us;
    }

    // 使用stream方式
    public static List<User> filterByStream(List<User> users){
        List<User> us = new ArrayList<>();
        Stream<User> stream  = users.stream();
        stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
                .forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
        stream.close(); // 記得關(guān)流,否則將耗盡你的內(nèi)存,造成內(nèi)存泄漏程序崩潰。
        return us;
    }

    public static void main(String[] args) {
        List<User> data = getData();
        // java8前的用法:
        StreamDemo.filterUser(data).forEach((User u)-> System.out.println(u.getName())); // 輸出張三 肖二
        // Stream的用法:
        StreamDemo.filterByStream(data).forEach((User u)-> System.out.println(u.getName()));// 輸出張三 肖二
    }

    public static List<User> getData(){
        List<User> users = new ArrayList<>();
        users.add(new User("張三",22,"男"));
        users.add(new User("李四",18,"女"));
        users.add(new User("王五",28,"男"));
        users.add(new User("肖二",20,"男"));
        return users;
    }
}

由上可以看出使用Stream是非常簡便的。那么它有什么書寫規(guī)則可以遵循呢?

其實書寫Stream分為三步走。第一步,先創(chuàng)建一個Stream()對象,然后執(zhí)行一些中間的聚合操作(如上面例子中的filter),最后是執(zhí)行最終的操作(如上面例子中的forEach)。其中中間方法是可以有多個的。其實它就是一個元素流管道,先經(jīng)過中間方法的處理,最后由最終操作(terminal operation)得到前面處理的結(jié)果。

下面我們來分步認(rèn)識一下

1.創(chuàng)建Stream(源)

其實,創(chuàng)建Stream是有許多種方式的,畢竟也有各種各樣的流嘛。下面列出了九種創(chuàng)建方式,請客官慢慢品一下吧。

- 空的Stream

Stream<String> streamEmpty  = Stream.empty;

創(chuàng)建一個空Stream通常被用來避免空指針或者零元素對象的Stream返回為null的現(xiàn)象。

- 集合Steram

Collections<String> co = Arrays.asList("a", "b", "c");
Stream<String> streamCollection = co.stream();

可以創(chuàng)建Collection家族下面的List、Set、Queue。

- 數(shù)組Stream

Stream<String> streamArray = Stream.of("a", "b", "c");

上面是一種簡便方式,當(dāng)然也可以先創(chuàng)建一個數(shù)組(arr),再給數(shù)組創(chuàng)建Stream(Arrays.stream(arr))。

- 通過構(gòu)建器來創(chuàng)建

Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build();

當(dāng)builder被用于指定參數(shù)類型時,應(yīng)被額外標(biāo)識在聲明右側(cè),否則方法build()將創(chuàng)建一個Stream(Object)實例:

- 通過生成器來生成一個Stream對象

Stream<String> streamGenerated = Stream.generate( () -> "element").limit(10); // limit也是一個terminal

方法generator()接受一個供應(yīng)器Supplier<T>用于元素生成。Supplier函數(shù)式接口在第二篇中有講到,不接收參數(shù)返回一個T類型結(jié)果,有點像工廠直接給你創(chuàng)建一個對象一樣。這里代碼的意思是一直創(chuàng)建String字符串“element”,然后用limit來限制只有10個。否則會一直創(chuàng)建到j(luò)vm內(nèi)存達(dá)到頂值。

- 通過迭代器

Stream<Integer> streamItreated = Stream.iterate(20, n -> n + 2).limit(10);

這里也是會永遠(yuǎn)迭代直到達(dá)到j(luò)vm內(nèi)存的頂值,所以也要使用limit來限制一下。生成流中的元素為:20、22、24、...。

- 基元流

怎么理解這個基佬流呢?說錯了,是基元流。其實它就是對于八大基礎(chǔ)數(shù)據(jù)類型的流,不過這里只提供了三個:int、long、double。分別為IntStream、LongStream、DoubleStream。

IntStream intStream = IntStream.range(1, 10);
LongStream longStream = LongStream.rangeClosed(1, 10);

方法range(int start, int end)創(chuàng)建了一個有序流。它使后面的值每個增加1,但卻不包括最后一個參數(shù)。所以上面的range方法最終會創(chuàng)建一個1到9的IntStream。方法rangeClosed(int start, int end)與range()大致相同,但它卻包含了最后一個值。所以最終會創(chuàng)建一個1到10的LongStream

這兩個方法用于生成三大基本數(shù)據(jù)類型的stream。

另外,以前我們用來創(chuàng)建隨機(jī)數(shù)的類Random也擴(kuò)展了用于生成基元流

Random random = new Random();
DoubleStream doubleStream = random.doubles(2);

- 字符串流

字符串流主要得益于char。char和int的私密關(guān)系我想你是知道的...

IntStream streamOfChars = "abc".chars();

- 文件流

Path path = Paths.get("C:\\file.txt");
Stream<String> streamString = Files.lines(path);
Stream<String> streamCharset = Files.lines(path, Charset.forName("utf-8"));

Java NIO類文件允許通過方法lines()生成文本文件的Stream<String>。文本的每一行都會變成stream的一個元素

2.中間(Intermediate)方法

我這里第二步的中間方法講的是 - 惰性求值。

什么是惰性求值呢?

答: 惰性求值就是像上面例子中filter的這類函數(shù),它最終不會產(chǎn)生出一個結(jié)果,而只是對數(shù)據(jù)進(jìn)行進(jìn)一步的操作。相反,還有一個叫做及早求值,如上面例子中的forEach,最終會從Stream中產(chǎn)生一個新的值。

++及早求值 我在這里把它歸到第三步中,也就是最后一步求出值的操作中(當(dāng)然,別忘了關(guān)流)++

那像這樣惰性求值的Intermediate有哪些呢?

答:主要有這些:map (mapToInt, mapToLong, mapToDouble)、flatMap 、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered。

這些惰性求值的函數(shù)具體都有什么用呢?

答: 這里賣個關(guān)子,請滑到下面的Stream繼續(xù)探索,我會在那里根據(jù)源碼來講解。

3.最終(terminal)方法完成對數(shù)據(jù)的處理

最終方法基本就是那些及早求值的函數(shù)了:

不過它也分為兩大類,一類是Terminal(終結(jié)符、末尾),還有一類是short-circuiting(短路)

  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
  • short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny

下面會通過源碼來學(xué)習(xí)幾個中間方法和最終方法


注意:

  1. JAVA8 是不允許重復(fù)使用stream的。如果對于同一個stream有相同的引用,則會造成IllegalStateException異常
  2. Stream的執(zhí)行順序是垂直調(diào)用,如例:
stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
                .skip(1)
                .forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
// 中間方法skip的作用是返回一個跳過第幾元素(上面為跳過第一元素)的流。所以上面的方法執(zhí)行會得到第二個元素開始的集合(如果值夠的話)

上面的例子會執(zhí)行filter方法,當(dāng)filter方法滿足條件執(zhí)行完成后就會再去執(zhí)行skip,最后到達(dá)最終方法forEach。這就是Stream的垂直調(diào)用。

Stream繼續(xù)探索(源碼解讀)

既然是探索,那肯定得帶著疑問走啊?,F(xiàn)在的問題就是我已經(jīng)明白了怎么去創(chuàng)建一個Stream了,但是還不懂中間方法和最終方法的作用和用法。所以下面的操作就是一起來跟著源碼來操作一番吧~(為了篇幅著想,這里只舉了幾個例子)

中間方法的幾個例子

1. map

map在Stream中算是一個非常常用的方法了。

  • 源碼:
// 接收一個Function功能型函數(shù)式接口的參數(shù)(第二篇中有講到即接收一個T類型的參數(shù),返回一個R類型的結(jié)果)
// 從下面定義的泛型可以看出參數(shù)函數(shù)式接口的類型應(yīng)該與返回值的類型關(guān)系應(yīng)該是繼承或是一致的
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
  • 用法:
/**
 * 如下例子:從集合中的每個數(shù)值都拿出來乘二后再組成一個新的集合
 */
List<Integer> data = Arrays.asList(1,2,4,8,16);
List<Integer> res = data.stream().map((param)->param*2).collect(Collectors.toList());
System.out.println(res); // [2, 4, 8, 16, 32]
  • 作用: 可以來映射每個元素到對應(yīng)的結(jié)果

2. flatMap

看完上面一個map,這里又來了一個flatMap。flat? 平的? 平面? A?

  • 源碼
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
  • 用法
List<String> hello = Arrays.asList("hello","hi","你好","||");
List<String> straightMan = Arrays.asList("隨便","你忙","多喝熱水","||"); //直男三連
List<String> goodBoy = Arrays.asList("你是好人","you are good guy","very good");

List<List<String>> data = new ArrayList<>();
data.add(hello);
data.add(straightMan);
data.add(goodBoy);

List<String> res = data.stream().flatMap(flat->flat.stream()).collect(Collectors.toList());
System.out.println(res);// 輸出[hello, hi, 你好, ||, 隨便, 你忙, 多喝熱水, ||, 你是好人, you are good guy, very good]

// 不多說,這個例子希望你是看得懂的^_^..
  • 作用: 和map類似,不同的是其每個元素轉(zhuǎn)換得到的是Stream對象,它其實是對流進(jìn)行一個扁平化的操作。

3. peek

剛剛看完平的,現(xiàn)在又來一個偷看(peek)?Stream可真好玩啊~

  • 源碼
// 這里又用到了一個Consumer消費(fèi)型函數(shù)式接口(接收一個T類型的參數(shù),不返回結(jié)果),果然是看在眼里記在心里。
Stream<T> peek(Consumer<? super T> action);
  • 用法
List<String> heart = Arrays.asList("I","Love","U");
Stream stream = heart.stream().peek(System.out::print); // 這里會輸出  ILoveU
Object say = stream.collect(Collectors.toList());
stream.close();
System.out.println(say); // 這里會輸出  ILoveU[I, Love, U]
  • 作用:從上面例子可以看出peek會返回Stream接口,它是一個Intermediate,所以它返回的Stream可以供后續(xù)繼續(xù)使用。

最終方法的幾個例子

1. reduce

  • 源碼
//BinaryOperator接口繼承于BiFunction函數(shù)式方法,在第一篇最后部分有講到,接收兩參數(shù)返回一結(jié)果
//BinaryOperator<T>接口用于執(zhí)行l(wèi)ambda表達(dá)式并返回一個T類型的返回值
T reduce(T identity, BinaryOperator<T> accumulator);
  • 用法
int reducedTwoParams =
                IntStream.range(1, 3).reduce(10, (a, b) -> a + b);
        System.out.println("reducedTwoParams ="+reducedTwoParams);  // reducedTwoParams = 13
  • 作用:reduce主要被用來計算值,結(jié)果也是一個值,如上reducedTwoParams = 10+1+2+3

2. findAny

  • 源碼
// Optional也是java8的一個新特性,在下一章會開始探索。主要被用來防止空指針異常的
Optional<T> findAny();
  • 用法
/**
* 源碼里面關(guān)于它的注釋:
* Returns an {@link Optional} describing some element of the stream,
* or an  empty {@code Optional} if the stream is empty.
*/
// 簡要:如果stream是空的情況下就返回一個empty,否則按照條件來返回相應(yīng)的值
Stream<String> stream = Stream.of("a", "o", "e","i","w").filter(element -> element.contains("e"));
Optional<String> anyElement = stream.findAny();
System.out.println(anyElement); // Optional[e]

// 如上,如果把filter操作改成filter(element -> element.contains("q"));,則會返回Optional.empty
  • 作用:可以用來查找相關(guān)數(shù)值.

關(guān)于Stream的知識我上面講的這些還遠(yuǎn)遠(yuǎn)不達(dá)標(biāo),所以探索下去的心請希望能一直在跳動著。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Jav8中,在核心類庫中引入了新的概念,流(Stream)。流使得程序媛們得以站在更高的抽象層次上對集合進(jìn)行操作。...
    仁昌居士閱讀 4,060評論 0 6
  • Java8 in action 沒有共享的可變數(shù)據(jù),將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說的函數(shù)式...
    鐵牛很鐵閱讀 1,355評論 1 2
  • Streams 原文鏈接: Streams 原文作者: shekhargulati 譯者: leege100 狀態(tài)...
    忽來閱讀 5,607評論 3 32
  • 引言 Stream意為流,是Lambda編程中的一個重要角色。Stream類主要用于對收集類、數(shù)組、文件的迭代,以...
    斯特的簡書閱讀 553評論 0 0
  • 本文翻譯自The Java 8 Stream API Tutorial 1. 簡介 ??本教程志在細(xì)致入微、深入底...
    Yodes閱讀 2,505評論 3 8

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