Java 8 知識(shí)歸納(一)—— 流 與 Lambda

一招刹、Java8 的三個(gè)編程概念

  • 流處理
    • 從輸入流中一個(gè)一個(gè)讀取數(shù)據(jù)項(xiàng),然后以同樣的方式將數(shù)據(jù)項(xiàng)寫(xiě)入輸出流辩涝。
  • 用行為參數(shù)化把代碼傳遞給方法
    • 即函數(shù)作為第一公民衰腌,可以作為值來(lái)傳遞
  • 并行與共享可變數(shù)據(jù)

二新蟆、流簡(jiǎn)介

?????? Stream APICollection API的行為差不多,但Collection API主要為了訪(fǎng)問(wèn)和存儲(chǔ)數(shù)據(jù)右蕊,而Stream API主要用于描述對(duì)數(shù)據(jù)的計(jì)算琼稻。

?????? 經(jīng)典的Java程序只能利用單核進(jìn)行計(jì)算,流提供了多核處理數(shù)據(jù)的能力饶囚。但前提是傳遞給Stream API的方法不會(huì)互動(dòng)(即有可變的共享對(duì)象)時(shí)帕翻,才能多核工作。

三萝风、Lambda

Lambda表達(dá)式由 參數(shù)列表 嘀掸、箭頭主體 組成:

微信截圖_20200204105243.png

四、函數(shù)式接口

函數(shù)式接口指只定義一個(gè)抽象方法的接口

?????? 注:哪怕有再多默認(rèn)方法规惰,只要接口中只定義了一個(gè)抽象方法睬塌,它仍然是函數(shù)式接口。

?????? Lambda允許你直接以?xún)?nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實(shí)現(xiàn)歇万,并把其作為函數(shù)式接口的實(shí)例揩晴。

FunctionalInterface注解

?????? @FunctionalInterface用于表示該接口為函數(shù)式接口。如果它不是函數(shù)式接口的話(huà)贪磺,編譯器將返回一個(gè)提示原因的錯(cuò)誤硫兰。

?????? 注:@FunctionalInterface不是必需的,但最好為函數(shù)式接口都標(biāo)注@FunctionalInterface.

函數(shù)描述符

?????? 函數(shù)式接口的抽象方法的基本簽名 本質(zhì)上就是 Lambda表達(dá)式的簽名寒锚。Java8將這種抽象方法叫作函數(shù)描述符劫映。

?????? Runnable接口的run方法即不接受任何參數(shù)也不返回,其函數(shù)描述符為:() -> void刹前。 該函數(shù)描述符代表了函數(shù)列表為空且返回void的函數(shù)苏研。

?????? ScalaKotlin等語(yǔ)言在其類(lèi)型系統(tǒng)中提供 顯式的類(lèi)型注釋 來(lái)描述函數(shù)的類(lèi)型(即函數(shù)類(lèi)型)

函數(shù)接口 函數(shù)描述符 基本類(lèi)型特化
Predicate<T> T -> boolean IntPredicate LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

五腮郊、方法引用

方法引用可以把現(xiàn)有方法像Lambda一樣傳遞摹蘑。

方法引用主要分三類(lèi):

  • 指向靜態(tài)方法的方法引用。(例如 IntegerparseInt方法轧飞,寫(xiě)作Integer::parseInt
  • 指向任意類(lèi)型實(shí)例方法的方法引用.(例如Stringlength,寫(xiě)作String::length)
    • 適用于對(duì)象作為Lambda表達(dá)式的一個(gè)參數(shù)衅鹿。
  • 指向現(xiàn)存對(duì)象或表達(dá)式實(shí)例方法的方法引用
    • 適用于調(diào)用現(xiàn)存外部對(duì)象的方法。
    • 適用于內(nèi)部的私有方法过咬。

注:構(gòu)造函數(shù)大渤、數(shù)組構(gòu)造函數(shù)以及父類(lèi)調(diào)用的方法引用形式比較特殊:

利用 類(lèi)名 和 關(guān)鍵字 new 來(lái)生成構(gòu)造方法的方法引用。

  • 對(duì)于默認(rèn)構(gòu)造函數(shù)掸绞,可以使用Supplier簽名泵三。

    Supplier<Apple> c1 = Apple::new;
    //等價(jià)于:
    Supplier<Apple> c1 = () -> new Apple();
    
  • 對(duì)于存在參數(shù)的構(gòu)造方法耕捞,可根據(jù)參數(shù)情況尋找適合的函數(shù)式接口的簽名。

    Function<Integer,Apple> c2 = Apple::new;
    //等價(jià)于:
    Function<Integer,Apple> c2 = (weight) -> new Apple(weight);
    

六烫幕、流

從支持?jǐn)?shù)據(jù)處理操作的源生成的元素序列 —— 流

流允許以聲明性方式處理數(shù)據(jù)集合俺抽。還可以透明地并行處理,無(wú)須寫(xiě)任何多線(xiàn)程代碼较曼。

注:

  • 流只遍歷一次磷斧。遍歷完后,流被消費(fèi)了捷犹,需要重新從原始數(shù)據(jù)源那里再次獲取一個(gè)新的流進(jìn)行遍歷弛饭。
  • 只有觸發(fā)終端操作,中間操作才會(huì)被執(zhí)行萍歉。
    • 中間操作一般都可以合并起來(lái)侣颂,在終端操作中一次性全部處理。

篩選

  • filter方法:接受一個(gè)謂詞(一個(gè)返回boolean的函數(shù))作為參數(shù)枪孩,并返回一個(gè)包括所有符合謂詞的元素的流憔晒。

    //輸出結(jié)果:[1, 3, 0]
    List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
    numbers.stream()
        //篩選只小于4的元素
        .filter(i -> i < 4)
        .collect(Collectors.toList());
    
  • distinct方法:依據(jù)流所生成元素的 hashCodeequals 方法,返回一個(gè)元素各異的流销凑。(即返回一個(gè)沒(méi)有重復(fù)元素的流)

    //輸出結(jié)果為:[2,4]
    List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
    numbers.stream()
        .filter(i -> i % 2 == 0)
        //一共存在3個(gè)元素符合filter篩選丛晌,而這其中存在重復(fù)的2仅炊。distinct()只會(huì)返回2和4
        .distinct()
        .collect(Collectors.toList());
    

流的切片

  • takeWhile方法:在第一個(gè) 不符合 要求的元素時(shí)停止處理斗幼。
//輸出結(jié)果為:[1, 2, 3, 3]
//在初始列表中的數(shù)據(jù)已排序的情況下:
List<Integer> numbers = Arrays.asList(1,2,3,3,4,4,5,6);
numbers.stream()
    //當(dāng)發(fā)現(xiàn)第一個(gè) i < 4 為 false 的元素時(shí),則停止處理
    .takeWhile(i -> i < 4)
    .collect(Collectors.toList());
  • dropWhile方法:在第一個(gè) 符合 要求的元素時(shí)停止處理抚垄,并返回所有剩余的元素蜕窿。
//輸出結(jié)果:[4, 4, 5, 6]
//在初始列表中的數(shù)據(jù)已排序(由高到低)的情況下:
List<Integer> numbers = Arrays.asList(1,2,3,3,4,4,5,6);
numbers.stream()
    //當(dāng)發(fā)現(xiàn)第一個(gè)i < 4 為 true 的元素時(shí),則停止處理,并返回所有剩余的元素呆馁。
    .dropWhile(i -> i < 4)
    .collect(Collectors.toList());
  • limit方法:返回一個(gè)不超過(guò)給定長(zhǎng)度的流桐经。
    • 如果流是有序的(如:源是List),則按順序返回前 n 個(gè)元素浙滤。
    • 如果流是無(wú)序的(如:源是set)阴挣,則不會(huì)以任意順序排序。
    • 對(duì)于無(wú)限流纺腊,可以使用limit將其變成有限流畔咧。
//輸出結(jié)果:[1, 3]
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
numbers.stream()
    //篩選只小于4的元素
    .filter(i -> i < 4)
    //只返回前兩個(gè)值
    .limit(2)
    .collect(Collectors.toList());
  • shkip方法:返回一個(gè)扔掉前 n 個(gè)元素的流。
    • 如果流中元素不足 n 個(gè)揖膜,則返回一個(gè)空流誓沸。
//輸出結(jié)果:[3, 0]
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
numbers.stream()
    //篩選只小于4的元素
    .filter(i -> i < 4)
    //跳過(guò)第一個(gè)值
    .skip(2)
    .collect(Collectors.toList());

映射

  • map方法:將流中的每一個(gè)元素映射成一個(gè)新的元素。
//輸出結(jié)果:[6, 2, 4, 1]
List<String> languages = Arrays.asList("Kotlin","Go","Java","C");
languages.stream()
    //將 字符串 轉(zhuǎn)為 int 
    .map(String::length)
    .collect(Collectors.toList());
  • flatMap方法:把 一個(gè)流 中的 每一個(gè)值 轉(zhuǎn)換成 另一個(gè)流壹粟,然后把 所有流 連接起來(lái)成一個(gè)流拜隧。
    • 簡(jiǎn)單說(shuō)就是:把流中的 元素(如:列表,數(shù)組)化為新的流,或把流中的 元素 結(jié)合 **外部的列表 (數(shù)組) ** 化為新的流洪添,再把新的流的元素整合到一個(gè)流中垦页。
//輸出結(jié)果:[K, o, t, l, i, n, G, J, a, v, C]
List<String> languages = Arrays.asList("Kotlin","Go","Java","C");
languages.stream()
    .map(str -> str.split(""))
    //Arrays::stream 將 str.split("") 返回的字符數(shù)組轉(zhuǎn)換為流,再由 flatMap 統(tǒng)一將這些流合并成一個(gè)流.最終:Stream<String[]> 轉(zhuǎn)換為 Stream<String>,
    //flatMap 本質(zhì)也是對(duì)流的元素進(jìn)行轉(zhuǎn)換(map也是對(duì)流的元素進(jìn)行轉(zhuǎn)換)薇组。將流的元素轉(zhuǎn)換為新的流外臂,再將其整合進(jìn)一個(gè)流中。
    .flatMap(Arrays::stream) // 等價(jià)于:flatMap(strArray -> Arrays.stream(strArray))
    .distinct()
    .collect(Collectors.toList());

練習(xí):

1律胀、返回所有對(duì)數(shù)

給定列表[ 1,2,3 ]和 列表[ 3, 4 ],返回[ (1,3) , (1,4) , (2,3) , (2,4) , (3,3) , (3,4) ]

//輸出結(jié)果:[ (1,3) , (1,4) , (2,3) , (2,4) , (3,3) , (3,4) ]
List<Integer> numbers1 = Arrays.asList(1,2,3);
List<Integer> numbers2 = Arrays.asList(3,4);
List<int[]> pairs = 
    numbers1.stream()
        //將其扁平化為一個(gè)流
        .flatMap(i ->
                numbers2.stream()
                    //將其轉(zhuǎn)換為一個(gè)數(shù)組宋光,并返回這個(gè)流
                    .map(j -> new int[]{i,j})
        ).collect(Collectors.toList());

查找與匹配

  • anyMatch方法:檢查流中是否至少有一個(gè)元素匹配給定的謂詞。
//輸出結(jié)果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().anyMatch(i -> i > 3);
  • allMatch檢查謂詞是否匹配所有元素炭菌。

  • allMatch方法:檢查流中全部元素都匹配給定的謂詞罪佳。

//輸出結(jié)果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().allMatch(i -> i < 10);
  • noneMatch方法:檢查流中全部元素都不匹配給定的謂詞。( 與allMatch相對(duì) )
//輸出結(jié)果:true
List<Integer> numbers = Arrays.asList(1,2,3,5,6,8);
numbers.stream().noneMatch(i -> i > 10);
  • findAny方法:返回當(dāng)前流中的任意元素黑低。
List<Apple> inventory = Arrays.asList(
    new Apple(80,"green"),
    new Apple(155, "green"),
    new Apple(120, "red"));
Optional<Apple> apple = inventory.stream()
    .filter(a -> a.getColor().equals("green"))
    .findAny();
  • findFirst方法:返回當(dāng)前流中的第一個(gè)元素赘艳。
List<Apple> inventory = Arrays.asList(
    new Apple(80,"green"),
    new Apple(155, "green"),
    new Apple(120, "red"));
Optional<Apple> apple = inventory.stream()
    .filter(a -> a.getColor().equals("green"))
    .findFirst();

注:

??????1、anyMatch克握、allMatchnoneMatch 都屬于終端操作蕾管。

?????? 2、anyMatch菩暗、allMatch 掰曾、 noneMatchfindFirstfindAny 不用處理整停团,只要找到一個(gè)元素旷坦,就可以得到結(jié)果了。

?????? 3佑稠、findAnyfindFirst 同時(shí)存在的原因是 并行 秒梅。findAny在并行流中限制較少。

歸約

將流中所有元素反復(fù)結(jié)合起來(lái)舌胶,從而得到一個(gè)值的查詢(xún)捆蜀,可以被歸類(lèi)為歸約操作。(用函數(shù)式編程語(yǔ)言的術(shù)語(yǔ)來(lái)說(shuō)幔嫂,這稱(chēng)為折疊)

reduce方法:接收的Lambda將列表中的所有元素進(jìn)行處理并歸約成一個(gè)新值辆它。

有初始值

接收一個(gè)初始值 和 一個(gè)BinaryOperator<T>將兩個(gè)元素結(jié)合起來(lái)產(chǎn)生一個(gè)新值。

T reduce(T identity, BinaryOperator<T> accumulator);

無(wú)初始值

一個(gè)BinaryOperator<T>將兩個(gè)元素結(jié)合起來(lái)產(chǎn)生一個(gè)新值婉烟。

Optional<T> reduce(BinaryOperator<T> accumulator);
  • 求和
//輸出值:36
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用帶初始值的reduce方法
int sum = numbers.stream()
    .reduce(0,Integer::sum);//等價(jià)于 reduce(0,(a,b) -> a + b)
//或使用無(wú)初始值的reduce方法
Optional<Integer> sumOptional = numbers.stream().reduce(Integer::sum);
  • 最大值
Optional<Integer> maxOptional = numbers.stream().reduce(Integer::max);
  • 最小值
Optional<Integer> minOptional = numbers.stream().reduce(Integer::min);

數(shù)值流

?????? 原先的歸約求和代碼中娩井,Integet::sum暗含裝箱和拆箱的成本。Stream API提供了原始類(lèi)型流特化似袁,專(zhuān)門(mén)支持處理數(shù)值流的方法洞辣。Java8引入原始類(lèi)型特化接口解決數(shù)值流拆箱與裝箱的問(wèn)題:IntStream咐刨、DoubleStreamLongStream,分別將流中的元素特化為 int扬霜、 longdouble定鸟。

  • 映射到數(shù)值流

mapToIntmapToDoublemapToLong用于將流轉(zhuǎn)換為特化流:

//輸出值:36
List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();
  • 轉(zhuǎn)換回對(duì)象流

當(dāng)需要把原始流轉(zhuǎn)換成一般流時(shí)(如:把 int 裝箱回 Integer )著瓶,可以使用 boxed联予。

List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用 IntStrean 特化流
IntStream intStream = numbers.stream()
    .mapToInt(Integer::intValue);
Stream<Integer> stream = intStream.boxed();
  • 默認(rèn)值OptionalInt

Optional也相應(yīng)的提供原始類(lèi)型特化版本:OptionalIntOptionalLongOptionalDouble材原。

List<Integer> numbers = Arrays.asList(1,3,8,6,0,7,5,6);
//使用 OptionalInt 特化Optional
OptionalInt maxNumber = numbers.stream()
    .mapToInt(Integer::intValue)
    .max();

數(shù)值范圍

IntStreamLongStream提供產(chǎn)生生成數(shù)值范圍的靜態(tài)方法:rangerangeClosed沸久。

range方法生成半閉區(qū)間(左閉右開(kāi)),rangeClosed方法生成閉區(qū)間余蟹。

IntStream.range(1,100)
    .filter(n -> n % 2 == 0)
    .count();

構(gòu)建流

  • 由值創(chuàng)建流

靜態(tài)方法 Stream.of 接受任意數(shù)量的參數(shù)卷胯,顯式創(chuàng)建一個(gè)流。

//顯式創(chuàng)建字符串流
Stream<String> strStream =Stream.of("Java","Kotlin","Go");

靜態(tài)方法Stream.empty創(chuàng)建一個(gè)空流威酒。

Stream<String> strStream =Stream.empty();
  • 由數(shù)組創(chuàng)建流

靜態(tài)方法Arrays.stream將數(shù)組創(chuàng)建為一個(gè)流窑睁。

int[] numbers = {2,3,5,6,7};
int sum = Arrays.stream(numbers).sum();
  • 由文件生成流

java.nio.file.Files中很多靜態(tài)方法會(huì)返回一個(gè)流,以便利用Stream API處理文件等I/O操作葵孤。

如:Files.lines返回一個(gè)由指定文件中的各行構(gòu)成的字符串流:

long uniqueWords = 0;
//流會(huì)自動(dòng)關(guān)閉担钮,不需要額外try-finally操作
try(Stream<String> lines = 
    Files.lines(Paths.get("data.text"), Charset.defaultCharset())){
    //統(tǒng)計(jì)有多少不重復(fù)的單詞。
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
        .distinct()
        .count();
}catch (IOException e){}
  • 由函數(shù)生成流:創(chuàng)建無(wú)限流

Stream API提供了兩個(gè)靜態(tài)方法來(lái)從函數(shù)生成流:Stream.iterate()String.generate()

不同于從集合創(chuàng)建的流尤仍,這兩個(gè)靜態(tài)方法創(chuàng)建的流沒(méi)有固定大小箫津,稱(chēng)為無(wú)限流

迭代:

iterate方法接收一個(gè)接受一個(gè)初始值作為流的第一個(gè)元素吓著。再接收一個(gè)Lambda依次應(yīng)用在每一個(gè)產(chǎn)生的新值上鲤嫡。

Stream.iterate(0,n -> n + 2)
    .limit(10)
    .forEach(System.out::println);

Java 9對(duì)iterate方法進(jìn)行增加送挑,接受多一個(gè)謂詞作為判斷迭代調(diào)用何時(shí)終止绑莺。(謂詞作為第二參數(shù)傳入)

IntStream.iterate(0,n -> n < 100,n -> n + 2)
    .forEach(System.out::println);

當(dāng)然,也可以使用takeWhile對(duì)流執(zhí)行短路操作(takeWhile函數(shù)Java9開(kāi)始支持):

IntStream.iterate(0,n -> n + 2)
    .takeWhile(n -> n < 100)
    .forEach(System.out::println);

生成:

generate接受一個(gè)Supplier<T>類(lèi)型的Lambda提供新值惕耕。

Stream.generate(Math::random)
    .limit(5)
    .forEach(System.out::println);

七纺裁、用流收集數(shù)據(jù)

流支持兩種類(lèi)型的操作:中間操作末端操作

  • 中間操作可以相互鏈接起來(lái)司澎,將一個(gè)流轉(zhuǎn)換為另一個(gè)流欺缘。中間操作不會(huì)消耗流,目的是建立一個(gè)流水線(xiàn)挤安。
  • 末端操作會(huì)消耗流谚殊,以產(chǎn)生一個(gè)最終結(jié)果。

歸約和匯總

  • Collectors 工廠(chǎng)類(lèi)提供了很多歸約的靜態(tài)工廠(chǎng)方法蛤铜。

    • Collectors.counting() 用于統(tǒng)計(jì)總和嫩絮。
    //求總和
    long count = menu.stream().collect(Collections.counting());
    
    • Collectors.maxByCollectors.minBy 用來(lái)計(jì)算流中的最大值和最小值丛肢。
    //求最大值
    Optional<Dish> mostCalorieDish = 
        menu.stream().collect(
          Comparator.maxBy(
                Comparator.comparingInt(Dish::getCalories)
            )
      );
    
  • 同時(shí)Collectors 類(lèi)專(zhuān)門(mén)為匯總提供了一些工廠(chǎng)方法。

    • Collectors.summingInt剿干、Collectors.summingLongCollectors.summingDouble 分別用于對(duì) int蜂怎、longdouble進(jìn)行求和。
    int sumValue = menu.stream().collect(summingInt(Dish::getCalories));
    
    • Collectors.averagingInt置尔、Collectors.averagingLongCollectors.averagingDouble 分別用于對(duì) int杠步、longdouble進(jìn)行求平均值。
    double avgValue = menu.stream().stream().collect(averagingInt(Dish::getCalories));
    
  • Collectors.joining 工廠(chǎng)方法會(huì)對(duì)流中每一個(gè)對(duì)象應(yīng)用 toString 方法得到所有字符串連接成一個(gè)字符串榜轿。

String nameStr = menu.stream().map(Dish::getName).collect(joining());

分組

CollectionsgroupingBy() 方法會(huì)把流中的元素分成不同的組幽歼。

微信截圖_20200224172850.png

操作分組的元素

  • 過(guò)濾

如果在 groupingBy() 之前,使用 filter() 對(duì)流進(jìn)行過(guò)濾操作谬盐,可能會(huì)造成鍵的丟失试躏。

例如:

?????? 存在以下Map: { FISH = [ prawns, salmon], OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

但如果在使用filter() 后,再 groupingBy() 可能對(duì)某些鍵在結(jié)果映射中完全消失:

?????? { OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

為此设褐,Collectors 類(lèi)提供了 filtering() 靜態(tài)工廠(chǎng)方法颠蕴,它接受一個(gè)謂詞對(duì)每一個(gè)分組中的元素執(zhí)行過(guò)濾操作。最后不符合謂詞條件的鍵將得到空的列表:

{ FISH = [], OTHER = [french fries, rice ], MEAT = [pork , beef, chicken] }

Map<Dish.Type,List<Dish>> caloricDishesByType = menu.stream()
    .collect( groupingBy(Dish::getType),
            filtering(dish -> dish.getCalories() > 500,toList()))

?????? 使用重載的 groupingBy() 方法 和 filtering()方法 :先分組再過(guò)濾助析;

?????? 先使用 filter()犀被,再使用 groupingBy() 方法:先過(guò)濾再分組。

  • 映射

Collectors 提供 mapping 靜態(tài)工廠(chǎng)方法外冀,接受一個(gè)映射函數(shù)和另外一個(gè) Collectors 函數(shù)作為參數(shù)寡键。映射函數(shù)將分組中的元素進(jìn)行轉(zhuǎn)換,作為參數(shù)的 Collectors 函數(shù)會(huì)收集對(duì)每個(gè)元素執(zhí)行該映射函數(shù)的結(jié)果雪隧。

Map<Dish.Type,List<String>> dishNamesByType = menu.stream().collect(
    groupingBy(
        Dish::getType,
        mapping(
            //將元素轉(zhuǎn)換為其名字
            Dish::getName,
            //用于收集該組進(jìn)行完映射的元素
            Collectors.toList()
        )
    )
)

Collectors 工具類(lèi)也提供了 flatMapping,跟 flatMap 類(lèi)似的功能西轩。

多級(jí)分組

同時(shí)Collectors 工具類(lèi)也提供了可以嵌套分組的groupingBy(),用于進(jìn)行多級(jí)分組

注:

?????? 可以理解為在進(jìn)行完第一次分組后,再對(duì)每一組元素進(jìn)行再次分組脑沿。

?????? groupingBy(f)( f 是分類(lèi)函數(shù) ) 實(shí)際上是groupingBy(f藕畔,toList())的簡(jiǎn)便寫(xiě)法。

Map<Dish.Type,Map<CaloricLevel,List<Dish>>> dishesByTypeCaloricLevel = 
    menu.stream().collect(
        groupingBy(
            Dish::getType,
            groupingBy(dish -> {
                if(dish.getCalories() <= 400)
                    return CaloricLevel.DIET;
                else if(dish.getCalories() <= 700)
                    return CaloricLevel.NORMAL;
                else 
                    return CaloricLevel.FAT;
            })
        )
    );
微信截圖_20200226150753.png

按子組收集數(shù)據(jù)

groupingBy()的第二個(gè)收集器可以是任何類(lèi)型庄拇。例如可以使用 counting() 收集器作為它的第二個(gè)參數(shù)注服,統(tǒng)計(jì)分組的數(shù)量:

Map<Dish.TYPE,Long> typesCount = menu.stream().collect(
    groupingBy(Dish::getType,counting())
);

得到以下的map: { MEAT = 3 , FISH = 2 , OTHER = 4 }

Map<Dish.Type,Dish> mostCaloricByType = 
    menu.stream().collect(
        groupingBy(Dish::getTpye,
                   collectingAndThen(
                        //maxBy返回的是Optional類(lèi)型對(duì)象
                        maxBy(comparingInt(Dish::getCalories)),
                       //當(dāng)找到最大值后,會(huì)執(zhí)行g(shù)et操作措近。
                        Optional::get
                   )
        )
    );

如果 menu 中沒(méi)有某一類(lèi)型的Dish,該類(lèi)型不會(huì)對(duì)應(yīng)一個(gè) Optional.empty() 值溶弟,而且根本不會(huì)在Map的鍵中。所以轉(zhuǎn)換函數(shù)Optional::get的操作是安全的瞭郑。

分區(qū)

Collectors 工具類(lèi)提供 partitionedMenu() 靜態(tài)工廠(chǎng)函數(shù)來(lái)實(shí)現(xiàn)分區(qū)辜御,分區(qū)是分組的特殊情況。由謂詞作為分類(lèi)函數(shù)屈张,這意味著得到的分組 Map 的鍵類(lèi)型是 Boolean ,最多分為 truefalse 兩組擒权。

//將得到以下結(jié)果:
Map<Boolean,List<Dish>> partitionedMenu = 
    menu.stream().collect(
        //分區(qū)函數(shù)
        partitioningBy(
            //分期的標(biāo)準(zhǔn)
            Dish::isVegetarian
        )
    )

同時(shí)partitionedMenu()也和groupingBy()類(lèi)似苇本,可以進(jìn)行二級(jí)分區(qū)。

收集器接口

public interface Collector<T, A, R> {
    //創(chuàng)建一個(gè)空的累加器
    Supplier<A> supplier();
    //將元素添加到結(jié)果容器
    BiConsumer<A, T> accumulator();
    //合并兩個(gè)結(jié)果(定義了對(duì)流的各個(gè)子部分進(jìn)行并行處理時(shí)菜拓,各個(gè)子部分歸約所得的累加器如何合并)
    BinaryOperator<A> combiner();
    //對(duì)結(jié)果容器應(yīng)用最終轉(zhuǎn)換
    Function<A, R> finisher();
    //定義收集器的行為
    Set<Characteristics> characteristics();
}

泛型的定義如下:

?????? T 表示流中要手機(jī)的項(xiàng)目的泛型瓣窄。

?????? A 表示累加器的類(lèi)型。(累加器是收集過(guò)程中用于累積部分結(jié)果的對(duì)象)

?????? R 表示收集操作得到的對(duì)象的類(lèi)型纳鼎。

ToListCollector為例

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    public ToListCollector() {}
    
    //創(chuàng)建ArrayList對(duì)象作為累加器
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }
    
    //利用add函數(shù)將流中的元素添加到列表中
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }
    
    //兩個(gè)累加器(即兩個(gè)ArrayList對(duì)象)進(jìn)行相加
    public BinaryOperator<List<T>> combiner() {
        return (list, list2) -> {
            list.addAll(list2);
            return list;
        };
    }
    
    //累加器進(jìn)行最終的轉(zhuǎn)換
    public Function<List<T>, List<T>> finisher() {
        //Function.identity()表示給什么返回什么俺夕,也就是不進(jìn)行轉(zhuǎn)換
        //恒等
        return Function.identity();
    }

    //定義收集器的行為
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}

Characteristics的三個(gè)枚舉:

  • UNORDERED —— 歸約結(jié)果不受流中項(xiàng)目的遍歷和累積順序的影響。
  • CONCURRENT—— accumulator 函數(shù)可以從多個(gè)線(xiàn)程同時(shí)調(diào)用贱鄙,且該收集器可以并行歸約流劝贸。(僅僅只是數(shù)據(jù)源無(wú)序時(shí)才會(huì)并行處理)
  • IDENTITY_FINISH—— 表明完成器方法返回的函數(shù)是一個(gè)恒等函數(shù),可以跳過(guò)逗宁。累加器對(duì)象會(huì)直接用作歸約過(guò)程的最終結(jié)果映九。這也意味著,將累加器A不加檢查的轉(zhuǎn)換為結(jié)果R是安全的瞎颗。

進(jìn)行自定義收集件甥,而不去實(shí)現(xiàn) Collector

對(duì)于 IDENTITY_FINISH 的收集操作,Stream重載的 collect 方法接受三個(gè)函數(shù)——supplier哼拔、accumulatorcombiner引有。該 collect 方法創(chuàng)建的收集器的 Characteristics 永遠(yuǎn)是Characteristics.IDENTITY_FINISHCharacteristics.CONCURRENT

List<Dish> dishes = menu.stream().collect(
    //創(chuàng)建累加容器
    ArrayList::new,
    //將流元素添加到累加容器中
    List::add,
    //合并累加容器
    List::addAll
);

八、并行數(shù)據(jù)處理與性能

  • 對(duì)順序流調(diào)用 parallel() 方法并不意味著流本身有任何實(shí)際的變化倦逐,它僅僅在內(nèi)部設(shè)置了一個(gè)boolean標(biāo)志譬正,表示你想讓調(diào)用parallel()之后的所有操作都并行執(zhí)行。對(duì)并行流調(diào)用 sequential 方法就可以把它變成順序流檬姥。
  • 并行流默認(rèn)的線(xiàn)程數(shù)量等于你處理器的核數(shù)曾我。

使用并行流時(shí),考慮以下因素

  • 留意自動(dòng)裝箱和拆箱健民。(應(yīng)盡量將其轉(zhuǎn)為原始類(lèi)型流)
  • 對(duì)于較小數(shù)據(jù)量抒巢,無(wú)需使用并行流。
  • 考慮流背后的數(shù)據(jù)結(jié)構(gòu)是否容易分解荞雏。
  • 部分操作本身在并行流上的性能比順序流差虐秦。如:limitfindFirst
  • 考慮合并 步驟的代價(jià)是大是小平酿。
  • 考慮操作流水線(xiàn)的總操作成本凤优。當(dāng)單個(gè)元素通過(guò)流水線(xiàn)的成本較高時(shí),使用并行流比較好蜈彼。

流的數(shù)據(jù)源和可分解性:

可分解性
ArrayList
LinkedList
IntStream.range 極佳
Stream.iterate
HashSet
TreeSet

九筑辨、Collection API的增強(qiáng)功能

Arrays.asList() 創(chuàng)建一個(gè)固定大小的列表,列表的元素可以更新幸逆,但不可以增加或刪除棍辕。

Java 9 引入以下工廠(chǎng)方法:

  • List.of ——?jiǎng)?chuàng)建一個(gè)只讀列表暮现,不可setadd等操作楚昭。

  • Set.of —— 創(chuàng)建一個(gè)只讀的Set集合栖袋。

  • Map.of —— 接受的列表中,以鍵值交替的方式創(chuàng)建map的元素抚太。塘幅、

    • 當(dāng)創(chuàng)建Map的鍵值對(duì)過(guò)多時(shí),可以使用map.ofEntries()Map.entry()創(chuàng)建map.
    import static java.util.Map.entry;
    Map<String,Integer> ageOfFriends = Map.ofEntries(
      entry("Raphael",30),
        entry("Olivia",25),
        entry("Thibaut",26)
    );
    

重載與變參

Java API中尿贫,List.of包含多個(gè)重載版本:

static <E> List<E> of(E e1);
static <E> List<E> of(E e1, E e2);

而不提供變參版本是因?yàn)樾枰~外的分配一個(gè)數(shù)組电媳,這個(gè)數(shù)組被封裝于列表中。使用變參版本的方法庆亡,就要負(fù)擔(dān)分配數(shù)組匾乓、初始化以及最后進(jìn)行垃圾回收的開(kāi)銷(xiāo)。(如果元素?cái)?shù)量超過(guò)10個(gè)又谋,實(shí)際調(diào)用的還是變參方法拼缝。)、

使用 List 彰亥、 SetMap

  • removeIf() —— 移除集合中匹配指定謂詞的元素珍促。(該方法由Collection接口提供默認(rèn)方法,ListSet 都可用)

    • //Collection.java
      //Predicate(謂詞)的函數(shù)描述符是:(T) -> boolean
      default boolean removeIf(Predicate<? super E> filter)
      
    • 當(dāng)使用for-each遍歷列表剩愧,進(jìn)行移除操作時(shí)猪叙,會(huì)導(dǎo)致ConcurrentModificationException.因?yàn)楸闅v使用的迭代器對(duì)象和集合對(duì)象的狀態(tài)同步。我們只能顯示調(diào)用迭代器對(duì)象(Iterator對(duì)象)的remove方法仁卷。因此Java8提供removeIf方法穴翩,安全簡(jiǎn)便的刪除符合謂詞的元素。

  • replaceAll() —— 使用一個(gè)函數(shù)替換ListMap 中的元素锦积。(該方法由List接口提供默認(rèn)方法)

    • //List.java
      //UnaryOperator的函數(shù)描述符是:(T) -> T
      default void replaceAll(UnaryOperator<E> operator)
      
    • 該函數(shù)只是在列表內(nèi)部進(jìn)行同類(lèi)型的轉(zhuǎn)換芒帕,并沒(méi)有創(chuàng)建新的列表。也就是說(shuō)初始為List<String>丰介,函數(shù)執(zhí)行完還是List<String>.

    • //Map.java
      // BiFunction的函數(shù)描述符是:(K,V) -> V
      default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
      
  • sort() —— 對(duì)列表自身進(jìn)行排序背蟆。(該方法由List接口提供默認(rèn)方法)

    • //List.java
      //Comparator的函數(shù)描述符是:(T,T) -> boolean
      default void sort(Comparator<? super E> c)
      
  • forEach() —— List 和 Set,甚至是Map在Java8中都支持forEach方法哮幢。而遍歷提供的便捷带膀,特別是Map的遍歷。

    • //Iterable.java
      //Consumer(消費(fèi)者)的函數(shù)描述符是:(T) -> void
      default void forEach(Consumer<? super T> action)
      //Map.java
      //BiConsumer(二元消費(fèi)者)的函數(shù)描述符是:(T,U) -> void
      default void forEach(BiConsumer<? super K, ? super V> action)
      
  • Entry.comparingByValue()Entry.comparingByKey() —— 對(duì)Map的值或鍵進(jìn)行排序橙垢。

  • Map.compute —— 使用指定的鍵計(jì)算新的值垛叨,并將其存儲(chǔ)到Map中,并返回新值柜某。嗽元。(指定一個(gè)key,再提供一個(gè)BiFunction敛纲,依據(jù)key舊值,計(jì)算新值剂癌。如果新值為null,則不會(huì)加入到Map中并將舊值移除淤翔。)

    • //BiFunction的函數(shù)描述符是:(K,V) -> V
      default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) 
      
  • Map.computeIfAbsent —— 如果指定的鍵沒(méi)有對(duì)應(yīng)的值(沒(méi)有該鍵或者該鍵對(duì)應(yīng)的值是空),使用該鍵計(jì)算新的值佩谷,并添加到Map中(如果新值為null,則不會(huì)加入到Map中并將舊值移除。)琳要,并返回新值寡具。

    • //Function的函數(shù)描述符是:(K) -> V
      default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)
      
    • 該方法對(duì)于值需要初始化時(shí)有用。比如向Map<K,List<V>>添加一個(gè)元素( 初始化對(duì)應(yīng)的ArrayList稚补,并返回該值):

      map.computeIfAbsent("daqi", name -> new ArrayList<>())
          .add("Java8")
      
  • Map.computeIfPresent —— 如果指定的鍵在Map中存在童叠,依據(jù)該鍵和舊值計(jì)算該鍵的新值,并將其添加到Map中课幕。(如果新值為null,則不會(huì)加入到Map中厦坛,并將舊值移除。)

    • //BiFunction的函數(shù)描述符是:(K,V) -> V
      default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) 
      
  • Map.remove —— 重載版本的remove可以刪除Map中某個(gè)鍵對(duì)應(yīng)某個(gè)特定值的映射對(duì)乍惊。(即 KeyValue都匹對(duì)上杜秸,才從Map中移除)

    • default boolean remove(Object key, Object value) 
      
  • Map.replace —— 重載版本的replace可以?xún)H在原有鍵對(duì)應(yīng)某個(gè)特定的值時(shí)才進(jìn)行替換。(即 KeyValue都匹對(duì)上润绎,才從Map中替換)

    • default V replace(K key, V value)
      
  • Map.merge —— 如果指定的鍵在Map中存在撬碟,依據(jù)該鍵和舊值計(jì)算該鍵的新值,并將其添加到Map中莉撇; 如果指定的鍵在Map中不存在呢蛤,依據(jù)指定的value作為Key的值,并將其添加到Map中棍郎。

    • //BiFunction的函數(shù)描述符:(V,V) -> V
      default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction)
      
    • 該函數(shù)可用于Map的合并其障,或用于將Collector轉(zhuǎn)換成Map.

      Map<String,Integer> language1 = new HashMap<>();
      language1.put("Java",8);
      language1.put("Kotlin",1);
      Map<String,Integer> language2 = new HashMap<>();
      language2.put("Java",11);
      language2.put("Go",1);
      
      //合并Map
      language1.forEach((key,value) -> {
          //Map的value可null,merge函數(shù)不允許value為null
          if (value != null) 
              language2.merge(key,value,Integer::sum);
      });
      
    • static class Score{
          private int score;
          private int studentId;
          private String studentName;
      
          public Score(int studentId, String studentName,int score) {
              this.score = score;
              this.studentId = studentId;
              this.studentName = studentName;
          }
          //get和set方法
      }
      
      //Collector轉(zhuǎn)換為Map(用途:統(tǒng)計(jì))
      List<Score> languageList = new ArrayList<>();
      languageList.add(new Score(1,"Java",80));
      languageList.add(new Score(2,"Kotlin",90));
      languageList.add(new Score(2,"Java",85));
      languageList.add(new Score(1,"Kotlin",70));
      Map<String,Integer> language3 = new HashMap<>();
      //Collectors.toMap(Function<? super T, ? extends K>,Function<? super T, ? extends U>,BinaryOperator<U>)內(nèi)部也是通過(guò)Map.merge()實(shí)現(xiàn)的。
      languageList.stream().collect(Collectors.toMap(Score::getStudentId,Score::getScore,Integer::sum));
      

十涂佃、 重構(gòu)

改善代碼可讀性

  • lambda表達(dá)式取代匿名類(lèi)励翼。

    • 匿名類(lèi)和lambda表達(dá)式中的 thissuper 的含義不同。在匿名類(lèi)中辜荠,this 代表的是類(lèi)自身;在lambda表達(dá)式中侨拦,this 代表的是包含類(lèi)。

    • 匿名類(lèi)可屏蔽包含類(lèi)的變量狱从,而lambda表達(dá)式不能(導(dǎo)致編譯報(bào)錯(cuò))。

      int a = 10;
      //lambda表達(dá)式
      Runnable r1 = () -> {
          //idea爆紅敞葛,提示:該變量已在作用域中被定義与涡。
          int a = 1;
      };
      //匿名類(lèi)
      Runnable r2 = new Runnable() {
          @Override
          public void run() {
              //編譯正常
              int a = 2;
          }
      };
      
    • 匿名內(nèi)部類(lèi)的類(lèi)型是在初始化時(shí)確定的,lambda的類(lèi)型取決于它的上下文氨肌。當(dāng)出現(xiàn)兩個(gè)或以上方法參數(shù)的函數(shù)描述符與lambda的函數(shù)描述符匹配時(shí)酌畜,需要顯示的類(lèi)型轉(zhuǎn)換來(lái)解決。

      interface daqiRunnable{
          public void action();
      }
      //無(wú)論Runnable恳守,還是daqiRunnable贩虾,其函數(shù)描述符為() -> void
      public static void doSomething(Runnable r){}
      public static void doSomething(daqiRunnable r){}
      
      public static void main(String[] args) {
          //顯示類(lèi)型轉(zhuǎn)換
          doSomething((daqiRunnable) () -> {});
      }
      
  • 方法引用 重構(gòu)lambda表達(dá)式缎罢,提高代碼的可讀性。

    • 將較復(fù)雜的Lambda邏輯封裝在方法中策精,使用方法引用替代該Lambda。

    • 盡量使用靜態(tài)輔助方法蔽午。比如:comparingmaxBy

      list.sort((a1,a2) -> a1.getWeight().compareTo(a2.getWeight()));
      //替換成:
      list.sort(Comparator.comparing(Apple::getWeight));
      
    • 很多通用的歸約操作酬蹋,都可以借助Collectors的輔助方法 + 方法引用替代。

      list.stream()
          .map(Dish::getCalories)
          .reduce(0,(c1,c2) -> c1 + c2);
      //替換成Collectors的輔助方法
      list.stream()
          .collect(summingInt(Dish::getCalories));
      
  • Stream API重構(gòu)命令式的數(shù)據(jù)處理

參考資料

Java實(shí)戰(zhàn)(第2版)

Java8系列

Java 8 知識(shí)歸納(一)—— 流 與 Lambda

Java 8 知識(shí)歸納(二)—— Optional

Java 8 知識(shí)歸納(三)—— 日期API

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載骄恶,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者僧鲁。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市斟叼,隨后出現(xiàn)的幾起案子春寿,更是在濱河造成了極大的恐慌,老刑警劉巖谢床,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厘线,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡渡讼,警方通過(guò)查閱死者的電腦和手機(jī)费薄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)楞抡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人凳厢,你說(shuō)我怎么就攤上這事竞慢。” “怎么了筹煮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵败潦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我檬洞,道長(zhǎng)沟饥,這世上最難降的妖魔是什么湾戳? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任砾脑,我火速辦了婚禮性昭,結(jié)果婚禮上县遣,老公的妹妹穿的比我還像新娘。我一直安慰自己其兴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布元旬。 她就那樣靜靜地躺著匀归,像睡著了一般耗帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上体啰,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天嗽仪,我揣著相機(jī)與錄音,去河邊找鬼沽翔。 笑死窿凤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卷玉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼威恼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼沸移!你這毒婦竟也來(lái)了看幼?” 一聲冷哼從身側(cè)響起论衍,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤阻桅,失蹤者是張志新(化名)和其女友劉穎弦牡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體卸留,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡椭豫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年赏酥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片框都。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姓言,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出囱淋,到底是詐尸還是另有隱情餐塘,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布税手,位于F島的核電站需纳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏不翩。R本人自食惡果不足惜麻裳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一津坑、第九天 我趴在偏房一處隱蔽的房頂上張望疆瑰。 院中可真熱鬧,春花似錦穆役、人聲如沸跟衅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叭莫。三九已至烁试,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間靖诗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工刊橘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留促绵,地道東北人嘴纺。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像尖坤,于是被迫代替她去往敵國(guó)和親闲擦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容