国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

從頭學(xué)Java17-Stream API(二)結(jié)合Record、Optional

這篇具有很好參考價(jià)值的文章主要介紹了從頭學(xué)Java17-Stream API(二)結(jié)合Record、Optional。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

Stream API

Stream API 是按照map/filter/reduce方法處理內(nèi)存中數(shù)據(jù)的最佳工具。
本系列教程由Record講起,然后結(jié)合Optional,討論collector的設(shè)計(jì)。

使用Record對(duì)不可變數(shù)據(jù)進(jìn)行建模

Java 語言為您提供了幾種創(chuàng)建不可變類的方法??赡茏钪苯拥氖莿?chuàng)建一個(gè)包含final字段的final類。下面是此類的示例。

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

編寫這些元素后,需要為字段添加訪問器。您還將添加一個(gè) toString() 方法,可能還有一個(gè) equals() 以及一個(gè) hashCode() 方法。手寫所有這些非常乏味且容易出錯(cuò),幸運(yùn)的是,您的 IDE 可以為您生成這些方法。

如果需要通過網(wǎng)絡(luò)或文件系統(tǒng)將此類的實(shí)例從一個(gè)應(yīng)用程序傳送到另一個(gè)應(yīng)用程序,則還可以考慮使此類可序列化。如果這樣做,還要添加一些有關(guān)如何序列化的信息。JDK 為您提供了幾種控制序列化的方法。

最后,您的Point類可能有一百多行,主要是IDE 生成的代碼,只是為了對(duì)需要寫入文件的兩個(gè)整數(shù)不可變集進(jìn)行建模。

Record已經(jīng)添加到 JDK 以改變這一切。只需一行代碼即可為您提供所有這些。您需要做的就是聲明record的狀態(tài);其余部分由編譯器為您生成。

呼叫Record支援

Record可幫助您使此代碼更簡(jiǎn)單。從 Java SE 14 開始,您可以編寫以下代碼。

public record Point(int x, int y) {}

這一行代碼為您創(chuàng)建以下元素。

  1. 它是一個(gè)不可變的類,有兩個(gè)字段:xy
  2. 它有一個(gè)標(biāo)準(zhǔn)的構(gòu)造函數(shù),用于初始化這兩個(gè)字段。
  3. toString()、equals() 和 hashCode() 方法是由編譯器為您創(chuàng)建的,其默認(rèn)行為與 IDE 將生成的內(nèi)容相對(duì)應(yīng)。如果需要,可以通過添加自己的實(shí)現(xiàn)來修改此行為。
  4. 它可以實(shí)現(xiàn)Serializable接口,以便您可以通過網(wǎng)絡(luò)或通過文件系統(tǒng)發(fā)送到其他應(yīng)用程序。序列化和反序列化record的方式遵循本教程末尾介紹的一些特殊規(guī)則。

record使創(chuàng)建不可變的數(shù)據(jù)集變得更加簡(jiǎn)單,無需任何 IDE 的幫助。降低了錯(cuò)誤的風(fēng)險(xiǎn),因?yàn)槊看涡薷膔ecord的組件時(shí),編譯器都會(huì)自動(dòng)更新 equals() 和 hashCode() 方法。

record的類

record也是類,是用關(guān)鍵字record而不是class聲明的類。讓我們聲明以下record。

public record Point(int x, int y) {}

編譯器在創(chuàng)建record時(shí)為您創(chuàng)建的類是final的。

此類繼承了 java.lang.Record 類。因此,您的record不能繼承其他任何類。

一條record可以實(shí)現(xiàn)任意數(shù)量的接口。

聲明record的組成部分

緊跟record名稱的塊是(int x, int y) 。它聲明了record組件。對(duì)于record的每個(gè)組件,編譯器都會(huì)創(chuàng)建一個(gè)同名的私有final字段。您可以在record中聲明任意數(shù)量的組件。

除了字段,編譯器還為每個(gè)組件生成一個(gè)訪問器。此訪問器跟組件的名稱相同,并返回其值。對(duì)于此record,生成的兩個(gè)方法如下。

public int x() {
    return this.x;
}

public int y() {
    return this.y;
}

如果此實(shí)現(xiàn)適用于您的應(yīng)用程序,則無需添加任何內(nèi)容。不過,也可以定義自己的訪問器。

編譯器為您生成的最后一個(gè)元素是 Object 類中 toString()、equals() 和 hashCode() 方法的重寫。如果需要,您可以定義自己對(duì)這些方法的覆蓋。

無法添加到record的內(nèi)容

有三件事不能添加到record中:

  1. 額外聲明的實(shí)例字段。不能添加任何與組件不對(duì)應(yīng)的實(shí)例字段。
  2. 實(shí)例字段的初始化。
  3. 實(shí)例的初始化塊。

您可以使用靜態(tài)字段,靜態(tài)初始化塊。

使用標(biāo)準(zhǔn)構(gòu)造函數(shù)構(gòu)造record

編譯器還會(huì)為您創(chuàng)建一個(gè)構(gòu)造函數(shù),稱為標(biāo)準(zhǔn)構(gòu)造函數(shù) canonical constructor。此構(gòu)造函數(shù)以record的組件作為參數(shù),并將其值復(fù)制到字段中。

在某些情況下,您需要覆蓋此默認(rèn)行為。讓我們研究?jī)煞N情況:

  1. 您需要驗(yàn)證組件的狀態(tài)
  2. 您需要制作可變組件的副本。

使用緊湊構(gòu)造函數(shù)

可以使用兩種不同的語法來重新定義record的標(biāo)準(zhǔn)構(gòu)造函數(shù)??梢允褂镁o湊構(gòu)造函數(shù)或標(biāo)準(zhǔn)構(gòu)造函數(shù)本身。

假設(shè)您有以下record。

public record Range(int start, int end) {}

對(duì)于該名稱的record,應(yīng)該預(yù)期 end大于start .您可以通過在record中編寫緊湊構(gòu)造函數(shù)來添加驗(yàn)證規(guī)則。

public record Range(int start, int end) {

    public Range {//不需要參數(shù)塊
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
    }
}

緊湊構(gòu)造函數(shù)不需要聲明其參數(shù)塊。

請(qǐng)注意,如果選擇此語法,則無法直接分配record的字段,例如this.start = start - 這是通過編譯器添加代碼為您完成的。 但是,您可以為參數(shù)分配新值,這會(huì)導(dǎo)致相同的結(jié)果,因?yàn)榫幾g器生成的代碼隨后會(huì)將這些新值分配給字段。

public Range {
    // set negative start and end to 0
    // by reassigning the compact constructor's
    // implicit parameters
    if (start < 0)
        start = 0;//無法給this.start賦值
    if (end < 0)
        end = 0;
}

使用標(biāo)準(zhǔn)構(gòu)造函數(shù)

如果您更喜歡非緊湊形式(例如,因?yàn)槟幌胫匦路峙鋮?shù)),則可以自己定義標(biāo)準(zhǔn)構(gòu)造函數(shù),如以下示例所示。

public record Range(int start, int end) {//跟緊湊構(gòu)造不能共存

    public Range(int start, int end) {
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
        if (start < 0) {
            this.start = 0;
        } else {
            this.start = start;
        }
        if (end > 100) {
            this.end = 10;
        } else {
            this.end = end;
        }
    }
}

這種情況下,您編寫的構(gòu)造函數(shù)需要為record的字段手動(dòng)賦值。

如果record的組件是可變的,則應(yīng)考慮在標(biāo)準(zhǔn)構(gòu)造函數(shù)和訪問器中制作它們的副本。

自定義構(gòu)造函數(shù)

還可以向record添加自定義構(gòu)造函數(shù),只要此構(gòu)造函數(shù)內(nèi)調(diào)用record的標(biāo)準(zhǔn)構(gòu)造函數(shù)即可。語法與經(jīng)典語法相同。對(duì)于任何類,調(diào)用this()必須是構(gòu)造函數(shù)的第一個(gè)語句。

讓我們檢查以下Staterecord。它由三個(gè)組件定義:

  1. 此州的名稱
  2. 該州首府的名稱
  3. 城市名稱列表,可能為空。

我們需要存儲(chǔ)城市列表的副本,確保它不會(huì)從此record的外部修改。 這可以通過使用緊湊形式,將參數(shù)重新分配給副本。

擁有一個(gè)不用城市作參數(shù)的構(gòu)造函數(shù)在您的應(yīng)用程序中很有用。這可以是另一個(gè)構(gòu)造函數(shù),它只接收州名和首都名。第二個(gè)構(gòu)造函數(shù)必須調(diào)用標(biāo)準(zhǔn)構(gòu)造函數(shù)。

然后,您可以將城市作為 vararg 傳遞。為此,您可以創(chuàng)建第三個(gè)構(gòu)造函數(shù)。

public record State(String name, String capitalCity, List<String> cities) {

    public State {
        // List.copyOf returns an unmodifiable copy,
        // so the list assigned to `cities` can't change anymore
        cities = List.copyOf(cities);
    }

    public State(String name, String capitalCity) {
        this(name, capitalCity, List.of());
    }

    public State(String name, String capitalCity, String... cities) {
        this(name, capitalCity, List.of(cities));//也是不可變的
    }

}

請(qǐng)注意,List.copyOf() 方法的參數(shù)不接受空值。

獲取record的狀態(tài)

您不需要向record添加任何訪問器,因?yàn)榫幾g器會(huì)為您執(zhí)行此操作。一條record的每個(gè)組件都有一個(gè)訪問器方法,該方法具有此組件的名稱。

但是,某些情況下,您需要定義自己的訪問器。 例如,假設(shè)上一節(jié)中的Staterecord在構(gòu)造期間沒有創(chuàng)建列表的不可修改的副本 - 那么它應(yīng)該在訪問器中執(zhí)行此操作,以確保調(diào)用方無法改變其內(nèi)部狀態(tài)。 您可以在record中添加以下代碼以返回此副本。

public List<String> cities() {
    return List.copyOf(cities);
}

序列化record

如果您的record類實(shí)現(xiàn)了可序列化,則可以序列化和反序列化record。不過也有限制。

  1. 可用于替換默認(rèn)序列化過程的任何系統(tǒng)都不適用于record。創(chuàng)建 writeObject() 和 readObject() 方法不起作用,也不能實(shí)現(xiàn) Externalizable。
  2. record可用作代理對(duì)象來序列化其他對(duì)象。readResolve() 方法可以返回record。也可以在record中添加 writeReplace()。
  3. 反序列化record始終調(diào)用標(biāo)準(zhǔn)構(gòu)造函數(shù)。因此,在此構(gòu)造函數(shù)中添加的所有驗(yàn)證規(guī)則都將在反序列化record時(shí)強(qiáng)制執(zhí)行。

這使得record在應(yīng)用程序中作為數(shù)據(jù)傳輸對(duì)象非常合適。

在實(shí)際場(chǎng)景中使用record

record是一個(gè)多功能的概念,您可以在許多上下文中使用。

第一種方法是在應(yīng)用程序的對(duì)象模型中攜帶數(shù)據(jù)。用record充當(dāng)不可變的數(shù)據(jù)載體,也是它們的設(shè)計(jì)目的。

由于可以聲明本地record,因此還可以使用它們來提高代碼的可讀性。

讓我們考慮以下場(chǎng)景。您有兩個(gè)建模為record的實(shí)體:City,State

public record City(String name, State state) {}
public record State(String name) {}

假設(shè)您有一個(gè)城市列表,您需要計(jì)算擁有最多城市數(shù)量的州??梢允褂?Stream API 首先使用每個(gè)州擁有的城市數(shù)構(gòu)建各州的柱狀圖。此柱狀圖由Map建模。

List<City> cities = List.of();

Map<State, Long> numberOfCitiesPerState =
    cities.stream()
          .collect(Collectors.groupingBy(
                   City::state, Collectors.counting()
          ));

獲取此柱狀圖的最大值是以下代碼。

Map.Entry<State, Long> stateWithTheMostCities =
    numberOfCitiesPerState.entrySet().stream()
                          .max(Map.Entry.comparingByValue())//最多城市
                          .orElseThrow();

最后一段代碼是技術(shù)性的;它不具有任何業(yè)務(wù)意義;因?yàn)槭褂?code>Map.Entry實(shí)例對(duì)柱狀圖的每個(gè)元素進(jìn)行建模。

使用本地record可以大大改善這種情況。下面的代碼創(chuàng)建一個(gè)新的record類,該類包含一個(gè)州和該州的城市數(shù)。它有一個(gè)構(gòu)造函數(shù),該構(gòu)造函數(shù)將 Map.Entry 的實(shí)例作為參數(shù),將鍵值對(duì)流映射到record流。

由于需要按城市數(shù)比較這些集,因此可以添加工廠方法來提供此比較器。代碼將變?yōu)橐韵聝?nèi)容。

record NumberOfCitiesPerState(State state, long numberOfCities) {

    public NumberOfCitiesPerState(Map.Entry<State, Long> entry) {
        this(entry.getKey(), entry.getValue());//mapping過程
    }

    public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() {
        return Comparator.comparing(NumberOfCitiesPerState::numberOfCities);
    }
}

NumberOfCitiesPerState stateWithTheMostCities =
    numberOfCitiesPerState.entrySet().stream()
                          .map(NumberOfCitiesPerState::new)
                          .max(NumberOfCitiesPerState.comparingByNumberOfCities())//record替換Entry
                          .orElseThrow();

您的代碼現(xiàn)在以有意義的方式提取最大值。您的代碼更具可讀性,更易于理解,不易出錯(cuò),從長(zhǎng)遠(yuǎn)來看更易于維護(hù)。

使用collector作為末端操作

讓我們回到Stream API。

使用collector收集流元素

您已經(jīng)使用了一個(gè)非常有用的模式collect(Collectors.toList())來收集由 List 中的流處理的元素。此 collect() 方法是在 Stream 接口中定義的末端方法,它將 Collector 類型的對(duì)象作為參數(shù)。此Collector接口定義了自己的 API,可用于創(chuàng)建任何類型的內(nèi)存中結(jié)構(gòu)來存儲(chǔ)流處理的數(shù)據(jù)??梢栽?code>Collection或Map的任何實(shí)例中進(jìn)行收集,它可用來創(chuàng)建字符串,并且您可以創(chuàng)建自己的Collector實(shí)例以將自己的結(jié)構(gòu)添加到列表中。

將使用的大多數(shù)collector都可以使用 Collectors 工廠類的工廠方法之一創(chuàng)建。這是您在編寫 Collectors.toList() 或 Collectors.toSet() 時(shí)所做的。使用這些方法創(chuàng)建的一些collector可以組合使用,從而產(chǎn)生更多的collector。本教程涵蓋了所有這些要點(diǎn)。

如果在此工廠類中找不到所需的內(nèi)容,則可以決定通過實(shí)現(xiàn) Collector 接口來創(chuàng)建自己的collector。本教程還介紹了如何實(shí)現(xiàn)此接口。

Collector API 在 Stream 接口和專用數(shù)字流IntStream、LongStreamDoubleStream中的處理方式不同:。Stream 接口有兩個(gè) collect() 方法重載,而數(shù)字流只有一個(gè)。缺少的正是將collector對(duì)象作為參數(shù)的那個(gè)。因此,不能將collector對(duì)象與專用的數(shù)字流一起使用。

在集合中收集

Collectors工廠類提供了三種方法,用于在Collection接口的實(shí)例中收集流的元素。

  1. toList() 將它們收集在 List 對(duì)象中。
  2. toSet() 將它們收集在 Set 對(duì)象中。
  3. 如果需要任何其他Collection實(shí)現(xiàn),可以使用 toCollection(supplier),其中 supplier 參數(shù)將用于創(chuàng)建所需的 Collection 對(duì)象。如果您需要在 LinkedList 實(shí)例中收集您的數(shù)據(jù),您應(yīng)該使用此方法。

代碼不應(yīng)依賴于這些方法當(dāng)前返回的 ListSet 的確切實(shí)現(xiàn),因?yàn)樗皇菢?biāo)準(zhǔn)的一部分。

您還可以使用 unmodifiableList()toUnmodifiableSet() 兩種方法獲取 ListSet 的不可變實(shí)現(xiàn)。

以下示例顯示了此模式的實(shí)際應(yīng)用。首先,讓我們?cè)谝粋€(gè)普通List實(shí)例中收集。

List<Integer> numbers =
IntStream.range(0, 10)
         .boxed()//需要裝箱
         .collect(Collectors.toList());
System.out.println("numbers = " + numbers);

此代碼使用 boxed() 中繼方法從 IntStream.range() 創(chuàng)建的 IntStream 創(chuàng)建一個(gè) Stream,方法是對(duì)該流的所有元素進(jìn)行裝箱。運(yùn)行此代碼將打印以下內(nèi)容。

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

第二個(gè)示例創(chuàng)建一個(gè)只有偶數(shù)且沒有重復(fù)項(xiàng)的 HashSet。

Set<Integer> evenNumbers =
IntStream.range(0, 10)
         .map(number -> number / 2)
         .boxed()
        .collect(Collectors.toSet());
System.out.println("evenNumbers = " + evenNumbers);

運(yùn)行此代碼將產(chǎn)生以下結(jié)果。

evenNumbers = [0, 1, 2, 3, 4]

最后一個(gè)示例使用 Supplier 對(duì)象來創(chuàng)建用于收集流元素的 LinkedList 實(shí)例。

LinkedList<Integer> linkedList =
IntStream.range(0, 10)
         .boxed()
         .collect(Collectors.toCollection(LinkedList::new));
System.out.println("linked listS = " + linkedList);

運(yùn)行此代碼將產(chǎn)生以下結(jié)果。

linked list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

使用collector計(jì)數(shù)

Collectors 工廠類為您提供了幾種方法來創(chuàng)建collector,這些collector執(zhí)行的操作與普通末端方法為您提供的操作相同。Collectors.counting() 工廠方法就是這種情況,它與在流上調(diào)用 count() 相同。

這是值得注意的,您可能想知道為什么使用兩種不同的模式實(shí)現(xiàn)了兩次這樣的功能。將在下一節(jié)有關(guān)在map中收集時(shí)回答此問題,您將在其中組合collector以創(chuàng)建更多collector。

目前,編寫以下兩行代碼會(huì)導(dǎo)致相同的結(jié)果。

Collection<String> strings = List.of("one", "two", "three");

long count = strings.stream().count();
long countWithACollector = strings.stream().collect(Collectors.counting());

System.out.println("count = " + count);
System.out.println("countWithACollector = " + countWithACollector);

運(yùn)行此代碼將產(chǎn)生以下結(jié)果。

count = 3
countWithACollector = 3

收集在字符串中

Collectors 工廠類提供的另一個(gè)非常有用的collector是 joining() 。此collector僅適用于字符串流,并將該流的元素連接為單個(gè)字符串。它有幾個(gè)重載。

  • 第一個(gè)將分隔符作為參數(shù)。
  • 第二個(gè)將分隔符、前綴和后綴作為參數(shù)。

讓我們看看這個(gè)collector的實(shí)際效果。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining());

System.out.println("joined = " + joined);

運(yùn)行此代碼將生成以下結(jié)果。

joined = 0123456789

可以使用以下代碼向此字符串添加分隔符。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining(", "));

System.out.println("joined = " + joined);

結(jié)果如下。

joined = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

讓我們看看最后一個(gè)重載,它接收分隔符、前綴和后綴。

String joined = 
    IntStream.range(0, 10)
             .boxed()
             .map(Object::toString)
             .collect(Collectors.joining(", ", "{"), "}");

System.out.println("joined = " + joined);

結(jié)果如下。

joined = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

請(qǐng)注意,此collector可以正確處理流為空或僅處理單個(gè)元素的極端情況。

當(dāng)您需要生成此類字符串時(shí),此collector非常方便。即使您前面的數(shù)據(jù)不在集合中或只有幾個(gè)元素,您也可能想使用它。如果是這種情況,使用 String.join() 工廠類或 StringJoiner 對(duì)象都將正常工作,無需支付創(chuàng)建流的開銷。

使用Predicate對(duì)元素進(jìn)行分區(qū)

Collector API 提供了三種模式,用于從流的元素創(chuàng)建map。我們介紹的第一個(gè)使用布爾鍵創(chuàng)建map。它是使用 partitionningBy() 工廠方法創(chuàng)建的。

流的所有元素都將綁定到布爾值truefalse。map將綁定到每個(gè)值的所有元素存儲(chǔ)在列表中。因此,如果將此collector應(yīng)用于Stream,它將生成具有以下類型的map:Map<Boolean,List<T>>。

測(cè)試的Predicate應(yīng)作為參數(shù)提供給collector。

下面的示例演示此collector的操作。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Boolean, List<String>> map =
    strings.stream()
           .collect(Collectors.partitioningBy(s -> s.length() > 4));

map.forEach((key, value) -> System.out.println(key + " :: " + value));

運(yùn)行此代碼將生成以下結(jié)果。

false :: [one, two, four, five, six, nine, ten]
true :: [three, seven, eight, eleven, twelve]

此工廠方法具有重載,它將另一個(gè)collector作為參數(shù)。此collector稱為下游collector。我們將在本教程的下一段中介紹,屆時(shí)我們將介紹 groupingBy() 。

在map中收集并進(jìn)行分組

我們提供的第二個(gè)collector非常重要,因?yàn)樗试S您創(chuàng)建柱狀圖。

對(duì)map中的流元素進(jìn)行分組

可用于創(chuàng)建柱狀圖的collector是使用 Collectors.groupingBy() 方法創(chuàng)建的。此方法具有多個(gè)重載。

collector將創(chuàng)建map。通過對(duì)其應(yīng)用 Function 實(shí)例,為流的每個(gè)元素計(jì)算一個(gè)鍵。此函數(shù)作為 groupingBy() 方法的參數(shù)提供。它在Collector API 中稱為分類器 classifier

除了不應(yīng)該返回 null 之外,此函數(shù)沒有任何限制。

此函數(shù)可能會(huì)為流的多個(gè)元素返回相同的鍵。groupingBy() 支持這一點(diǎn),并將所有這些元素收集在一個(gè)列表中。

因此,如果您正在處理 Stream 并使用 Function<T, K> 作為分類器,則 groupingBy() 會(huì)創(chuàng)建一個(gè) Map<K,List<T>>。

讓我們檢查以下示例。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, List<String>> map =
    strings.stream()
           .collect(Collectors.groupingBy(String::length));//返回<Integer, List<String>>

map.forEach((key, value) -> System.out.println(key + " :: " + value));

此示例中使用的分類器是一個(gè)函數(shù),用于從該流返回每個(gè)字符串的長(zhǎng)度。因此,map按字符串長(zhǎng)度將字符串分組到列表中。它具有Map<Interger,List<String>>的類型。

運(yùn)行此代碼將打印以下內(nèi)容。

3 :: [one, two, six, ten]
4 :: [four, five, nine]
5 :: [three, seven, eight]
6 :: [eleven, twelve]

對(duì)分組后的值進(jìn)行處理

計(jì)算數(shù)量

groupingBy() 方法還接受另一個(gè)參數(shù),即另一個(gè)collector。此collector在Collector API 中稱為下游collector,但它沒有什么特別的。使它成為下游collector的原因只是,它作為參數(shù)傳遞給前一個(gè)collector的創(chuàng)建。

此下游collector用于收集由 groupingBy() 創(chuàng)建的map的值。

在前面的示例中,groupingBy() 創(chuàng)建了一個(gè)map,其值是字符串列表。如果為 groupingBy() 方法提供下游collector,API 將逐個(gè)流式傳輸這些列表,并使用下游collector收集這些流。

假設(shè)您將 Collectors.counting() 作為下游collector傳遞。將計(jì)算的內(nèi)容如下。

[one, two, six, ten]  .stream().collect(Collectors.counting()) -> 4L
[four, five, nine]    .stream().collect(Collectors.counting()) -> 3L
[three, seven, eight] .stream().collect(Collectors.counting()) -> 3L
[eleven, twelve]      .stream().collect(Collectors.counting()) -> 2L

此代碼不是 Java 代碼,因此您無法執(zhí)行它。它只是在那里解釋如何使用這個(gè)下游collector。

下面將創(chuàng)建的map取決于您提供的下游collector。鍵不會(huì)修改,但值可能會(huì)。在 Collectors.counting() 的情況下,值將轉(zhuǎn)換為 Long。然后,map的類型將變?yōu)?Map<Integer,Long>。

前面的示例變?yōu)橐韵聝?nèi)容。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Map<Integer, Long> map =
    strings.stream()
           .collect(
               Collectors.groupingBy(
                   String::length, 
                   Collectors.counting()));//List<String>轉(zhuǎn)為Stream向下傳遞,變成Long

map.forEach((key, value) -> System.out.println(key + " :: " + value));

運(yùn)行此代碼將打印以下結(jié)果。它給出了每個(gè)長(zhǎng)度的字符串?dāng)?shù),這是字符串長(zhǎng)度的柱狀圖。

3 :: 4
4 :: 3
5 :: 3
6 :: 2

連接列表的值

您還可以將 Collectors.joining() collector作為下游collector傳遞,因?yàn)榇薽ap的值是字符串列表。請(qǐng)記住,此collector只能用于字符串流。這將創(chuàng)建 Map<Integer,String> 的實(shí)例。您可以將上一個(gè)示例更改為以下內(nèi)容。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Map<Integer, String> map =
        strings.stream()
                .collect(
                        Collectors.groupingBy(
                                String::length,
                                Collectors.joining(", ")));//變成String
map.forEach((key, value) -> System.out.println(key + " :: " + value));

運(yùn)行此代碼將生成以下結(jié)果。

3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve

控制map的實(shí)例

groupingBy() 方法的最后一個(gè)重載將supplier的實(shí)例作為參數(shù),以便您控制需要此collector創(chuàng)建的 Map 實(shí)例。

您的代碼不應(yīng)依賴于 groupingBy() 返回的確切map類型,因?yàn)樗皇菢?biāo)準(zhǔn)的一部分。

使用ToMap在map中收集

Collector API 為您提供了創(chuàng)建map的第二種模式:Collectors.toMap() 模式。此模式適用于兩個(gè)函數(shù),這兩個(gè)函數(shù)都應(yīng)用于流的元素。

  1. 第一個(gè)稱為密鑰mapper,用于創(chuàng)建密鑰。
  2. 第二個(gè)稱為值mapper,用于創(chuàng)建值。

此collector的使用場(chǎng)景與 Collectors.groupingBy() 不同。特別是,它不處理流的多個(gè)元素生成相同密鑰的情況。這種情況下,默認(rèn)情況下會(huì)引發(fā)IllegalStateException。

這個(gè)collector能非常方便的創(chuàng)建緩存。假設(shè)User類有一個(gè)類型為 LongprimaryKey屬性。您可以使用以下代碼創(chuàng)建User對(duì)象的緩存。

List<User> users = ...;

Map<Long, User> userCache = 
    users.stream()
         .collect(User::getPrimaryKey, 
                 Function.idendity());//key必須不同

使用 Function.identity() 工廠方法只是告訴collector不要轉(zhuǎn)換流的元素。

如果您希望流的多個(gè)元素生成相同的鍵,則可以將進(jìn)一步的參數(shù)傳遞給 toMap() 方法。此參數(shù)的類型為 BinaryOperator。當(dāng)檢測(cè)到?jīng)_突元素時(shí),實(shí)現(xiàn)將它應(yīng)用于沖突元素。然后,您的binary operator將生成一個(gè)結(jié)果,該結(jié)果將代替先前的值放入map中。

下面演示如何使用具有沖突值的此collector。此處的值用分隔符連接在一起。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, String> map =
    strings.stream()
            .collect(
                    Collectors.toMap(
                            element -> element.length(),
                            element -> element, 
                            (element1, element2) -> element1 + ", " + element2));//相同key,解決沖突,返回新值

map.forEach((key, value) -> System.out.println(key + " :: " + value));

在此示例中,傳遞給 toMap() 方法的三個(gè)參數(shù)如下:

  1. element -> element.length()鍵mapper。
  2. element -> element值mapper
  3. (element1, element2) -> element1 + ", " + element2)合并函數(shù),相同鍵的兩個(gè)元素會(huì)調(diào)用。

運(yùn)行此代碼將生成以下結(jié)果。

3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve

另外也可以將supplier作為參數(shù)傳遞給 toMap() 方法,以控制此collector將使用的 Map 接口實(shí)例。

toMap() collector有一個(gè)孿生方法 toConcurrentMap(),它將在并發(fā)map中收集數(shù)據(jù)。實(shí)現(xiàn)不保證map的確切類型。

從柱狀圖中提取最大值

groupingBy() 是分析計(jì)算柱狀圖的最佳模式。讓我們研究一個(gè)完整的示例,其中您構(gòu)建柱狀圖,然后嘗試根據(jù)要求找到其中的最大值。

提取唯一的最大值

您要分析的柱狀圖如下。它看起來像我們?cè)谇懊娴氖纠惺褂玫哪莻€(gè)。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
            .collect(
                    Collectors.groupingBy(
                            String::length,
                            Collectors.counting()));

histogram.forEach((key, value) -> System.out.println(key + " :: " + value));

打印此柱狀圖將得到以下結(jié)果。

3 :: 4 //期望是4 =>3
4 :: 3
5 :: 3
6 :: 2

從此柱狀圖中提取最大值應(yīng)得到結(jié)果:3 :: 4。Stream API 具有提取最大值所需的所有工具。不幸的是,Map接口上沒有stream()方法。要在map上創(chuàng)建流,您首先需要獲取可以從map獲取的集合之一。

  1. entrySet() 方法的映射集。
  2. keySet() 方法的鍵集。
  3. 或者使用 values() 方法收集值。

這里你需要鍵和最大值,所以正確的選擇是流式傳輸 entrySet() 返回的集合。

您需要的代碼如下。

Map.Entry<Integer, Long> maxValue =
    histogram.entrySet().stream()
             .max(Map.Entry.comparingByValue())
             .orElseThrow();

System.out.println("maxValue = " + maxValue);

您可以注意到,此代碼使用 Stream 接口中的 max() 方法,該方法將comparator作為參數(shù)。實(shí)際上,Map.Entry 接口的確有幾個(gè)工廠方法來創(chuàng)建這樣的comparator。我們?cè)诖耸纠惺褂玫倪@個(gè),創(chuàng)建了一個(gè)可以比較 Map.Entry 實(shí)例的comparator,使用這些鍵值對(duì)的值。僅當(dāng)值實(shí)現(xiàn)Comparable接口時(shí),此比較才有效。

這種代碼模式非常普通,只要具有可比較的值,就可以在任何map上使用。我們可以使其特別一點(diǎn),更具可讀性,這要?dú)w功于Java SE 16中記錄Record的引入。

讓我們創(chuàng)建一個(gè)record來模擬此map的鍵值對(duì)。創(chuàng)建record只需要一行。由于該語言允許local records,因此您可以到任何方法中。

record NumberOfLength(int length, long number) {
    
    static NumberOfLength fromEntry(Map.Entry<Integer, Long> entry) {
        return new NumberOfLength(entry.getKey(), entry.getValue());//mapping過程
    }

    static Comparator<NumberOfLength> comparingByLength() {
        return Comparator.comparing(NumberOfLength::length);
    }
}

使用此record,以前的模式將變?yōu)橐韵聝?nèi)容。

NumberOfLength maxNumberOfLength =
    histogram.entrySet().stream()
             .map(NumberOfLength::fromEntry)
             .max(NumberOfLength.comparingByLength())//Record替換Entry,后面要引用字段
             .orElseThrow();

System.out.println("maxNumberOfLength = " + maxNumberOfLength);

運(yùn)行此示例將打印出以下內(nèi)容。

maxNumberOfLength = NumberOfLength[length=3, number=4]

您可以看到此record看起來像 Map.Entry 接口。它有一個(gè)mapping鍵值對(duì)的工廠方法和一個(gè)用于創(chuàng)建comparator的工廠方法。柱狀圖的分析變得更加可讀和易于理解。

提取多個(gè)最大值

前面的示例是一個(gè)很好的示例,因?yàn)榱斜碇兄挥幸粋€(gè)最大值。不幸的是,現(xiàn)實(shí)生活中的情況通常不是那么好,您可能有幾個(gè)與最大值匹配的鍵值對(duì)。

讓我們從上一個(gè)示例的集合中刪除一個(gè)元素。

Collection<String> strings =
    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
            .collect(
                    Collectors.groupingBy(
                            String::length,
                            Collectors.counting()));

histogram.forEach((key, value) -> System.out.println(key + " :: " + value));

打印此柱狀圖將得到以下結(jié)果。

3 :: 3
4 :: 3
5 :: 3//期望是3 =>[3,4,5]
6 :: 2

現(xiàn)在我們有三個(gè)鍵值對(duì)的最大值。如果使用前面的代碼模式提取它,則將選擇并返回這三個(gè)中的一個(gè),隱藏其他兩個(gè)。

解決此問題的解決方案是創(chuàng)建另一個(gè)map,其中鍵是字符串?dāng)?shù)量,值是與之匹配的長(zhǎng)度。換句話說:您需要反轉(zhuǎn)此map。對(duì)于 groupingBy() 來說,這是一個(gè)很好的場(chǎng)景。此示例將在本部分的后面介紹,因?yàn)槲覀冞€需要一個(gè)元素來編寫此代碼。

使用中繼collector

到目前為止,我們介紹的collector只是計(jì)數(shù)、連接和收集到列表或map中。它們都屬于末端操作。Collector API 也提供了執(zhí)行中繼操作的其他collector:mapping、filtering和flatmapping。您可能想知道這樣的意義是什么。事實(shí)上,這些特殊的collector并不能單獨(dú)創(chuàng)建。它們的工廠方法都需要下游collector作為第二個(gè)參數(shù)。

也就是說,您這樣創(chuàng)建的整體collector是中繼操作和末端操作的組合。

使用collector來mapping

我們可以檢查的第一個(gè)中繼操作是mapping操作。mapping collector是使用 Collectors.mapping() 工廠方法創(chuàng)建的。它將常規(guī)mapping函數(shù)作為第一個(gè)參數(shù),將必需的下游collector作為第二個(gè)參數(shù)。

在下面的示例中,我們將mapping與列表中mapping后的元素的集合相結(jié)合。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

List<String> result = 
    strings.stream()
        .collect(
            Collectors.mapping(String::toUpperCase, Collectors.toList()));//集成了mapping

System.out.println("result = " + result);

Collectors.mappping() 工廠方法創(chuàng)建一個(gè)常規(guī)collector。您可以將此collector作為下游collector傳遞給任何接受collector的方法,例如,包括 groupingBy()toMap()。您可能還記得在“提取多個(gè)最大值”一節(jié)中,我們留下了一個(gè)關(guān)于反轉(zhuǎn)map的懸而未決的問題。讓我們使用這個(gè)mapping collector來解決問題。

在此示例中,您創(chuàng)建了一個(gè)柱狀圖?,F(xiàn)在,您需要使用 groupingBy() 反轉(zhuǎn)此柱狀圖以查找所有最大值。

以下代碼創(chuàng)建此類map。

Map<Integer, Long> histogram = ...; 

var map = 
    histogram.entrySet().stream()
        .map(NumberOfLength::fromEntry)
        .collect(
            Collectors.groupingBy(NumberOfLength::number));//<Long, List<NumberOfLength>>

讓我們檢查此代碼并確定所構(gòu)建map的確切類型。

此map的鍵是每個(gè)長(zhǎng)度在原始流中存在的次數(shù)。它是NumberOfLengthrecord的number部分,Long。類型。

這些值是此流的元素,收集到列表中。因此,是NumberOfLength的對(duì)象列表。這張map的確切類型是Map<Long,NumberOfLength>。

當(dāng)然,這不是您所要的。您需要的只是字符串的長(zhǎng)度,而不是record。從record中提取組件是一個(gè)mapping過程。您需要將這些NumberOfLength實(shí)例mapping為其length組件?,F(xiàn)在我們介紹了mapping collector,可以解決這一點(diǎn)。您需要做的就是將正確的下游collector添加到 groupingBy() 調(diào)用中。

代碼將變?yōu)橐韵聝?nèi)容。

Map<Integer, Long> histogram = ...; 

var map = 
    histogram.entrySet().stream()
        .map(NumberOfLength::fromEntry)
        .collect(
            Collectors.groupingBy(
                NumberOfLength::number, 
                Collectors.mapping(NumberOfLength::length, Collectors.toList())));//<Long, List<Integer>>

構(gòu)建的map的值現(xiàn)在是使用NumberOfLength::length對(duì)NumberOfLength做mapping后生成的對(duì)象列表。此map的類型為Map<Long,List<Integer>>,這正是您所需要的。

要獲取所有最大值,您可以像之前那樣,使用 key 獲取最大值而不是值。

柱狀圖中的完整代碼,包括最大值提取,如下所示。

Map<Long, List<Integer>> map =
    histogram.entrySet().stream()
             .map(NumberOfLength::fromEntry)
             .collect(
                Collectors.groupingBy(
                    NumberOfLength::number,//變成了number=>length列表
                    Collectors.mapping(NumberOfLength::length, Collectors.toList())));

Map.Entry<Long, List<Integer>> result =
    map.entrySet().stream()
       .max(Map.Entry.comparingByKey())//再求key的max
       .orElseThrow();

System.out.println("result = " + result);

運(yùn)行此代碼將生成以下內(nèi)容。

result = 3=[3, 4, 5]//最多的length列表

這意味著有三種長(zhǎng)度的字符串在此流中出現(xiàn)三次:3、4 和 5。

此示例顯示嵌套在另外兩個(gè)collector中的collector,在使用此 API 時(shí),這種情況經(jīng)常發(fā)生。乍一看可能看起來很嚇人,但它只是使用下游collector組合成了collector。

您可以看到為什么擁有這些中繼collector很有趣。通過使用collector提供的中繼操作,您可以為幾乎任何類型的處理創(chuàng)建下游collector,從而對(duì)map的值進(jìn)行后續(xù)處理。

使用collector進(jìn)行filtering和flatmapping

filtering collector遵循與mapping collector相同的模式。它是使用 Collectors.filtering() 工廠方法創(chuàng)建的,該方法接收常規(guī)Predicate來filter數(shù)據(jù),同時(shí)要有必需的下游collector。

Collectors.flatMapping() 工廠方法創(chuàng)建的flatmapping collector也是如此,它接收flatmapping函數(shù)(返回流的函數(shù))和必需的下游collector。

使用末端collector

Collector API 還提供了幾個(gè)末端操作,對(duì)應(yīng)于Stream API 上可用的末端操作。

  • maxBy()minBy()。這兩個(gè)方法都將comparator作為參數(shù),如果處理的流本身為空,則返回一個(gè)Optional對(duì)象。
  • summingInt()、summingLong()summingDouble()。這三種方法將mapping函數(shù)作為參數(shù),分別將流的元素mapping為int,long,double ,然后對(duì)它們求和。
  • averageagingInt()、 averageagingLong()averageagingDouble().這三種方法也將mapping函數(shù)作為參數(shù),在計(jì)算平均值之前分別將流的元素map為 int,long,double。這些collector的工作方式與 IntStream、LongStreamDoubleStream 中定義的相應(yīng) average() 方法不同。它們都返回一個(gè) Double 實(shí)例,對(duì)于空流返回 0。而數(shù)字流的average() 方法返回一個(gè)Optional對(duì)象,該對(duì)象對(duì)于空流為空。

創(chuàng)建自己的collector

了解collector的工作原理

如前所述,Collectors工廠類僅處理對(duì)象流,因?yàn)閷ollector對(duì)象作為參數(shù)的 collect() 方法僅存在于 Stream 中。如果您需要收集數(shù)字流,那么您需要了解collector的組成元素是什么。

簡(jiǎn)單說,collector建立在四個(gè)基本組件之上。前兩個(gè)用于收集流的元素。第三個(gè)僅用于并行流。某些類型的collector需要第四個(gè),這些collector需要對(duì)構(gòu)建的容器作后續(xù)處理。

第一個(gè)組件用于創(chuàng)建收集流元素的容器。此容器易于識(shí)別。例如,在上一部分介紹的情況下,我們使用了 ArrayList 類、HashSet 類和 HashMap 類??梢允褂?code>supplier實(shí)例對(duì)創(chuàng)建此類容器進(jìn)行建模。第一個(gè)組件稱為supplier。

第二個(gè)組件旨在將流中的單個(gè)元素添加到容器。Stream API 的實(shí)現(xiàn)將重復(fù)調(diào)用此操作,將流的所有元素逐個(gè)添加到容器中。

在Collector API中,此組件由BiConsumer的實(shí)例建模。這個(gè)biconsumer有兩個(gè)參數(shù)。

  1. 第一個(gè)是容器本身,流的先前元素填充了部分。
  2. 第二個(gè)是應(yīng)添加的流元素。

此biconsumer在Collector API 的上下文中稱為accumulator。

這兩個(gè)組件應(yīng)該足以讓collector工作,但 Stream API 帶來了一個(gè)約束,使collector正常工作需要另外兩個(gè)組件。

你可能還記得,Stream API 支持并行化。本教程稍后將更詳細(xì)地介紹這一點(diǎn)。您需要知道的是,并行化將流的元素拆分為子流,每個(gè)元素都由 CPU 的內(nèi)核處理。Collector API 可以在這樣的上下文中工作:每個(gè)子流將只收集在自己的容器實(shí)例中。

處理完這些子流后,您將擁有多個(gè)容器,每個(gè)容器都包含它所處理的子流中的元素。這些容器是相同的,因?yàn)樗鼈兪桥c同一supplier一起創(chuàng)建的。現(xiàn)在,您需要一種方法將它們合并為一個(gè)。為了能夠做到這一點(diǎn),Collector API 需要第三個(gè)組件,即combiner,它將這些容器合并在一起。combiner由 BinaryOperator 的實(shí)例建模,該實(shí)例接收兩個(gè)部分填充的容器并返回一個(gè)。

Stream API 的 collect() 也有個(gè)重載,這個(gè) BinaryOperator 變成了 BiConsumer ,我們主要使用這個(gè)。

第四個(gè)組件稱為finisher,本部分稍后將介紹。

在集合中收集原始類型

使用前三個(gè)組件,您可以嘗試專用數(shù)字流中的 collect() 方法。IntStream.collect() 方法有三個(gè)參數(shù):

  • Supplier的實(shí)例,稱為supplier;
  • ObjIntConsumer的實(shí)例,稱為accumulator;
  • BiConsumer的實(shí)例,稱為combiner。

讓我們編寫代碼以在List<Integer>中收集IntStream。

Supplier<List<Integer>> supplier                  = ArrayList::new;//容器
ObjIntConsumer<List<Integer>> accumulator         = Collection::add;//元素如何進(jìn)入容器
BiConsumer<List<Integer>, List<Integer>> combiner = Collection::addAll;//多個(gè)片段如何合并

List<Integer> collect =
    IntStream.range(0, 10)
             .collect(supplier, accumulator, combiner );

System.out.println("collect = " + collect);

運(yùn)行此代碼將生成以下結(jié)果。

collect = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

將這些數(shù)據(jù)收集為Set只需要更改supplier的實(shí)現(xiàn)并相應(yīng)地調(diào)整類型。

在 StringBuffer 中收集原始類型

讓我們研究一下如何自己實(shí)現(xiàn) Collectors.joining() ,以將原始類型流的元素連接在單個(gè)字符串中。String 類是不可變的,因此無法在其中累積元素。您可以使用可變的 StringBuffer 類。

StringBuffer 中收集元素遵循與前一個(gè)相同的模式。

Supplier<StringBuffer> supplier                 = StringBuffer::new;//
ObjIntConsumer<StringBuffer> accumulator        = StringBuffer::append;
BiConsumer<StringBuffer, StringBuffer> combiner = StringBuffer::append;

StringBuffer collect = 
    IntStream.range(0, 10)
             .collect(supplier, accumulator, combiner);

System.out.println("collect = " + collect);

運(yùn)行此代碼將生成以下結(jié)果。

collect = 0123456789

使用finisher對(duì)collector進(jìn)行后續(xù)處理

你在上一段中編寫的代碼幾乎完成了你需要的:它在 StringBuffer 實(shí)例中連接字符串,你可以通過調(diào)用它的 toString() 方法來創(chuàng)建一個(gè)常規(guī)的 String 對(duì)象。但是 Collectors.joining() collector直接生成一個(gè)字符串,而無需你調(diào)用 toString()。那么它是怎么做到的呢?

Collector API 精確地定義了第四個(gè)組件來處理這種情況,稱為finisher。finisher是一個(gè)Function,它獲取累積元素的容器并將其轉(zhuǎn)換為其他內(nèi)容。在 Collectors.joining() 的情況下,這個(gè)函數(shù)只是下面的。

Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();

對(duì)很多collector來說,finisher只是恒等函數(shù)。比如:toList()toSet()、groupingBy()toMap()。

其他情況下,collector內(nèi)部使用的可變?nèi)萜鞒蔀橹欣^容器,在返回到應(yīng)用程序之前,該容器將mapping為其他對(duì)象(可能是另一個(gè)容器)。這就是Collector API 處理不可變列表、set或map創(chuàng)建的方式。finisher用于將中繼容器密封到不可變?nèi)萜髦?,返回到?yīng)用程序。

finisher還有其他用途,可以提高代碼的可讀性。Collectors 工廠類有一個(gè)工廠方法,我們還沒有介紹:collectingAndThen() 方法。此方法將collector作為第一個(gè)參數(shù),將finisher作為第二個(gè)參數(shù)。它會(huì)將第一個(gè)collector收集的結(jié)果,使用您提供的finisher對(duì)其進(jìn)行mapping。

您可能還記得以下示例,我們已經(jīng)在前面的部分中多次檢查過該示例。它是關(guān)于提取柱狀圖的最大值。

Collection<String> strings =
    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten", "eleven", "twelve");

Map<Integer, Long> histogram =
    strings.stream()
           .collect(
                   Collectors.groupingBy(
                           String::length,
                           Collectors.counting()));

Map.Entry<Integer, Long> maxValue =
    histogram.entrySet().stream()
             .max(Map.Entry.comparingByValue())
             .orElseThrow();

System.out.println("maxValue = " + maxValue);

第一步,您構(gòu)建了 Map<Integer,Long> 類型的柱狀圖,在第二步中,您提取了此柱狀圖的最大值,按值比較鍵值對(duì)。

第二步實(shí)際上是將map轉(zhuǎn)換為特殊的鍵/值對(duì)。您可以使用以下函數(shù)對(duì)其進(jìn)行建模。

Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher = 
    map -> map.entrySet().stream()
              .max(Map.Entry.comparingByValue())
              .orElseThrow();

此函數(shù)的類型起初可能看起來很復(fù)雜。事實(shí)上,它只是從map中提取一個(gè)鍵值對(duì),類型為 Map.Entry

現(xiàn)在您已經(jīng)有了這個(gè)函數(shù),您可以使用 collectingAndThen() 將此最大值提取步驟集成到collector本身中。然后,模式將變?yōu)橐韵聝?nèi)容。

Collection<String> strings =
        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher =
    map -> map.entrySet().stream()
              .max(Map.Entry.comparingByValue())
              .orElseThrow();//提取此finisher需要特別注意類型,消費(fèi)Map,產(chǎn)出Entry

Map.Entry<Integer, Long> maxValue =
    strings.stream()
           .collect(
               Collectors.collectingAndThen(
                   Collectors.groupingBy(
                           String::length,
                           Collectors.counting()),
                   finisher
               ));

System.out.println("maxValue = " + maxValue);

您可能想知道為什么需要編寫此看起來非常復(fù)雜的代碼?

現(xiàn)在,您已經(jīng)擁有了由單個(gè)collector建模的最大值提取器,您可以將其用作另一個(gè)collector的下游collector。做到這一點(diǎn),可以組合更多的collector對(duì)您的數(shù)據(jù)進(jìn)行更復(fù)雜的計(jì)算。

將兩個(gè)collector的結(jié)果與三通collector相結(jié)合

在 Java SE 12 的 Collectors 類中添加了一個(gè)名為 teeing() 的方法。此方法需要兩個(gè)下游collector和一個(gè)合并函數(shù)。

讓我們通過一個(gè)場(chǎng)景,看看您可以使用collector做什么。想象一下,您有以下Car,Truck兩種record。

enum Color {
    RED, BLUE, WHITE, YELLOW
}

enum Engine {
    ELECTRIC, HYBRID, GAS
}

enum Drive {
    WD2, WD4
}

interface Vehicle {}

record Car(Color color, Engine engine, Drive drive, int passengers) {}

record Truck(Engine engine, Drive drive, int weight) {}

Car對(duì)象有幾個(gè)組成部分:顏色、引擎、驅(qū)動(dòng)器以及它可以運(yùn)輸?shù)囊欢〝?shù)量的乘客。Truck有引擎,有驅(qū)動(dòng)器,可以運(yùn)輸一定量的貨物。兩者都實(shí)現(xiàn)相同的接口:Vehicle

假設(shè)您有一系列Vehicle,您需要找到所有配備電動(dòng)引擎的Car。根據(jù)您的應(yīng)用程序,您可能會(huì)使用流filter您的Car集合。或者,如果您知道下一個(gè)需求,將是找到配備混合動(dòng)力引擎的Car,您可能更愿意準(zhǔn)備一個(gè)map,以引擎為鍵,并以配備該引擎的Car列表作為值。在這兩種情況API 都會(huì)為你提供正確的模式來獲取所需的內(nèi)容。

假設(shè)您需要將所有電動(dòng)Truck添加到此集合中。也有可能想一次處理所有Vehicle,但是用于filter數(shù)據(jù)的Predicate變得越來越復(fù)雜。它可能如下所示。

Predicate<Vehicle> predicate =
    vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC ||
               vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;
//這個(gè)是instanceof新用法,后面直接賦值變量,同時(shí)跟短路操作

您真正需要的是以下內(nèi)容:

  1. filterVehicle以獲得所有電動(dòng)Car
  2. filterVehicle以獲得所有電動(dòng)Truck
  3. 合并兩個(gè)結(jié)果。

這正是teeing collector可以為您做的事情。teeing collector由 Collectors.teeing() 工廠方法創(chuàng)建,該方法接收三個(gè)參數(shù)。

  1. 第一個(gè)下游collector,用于收集流的數(shù)據(jù)。
  2. 第二個(gè)下游collector,也用于收集數(shù)據(jù)。
  3. 一個(gè)bifunction,用于合并由兩個(gè)下游collector創(chuàng)建的兩個(gè)容器。

您的數(shù)據(jù)將一次性處理,以保證最佳性能。

我們已經(jīng)介紹了使用collector來filter流元素的模式。合并函數(shù)只是對(duì) Collection.addAll() 方法的調(diào)用。以下是代碼:

List<Vehicle> electricVehicles = vehicles.stream()
    .collect(
        Collectors.teeing(
            Collectors.filtering(
                vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC,
                Collectors.toList()),
            Collectors.filtering(
                vehicle -> vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC,
                Collectors.toList()),
            (cars, trucks) -> {
                cars.addAll(trucks);
                return cars;
            }));

實(shí)現(xiàn)collector接口

為什么要實(shí)現(xiàn)collector接口?

有三種方法可以創(chuàng)建自己的collector。

包括將現(xiàn)有collector與Collectors工廠類結(jié)合,將collector作為下游collector傳遞給另一個(gè)collector,或者作為finisher一起使用 collectingAndThen() 。我們上面教程中已經(jīng)介紹過。

您還可以調(diào)用 collect() 方法,該方法接收構(gòu)建collector的三個(gè)元素。這些方法在原始類型流和對(duì)象流上都可用。他們接收了我們?cè)谇懊娌糠种刑岢龅娜齻€(gè)參數(shù)。

  1. 用于創(chuàng)建可變?nèi)萜鞯?em>supplier,其中累積了流的元素。
  2. accumulator,由biconsumer建模。
  3. combiner也由biconsumer建模,用于組合兩個(gè)部分填充的容器,用于并行流的情況。

第三種方法是自己實(shí)現(xiàn) Collector 接口,并將您的實(shí)現(xiàn)傳遞給我們已經(jīng)介紹過的 collect() 方法。實(shí)現(xiàn)自己的collector可以為您提供最大的靈活性,但也更具技術(shù)性。

了解collector的參數(shù)類型

讓我們檢查一下這個(gè)接口的參數(shù)。

interface Collector<T, A, R> {
    
    // content of the interface
}

讓我們首先檢查以下類型:T,R

第一種類型是 ,它對(duì)應(yīng)于此collector正在處理的流元素的類型。T

最后一個(gè)類型是 ,它是此collector生成的類型。R

比如在 Stream 實(shí)例上調(diào)用的 toList() collector,類型RList。它 toSet() collector將是 Set。

groupingBy() 方法接收一個(gè)函數(shù)作參數(shù),來計(jì)算返回map的鍵。如果用它收集 Stream,則需要傳遞一個(gè)對(duì)T實(shí)例作mapping 的函數(shù)。因此,生成的map的類型將為 Map<K,List<T>>。也就是R的類型。

A類型處理起來比較復(fù)雜。您可能已嘗試使用 IDE 來存儲(chǔ)您在前面的示例中創(chuàng)建的collector之一。如果這樣做,您可能意識(shí)到 IDE 沒有為此類型提供顯式值。以下示例就是這種情況。

Collection<String> strings =
        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
                "ten", "eleven", "twelve");

Collector<String, ?, List<String>> listCollector = Collectors.toList();
List<String> list = strings.stream().collect(listCollector);

Collector<String, ?, Set<String>> setCollector = Collectors.toSet();
Set<String> set = strings.stream().collect(setCollector);

Collector<String, ?, Map<Integer, Long>> groupingBy = 
        Collectors.groupingBy(String::length, Collectors.counting());
Map<Integer, Long> map = strings.stream().collect(groupingBy);

對(duì)于所有這些collector,第二個(gè)參數(shù)類型僅為 ?

如果需要實(shí)現(xiàn)Collector接口,則必須為A提供個(gè)顯式值。A是此collector使用的中繼可變?nèi)萜鞯膶?shí)際類型。對(duì)于 toList() collector,它將是 ArrayList,對(duì)于 toSet() collector,它將是 HashSet。事實(shí)上,此類型被 toList() 的返回類型隱藏了,這就是為什么在前面的示例中無法將 ?類型替換為 ArrayList 的原因。

即使內(nèi)部可變?nèi)萜魇怯蓪?shí)現(xiàn)直接返回的,也可能發(fā)生類型AR不同的情況。例如, toList() ,您可以通過修改 ArrayList 和 List 來實(shí)現(xiàn) Collector><T,A,R> 接口。

了解collector的特征

collector定義了內(nèi)部特征,流實(shí)現(xiàn)用它來優(yōu)化collector使用。

有三個(gè)。

  1. IDENTITY_FINISH指示此collector的finisher是恒等函數(shù)。該實(shí)現(xiàn)不會(huì)為具有此特征的collector調(diào)用finisher。
  2. UNORDERED指示此collector不保留它處理流元素的順序。toSet() collector就是這種情況。而toList() 就沒有。
  3. CONCURRENT 特性表示accumulator用來存儲(chǔ)已處理元素的容器支持并發(fā)訪問。這一點(diǎn)對(duì)于并行流很重要。

這些特征在collectorCollector.Characteristics枚舉中定義,并由Collector接口的 characteristics() 方法以set返回。

實(shí)現(xiàn) toList() 和 toSet() collector

使用這些元素,您現(xiàn)在可以重新創(chuàng)建類似于 toList() collector的實(shí)現(xiàn)。

class ToList<T> implements Collector<T, List<T>, List<T>> {


    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return Collection::add;
    }

    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {list1.addAll(list2); return list1; };
    }

    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.IDENTITY_FINISH);//不調(diào)用finisher
    }
}

可以使用以下模式使用此collector。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five") ;

List<String> result = strings.stream().collect(new ToList<>());
System.out.println("result = " + result);

此代碼打印出以下結(jié)果。

result = [one, two, three, four, five]

實(shí)現(xiàn)一個(gè)類似于 toSet() 的collector只需要兩處修改。

  • supplier() 方法將返回 HashSet::new
  • characteristics() 方法會(huì)將 Features.UNORDERED 添加到返回的set中。

實(shí)現(xiàn) joining() collector

重新創(chuàng)建此collector的實(shí)現(xiàn)很有趣,因?yàn)樗粚?duì)字符串進(jìn)行操作,并且它的finisher不是恒等函數(shù)。

此collector在 StringBuffer 實(shí)例中累積它處理的字符串,然后調(diào)用 toString() 方法以生成final結(jié)果。

此collector的特征集為空。它確實(shí)保留了處理元素的順序(因此沒有UNORDERED特征),它的finisher不是恒等函數(shù),并且不能并發(fā)使用。

讓我們看看如何實(shí)現(xiàn)這個(gè)collector。

class Joining implements Collector<String, StringBuffer, String> {

    public Supplier<StringBuffer> supplier() {
        return StringBuffer::new;
    }

    public BiConsumer<StringBuffer, String> accumulator() {
        return StringBuffer::append;
    }

    public BinaryOperator<StringBuffer> combiner() {
        return StringBuffer::append;
    }

    public Function<StringBuffer, String> finisher() {//會(huì)調(diào)用
        return Object::toString;
    }

    public Set<Characteristics> characteristics() {
        return Set.of();
    }
}

您可以在以下示例中看到如何使用此collector。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five") ;

String result = strings.stream().collect(new Joining());
System.out.println("result = " + result);

運(yùn)行此代碼將生成以下結(jié)果。

result = onetwothreefourfive

要支持分隔符、前綴和后綴可以使用 StringJoiner 。

使用Optional

創(chuàng)建Optional對(duì)象

Optional 類是具有私有構(gòu)造函數(shù)的final類。因此,創(chuàng)建它實(shí)例的唯一方法是調(diào)用其工廠方法之一。其中有三個(gè)。

  1. 您可以通過調(diào)用 Optional.empty() 創(chuàng)建一個(gè)空的Optional。
  2. 您可以通過調(diào)用 Optional.of() 將某元素作為參數(shù)。不允許將 null傳遞給此方法。這種情況下,您將獲得一個(gè) NullPointerException。
  3. 您可以通過調(diào)用 Optional.ofNullable() 將某元素作為參數(shù)??梢詫ull傳遞給此方法。這種情況下,您將獲得一個(gè)空的Optional。

這些是創(chuàng)建此類實(shí)例的唯一方法。如您所見,不能將null直接賦給Optional對(duì)象。打開非空Optional將始終返回非null。

Optional<T> 有三個(gè)等效的類,用于專用數(shù)字流:OptionalInt、OptionalLongOptionalDouble。這些類是原始類型(即值)的包裝器。ofNullable() 方法對(duì)這些類沒有意義,因?yàn)樵贾挡荒転閚ull。

打開Optional對(duì)象

有幾種方法可以使用Optional元素并訪問它包裝的元素(如果有)。你可以直接查詢你擁有的實(shí)例,如果里面有東西,就打開它,或者你可以在上面使用類似流的方法: map(), flatMap(), filter(),甚至是 forEach() 的等價(jià)物。

打開Optional以獲取其內(nèi)容時(shí)應(yīng)謹(jǐn)慎,因?yàn)槿绻鸒ptional為空,它將引發(fā) NoSuchElementException。除非您確定Optional元素中存在元素,否則應(yīng)首先通過測(cè)試來保護(hù)此操作。

有兩種方法可供您測(cè)試Optional對(duì)象:isPresent()isEmpty(),它們?cè)?Java SE 11 中添加。

然后,要打開您的Optional,您可以使用以下方法。

  • get():此方法已被棄用,因?yàn)?is 看起來像一個(gè) getter,但如果Optional為空,它可以拋出 NoSuchElementException。
  • orElseThrow() 是自 Java SE 10 以來的首選模式。它與get()方法相同,但它的名稱毫無疑問它可以拋出NoSuchElementException。
  • orElseThrow(Supplier exceptionSupplier):與前面的方法相同。它使用您傳遞的supplier作為參數(shù)來創(chuàng)建它引發(fā)的異常。

您還可以提供一個(gè)對(duì)象,如果Optional對(duì)象為空,將返回該對(duì)象。

  • orElse(T returnObject):如果在空的Optional值上調(diào)用,則返回參數(shù)。
  • orElseGet(Supplier supplier):與前一個(gè)相同。實(shí)際上,僅在需要時(shí)調(diào)用所提供的supplier。

最后,如果此Optional為空,則可以創(chuàng)建另一個(gè)Optional。

  • or(supplier<Optional>supplier):如果它不為空,則返回此未修改的Optional,如果空,則調(diào)用提供的supplier。此supplier創(chuàng)建另一個(gè)Optional供方法返回。

處理Optional對(duì)象

Optional 類還提供模式,以便您可以將Optional對(duì)象與流處理集成。它具有直接對(duì)應(yīng)Stream API 的方法,您可以使用這些方法以相同的方式處理數(shù)據(jù),并且將與流無縫集成。這些方法是 map(), filter(),和flatMap(),前兩個(gè)接收的參數(shù)與Stream API中的方法相同,后者的函數(shù)參數(shù)需要返回Optional<T>而不是Stream。

這些方法按以下規(guī)則返回Optional對(duì)象。

  1. 如果調(diào)用的對(duì)象為空,則返回Optional。
  2. 如果不為空,則它們的參數(shù)、函數(shù)或Predicate將應(yīng)用于此Optional的內(nèi)容。將結(jié)果包裝在另一個(gè)Optional中返回。

使用這些方法可以在某些流模式中生成更具可讀性的代碼。

假設(shè)您有一個(gè)具有id屬性的Customer實(shí)例列表。您需要查找具有給定 ID 的客戶的名稱。

您可以使用以下模式執(zhí)行此操作。

String findCustomerNameById(int id){
    List<Customer> customers = ...;

    return customers.stream()
                    .filter(customer->customer.getId() == id);
                    .findFirst()//返回Optional
                    .map(Customer::getName)
                    .orElse("UNKNOWN");
}

您可以看到 map() 方法來自 Optional 類,它與流處理很好地集成在一起。你不需要檢查 findFirst() 方法返回的Optional對(duì)象是否為空;調(diào)用map()實(shí)際上可以為您執(zhí)行此操作。

找出發(fā)表文章最多的兩位聯(lián)合作者

讓我們看另一個(gè)更復(fù)雜的示例。通過此示例,向您展示Stream API、Collector API 和Optional對(duì)象的幾種主要模式。

假設(shè)您有一組需要處理的文章。一篇文章有標(biāo)題、發(fā)表年份和作者列表。作者有一個(gè)名字。

您的列表中有很多文章,您需要知道哪些作者一起聯(lián)合發(fā)表了最多的文章。

您的第一個(gè)想法可能是為文章構(gòu)建一對(duì)作者的流。這實(shí)際上是文章和作者集的笛卡爾乘積。您并不需要此流中的所有對(duì)。您對(duì)兩位作者實(shí)際上是同一對(duì)的情況不感興趣;一對(duì)作者(A1,A2)與(A2,A1)實(shí)際相同。若要實(shí)現(xiàn)此約束,可以添加約束條件,聲明一對(duì)作者時(shí),聲明作者按字母順序排序。

讓我們?yōu)檫@個(gè)模型寫兩條record。

record Article (String title, int inceptionYear, List<Author> authors) {}

record Author(String name) implements Comparable<Author> {

    public int compareTo(Author other) {
        return this.name.compareTo(other.name);
    }
}

record PairOfAuthors(Author first, Author second) {
    
    public static Optional<PairOfAuthors> of(Author first, Author second) {//用Optional實(shí)現(xiàn)了排序后的創(chuàng)建
        if (first.compareTo(second) > 0) {
            return Optional.of(new PairOfAuthors(first, second));
        } else {
            return Optional.empty();
        }
    }
}

PairOfAuthorsrecord中的創(chuàng)建工廠方法,可以控制哪些實(shí)例是允許的,并防止不需要的創(chuàng)建。若要表明此工廠方法可能無法生成結(jié)果,可以將其包裝在Optional方法中。這完全尊重了以下原則:如果無法生成結(jié)果,則返回一個(gè)空的 optional。

讓我們編寫一個(gè)函數(shù),為給定的文章創(chuàng)建一個(gè) Stream<PairOfAuthors> 。您可以用兩個(gè)嵌套流生成笛卡爾乘積。

作為第一步,您可以編寫一個(gè)bifunction,從文章和作者創(chuàng)建此流。

BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors =
    (article, firstAuthor) ->
        article.authors().stream().flatMap(//對(duì)每個(gè)author都遍歷authors創(chuàng)建作者對(duì),生成Stream<PairOfAuthors>
            secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());//Optional的Stream

此bifunction從 firstAuthorsecondAuthor 創(chuàng)建一個(gè)Optional對(duì)象,取自基于文章作者構(gòu)建的流。您可以看到 stream() 方法是在 of() 方法返回的Optional對(duì)象上調(diào)用的。如果Optional流為空,則返回的流為空,否則僅包含一對(duì)作者。此流由 flatMap() 方法處理。此方法打開流,空的流將消失,并且只有有效的對(duì)將出現(xiàn)在生成的流中。

您現(xiàn)在可以構(gòu)建一個(gè)函數(shù),該函數(shù)使用此bifunction從文章中創(chuàng)建作者對(duì)流。

Function<Article, Stream<PairOfAuthors>> toPairOfAuthors =
    article ->
    article.authors().stream()
                     .flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));

找到聯(lián)合發(fā)表最多的兩位作者可以通過柱狀圖來完成,柱狀圖中的鍵是作者對(duì),值是他們一起寫的文章數(shù)。

您可以使用 groupingBy() 構(gòu)建柱狀圖。讓我們首先創(chuàng)建一對(duì)作者的流。

Stream<PairOfAuthors> pairsOfAuthors =
    articles.stream()
            .flatMap(toPairOfAuthors);

此流的構(gòu)建方式是,如果一對(duì)作者一起寫了兩篇文章,則這對(duì)作者在流中出現(xiàn)兩次。因此,您需要做的是計(jì)算每個(gè)對(duì)在此流中出現(xiàn)的次數(shù)。這可以通過 groupingBy() 來完成,其中分類器是恒等函數(shù):對(duì)本身。此時(shí),這些值是您需要計(jì)數(shù)的對(duì)列表。所以下游collector只是 counting() collector。

Map<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)//所有文章的Stream<PairOfAuthors>
            .collect(Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()//<PairOfAuthors, Long>
            ));

找到一起發(fā)表文章最多的作者包括提取此map的最大值。您可以為此處理創(chuàng)建以下函數(shù)。

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor =
    map -> map.entrySet().stream()
                         .max(Map.Entry.comparingByValue())
                         .orElseThrow();

此函數(shù)在 Stream.max() 方法返回的Optional對(duì)象上調(diào)用 orElseThrow() 方法。

這個(gè)Optional對(duì)象可以為空嗎?要使其為空,map本身必須為空,這意味著原始流中沒有成對(duì)的作者。只要您至少有一篇文章至少有兩位作者,那么這個(gè)Optional就不為空。

找出每年發(fā)表文章最多的兩位聯(lián)合作者

讓我們更進(jìn)一步,想知道您是否可以根據(jù)年份進(jìn)行相同的處理。事實(shí)上,如果能使用單個(gè)collector實(shí)現(xiàn),接下來就可以將其作為下游collector傳遞給 groupingBy(Article::inceptionYear) 。

對(duì)map后續(xù)提取最大值可以使用collectingAndThen()。此模式已在上一節(jié)“使用finisher對(duì)collector進(jìn)行后續(xù)處理”中介紹過。此collector如下。

讓我們提取 groupingBy() collector和finisher。如果使用 IDE 鍵入此代碼,可以獲取collector的正確類型。

Collector<PairOfAuthors, ?, Map<PairOfAuthors, Long>> groupingBy =
        Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
        );

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> finisher =
        map -> map.entrySet().stream()
                  .max(Map.Entry.comparingByValue())
                  .orElseThrow();

現(xiàn)在,您可以將它們合并到單個(gè) collectingAndThen() 中。將 groupingBy() 作為為第一個(gè)參數(shù),將finisher作為第二個(gè)。

Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector =
    Collectors.collectingAndThen(
            Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
            ),
            map -> map.entrySet().stream()
                      .max(Map.Entry.comparingByValue())
                      .orElseThrow()
    );

現(xiàn)在,您可以使用初始flatmap操作和此collector編寫完整模式。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)
            .collect(pairOfAuthorsEntryCollector);

多虧了 flatMapping(),您可以通過合并中繼 flatMap() 和末端collector來使用單個(gè)collector編寫此代碼。以下代碼等效于上一個(gè)代碼。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .collect(
                Collectors.flatMapping(
                    toPairOfAuthors,
                    pairOfAuthorsEntryCollector));

找到每年發(fā)表最多的兩位聯(lián)合作者,只需將這個(gè) flatMapping() 作為下游collector傳遞給正確的 groupingBy() 即可。

Collector<Article, ?, Map.Entry<PairOfAuthors, Long>> flatMapping = 
    Collectors.flatMapping(
            toPairOfAuthors,
            pairOfAuthorsEntryCollector));

Map<Integer, Map.Entry<PairOfAuthors, Long>> result =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                    Article::inceptionYear,
                    flatMapping
                )
            );

你可能還記得,在這個(gè)flatMapping()的深處,有一個(gè)對(duì)Optional.orElseThrow()的調(diào)用。在這個(gè)的模式中,很容易檢查此調(diào)用是否會(huì)失敗,因?yàn)榇藭r(shí)有一個(gè)空的Optional很容易猜到。

現(xiàn)在我們已將此collector用作下游collector,情況就不同了。你怎么能確定,每年至少有一篇文章由至少兩位作者撰寫?保護(hù)此代碼免受任何 NoSuchElementException 的影響會(huì)更安全。

避免打開Optional

在第一個(gè)上下文中可以接受的模式現(xiàn)在更加危險(xiǎn)。處理它包括首先不要調(diào)用orElseThrow()。

這種情況下,collector將變?yōu)橐韵马?xiàng)。它不是創(chuàng)建一對(duì)作者和一長(zhǎng)串?dāng)?shù)字的鍵值對(duì),而是將結(jié)果包裝在一個(gè)Optional對(duì)象中。

Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>> 
        pairOfAuthorsEntryCollector =
            Collectors.collectingAndThen(
                Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()
                ),
                map -> map.entrySet().stream()
                          .max(Map.Entry.comparingByValue())
            );

請(qǐng)注意,orElseThrow() 不再被調(diào)用,從而導(dǎo)致collector的簽名中有一個(gè)Optional。

這個(gè)Optional也出現(xiàn)在 flatMapping() collector的簽名中。

Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =
        Collectors.flatMapping(
                toPairOfAuthors,
                pairOfAuthorsEntryCollector
        );

使用此collector會(huì)創(chuàng)建一個(gè)類型為 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>> 類型的map,我們不需要這種類型:擁有一個(gè)值為Optional的map是無用的,而且可能很昂貴。這是一種反模式。不幸的是,在計(jì)算此最大值之前,您無法猜測(cè)此Optional是否為空。

構(gòu)建此中繼map后,您需要?jiǎng)h除空的Optional來構(gòu)建表示所需柱狀圖的map。我們將使用與之前相同的技術(shù):在flatMap() 中調(diào)用Optional的stream())方法,以便 flatMap() 操作靜默刪除空的Optional。

模式如下。

Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                        Article::inceptionYear,
                        flatMapping
                )
            )  // Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>
            .entrySet().stream()
            .flatMap(
                entry -> entry.getValue()//如果Optional為空,會(huì)成為空流,從而安全跳過
                              .map(value -> Map.entry(entry.getKey(), value))
                              .stream())
            .collect(Collectors.toMap(
                    Map.Entry::getKey, Map.Entry::getValue
            )); // Map<Integer, Map.Entry<PairOfAuthors, Long>>

請(qǐng)注意此模式中的flatmap函數(shù)。它接受一個(gè)entry作參數(shù) ,類型為 Optional<Map.Entry<PairOfAuthors, Long>> ,并在此Optional上調(diào)用 map()。

如果Optional為空,則此調(diào)用返回空的Optional。然后忽略map函數(shù)。接下來調(diào)用 stream() 返回一個(gè)空流,該流將從主流中刪除,因?yàn)槲覀兲幱?flatMap() 調(diào)用中。

如果Optional中有一個(gè)值,則使用此值調(diào)用map函數(shù)。此map函數(shù)創(chuàng)建一個(gè)具有相同鍵和此現(xiàn)有值的新鍵值對(duì)。此鍵值對(duì)的類型為 Map.Entry,并且通過此 map() 方法將其包裝在Optional對(duì)象中。對(duì) stream() 的調(diào)用會(huì)創(chuàng)建一個(gè)包含此Optional內(nèi)容的流,然后由 flatMap() 調(diào)用打開該流。

此模式用空的Optional將 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>>mapping為 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>,刪除所有具有空Optional的鍵/值對(duì)。

使用 toMap() collector可以安全地重新創(chuàng)建map,因?yàn)槟涝诖肆髦胁荒苁褂脙纱蜗嗤逆I。

此模式使用了Optional和Stream API 的三個(gè)要點(diǎn)。

  1. Optional.map() 方法,如果在空Optional上調(diào)用,則返回空的Optional。
  2. Optional.stream() 方法,該方法在 Optional的內(nèi)容上打開流。如果Optional為空,則返回的流也為空。它允許您從Optional空間無縫的移動(dòng)到流空間。
  3. Stream.flatMap() 方法,用于打開從Optional構(gòu)建的流,以靜默方式刪除空流。

消費(fèi)Optional的內(nèi)容

Optional 類還具有兩個(gè)將Consumer作為參數(shù)的方法。

  • ifPresent(Consumer consumer):此方法使用此Optional的內(nèi)容(如果有)調(diào)用提供的Consumer。它實(shí)際上等同于 Stream.forEach(Consumer) 方法。

  • ifPresentOrElse(Consumer consumer, Runnable runnable):如果 Optional非空,此方法與前一個(gè)方法相同。如果空,則調(diào)用提供的 Runnable 實(shí)例。

燒哥總結(jié)

(驗(yàn)證中,代碼庫(kù)持續(xù)更新)

lambda將匿名類換成了匿名方法,能代表某個(gè)操作,讓代碼更直觀(語法糖),但良好的命名很重要。

改寫為lambda首先得是函數(shù)接口,Operator是Function的簡(jiǎn)化版。

可以序列化,從而可以作為字段、方法參數(shù)和返回類型,實(shí)現(xiàn)了方法引用、鏈?zhǔn)秸{(diào)用、函數(shù)式編程。

lambda已經(jīng)深入JDK內(nèi)部,所以性能方面很關(guān)注,為避免裝箱拆箱,提供了很多原生類型專用版,但有時(shí)候要手動(dòng)裝箱。

為了性能,避免在內(nèi)存中處理大量數(shù)據(jù),同時(shí)也提高可讀性,出現(xiàn)了Stream API。

流處理的整個(gè)過程最好都是流,所以有flatmap、mapMulti各種中繼操作,

甚至末端collector也可以有下游collector,甚至collector可以串聯(lián)、三通,比如神奇的Collectors.flatMapping()

流不應(yīng)該作為變量或參數(shù)。

流中不應(yīng)該改變外圍變量,會(huì)捕獲外界變量,降低處理性能,也會(huì)把并行流變成多線程并發(fā)。

每次中繼操作都產(chǎn)生一個(gè)新流。

同樣為了性能,reduce可以并行,但要具有可結(jié)合性、要有幺元。如果幺元未知,會(huì)返回Optional。

三參數(shù)的reduce組合了mapping過程。

專用數(shù)字流的sum、min、max、count、average、summaryStatistics為末端操作。

轉(zhuǎn)換為流的源如果用Set,會(huì)是亂序的。

map、flatmap會(huì)刪除SORTED、DISTINCTED、NONNULL。

本教程未詳細(xì)說明的:spliterator、不可變流、并發(fā)流。

Stream.collect(Collectors.toList()) 只能用于對(duì)象流,數(shù)字流要么裝箱,要么用三參數(shù)那個(gè),或者自定義collector,五個(gè)參數(shù)。

flatmap會(huì)跳過空流,包括Optional.stream()產(chǎn)生的流,所以看到Optional,不要orElseThrow(),可以用flatmap取出。

API是看起來越來越復(fù)雜,Collectors.mapping()

public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper,
 Collector<? super U,A,R> downstream)

方法名前面四個(gè),返回類型里三個(gè),還有問號(hào),參數(shù)里super了三個(gè)。

Map<City, Set<String>> lastNamesByCity
   = people.stream().collect(
     groupingBy(Person::getCity,
                mapping(Person::getLastName,
                        toSet())));

雖然很好用。文章來源地址http://www.zghlxwxcb.cn/news/detail-522858.html

到了這里,關(guān)于從頭學(xué)Java17-Stream API(二)結(jié)合Record、Optional的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • JDK8 新特性 Stream API 進(jìn)階 (結(jié)合案例詳解--通透--講清)

    JDK8 新特性 Stream API 進(jìn)階 (結(jié)合案例詳解--通透--講清)

    ??我親愛的各位大佬們好?????? ?? 本篇文章記錄的為 JDK8 新特性 Stream API 進(jìn)階 相關(guān)內(nèi)容,適合在學(xué)Java的小白,幫助新手快速上手,也適合復(fù)習(xí)中,面試中的大佬??????。 ?? 如果文章有什么需要改進(jìn)的地方還請(qǐng)大佬不吝賜教?????? ????? 個(gè)人主頁 : 阿千弟 ??

    2024年02月06日
    瀏覽(19)
  • 從頭開始用JAVA創(chuàng)建一個(gè)自己的簡(jiǎn)單API并實(shí)現(xiàn)第三方調(diào)用

    從頭開始用JAVA創(chuàng)建一個(gè)自己的簡(jiǎn)單API并實(shí)現(xiàn)第三方調(diào)用

    ????????相信大家對(duì)這個(gè)詞匯并不陌生,通俗來說API就是程序之間的接口,在學(xué)習(xí)和工作中經(jīng)常會(huì)調(diào)用別人的API,那么如果我們要做一個(gè)自己的API,要如何下手呢。本文將用Spring+JAVA編寫一個(gè)簡(jiǎn)單的API,過程可供初學(xué)者參考。 ? ? ? ? 為了顧及完全沒有經(jīng)驗(yàn)的小白(比如我

    2024年02月10日
    瀏覽(20)
  • Stream——集合數(shù)據(jù)按照某一字段排序

    Stream——集合數(shù)據(jù)按照某一字段排序

    之前,針對(duì) Stream 鏈?zhǔn)骄幊讨械膸讉€(gè)方法做了大致的說明。詳情可以參考: JDK 1.8 新特性之Stream 詳解個(gè)人筆記 但實(shí)際業(yè)務(wù)中,總會(huì)存在很多復(fù)雜的思維,需要使用到 Stream ,此時(shí)玩的不熟練總感覺無從下手。 今后開始寫幾種常用的邏輯方式。 在數(shù)據(jù)查詢出來后,需要針對(duì)集

    2024年02月04日
    瀏覽(30)
  • 【Java系列】深入解析Stream API

    【Java系列】深入解析Stream API

    你只管努力,其他交給時(shí)間,時(shí)間會(huì)證明一切。 文章標(biāo)記顏色說明: 黃色 :重要標(biāo)題 紅色 :用來標(biāo)記結(jié)論 綠色 :用來標(biāo)記論點(diǎn) 藍(lán)色 :用來標(biāo)記論點(diǎn) 希望這篇文章能讓你不僅有一定的收獲,而且可以愉快的學(xué)習(xí),如果有什么建議,都可以留言和我交流 Stream API是Java 8中最

    2024年02月10日
    瀏覽(24)
  • Java 8:Stream API 流式操作

    Java 8:Stream API 流式操作

    ??wei_shuo的個(gè)人主頁 ??wei_shuo的學(xué)習(xí)社區(qū) ??Hello World ! Java 8 中的 Stream API 是一組用于對(duì)集合數(shù)據(jù)進(jìn)行處理的新特性;提供一種以聲明式風(fēng)格對(duì)集合進(jìn)行操作的方式,簡(jiǎn)化集合的處理,使得代碼更加簡(jiǎn)潔、優(yōu)雅,并且能夠更高效地處理數(shù)據(jù); 這種風(fēng)格將要處理的元素集合看

    2024年02月13日
    瀏覽(13)
  • Java 8:Stream API 流式操作(學(xué)習(xí))

    Java 8 中的 Stream API 是一組用于對(duì)集合數(shù)據(jù)進(jìn)行處理的新特性;提供一種以聲明式風(fēng)格對(duì)集合進(jìn)行操作的方式,簡(jiǎn)化集合的處理,使得代碼更加簡(jiǎn)潔、優(yōu)雅,并且能夠更高效地處理數(shù)據(jù); 這種風(fēng)格將要處理的元素集合看作一種流, 流在管道中傳輸, 并且可以在管道的節(jié)點(diǎn)上進(jìn)

    2024年02月10日
    瀏覽(45)
  • Java Stream API的基本使用方法

    Java Stream API的基本使用方法

    Java各個(gè)版本所更新的主要內(nèi)容: 1.Java SE 8:引入了一些新特性,如lambda表達(dá)式、Stream API、格式化日期、國(guó)際化等。此外,還對(duì)并發(fā)編程進(jìn)行了改進(jìn),引入了線程安全的 Stream API 。 2.Java SE 9:新增了分布式架構(gòu)的支持,引入了CompletableFuture、ZK等新特性。此外,還對(duì)Jit編譯器進(jìn)

    2024年02月03日
    瀏覽(31)
  • Optional常用方法實(shí)例, 源碼挺簡(jiǎn)單的,重要的是option 與 stream 以及l(fā)ambda 的連貫使用

    ?empty(): ?of() // 非null 就繼續(xù)執(zhí)行 否則拋異常 ? ofNullable () ?get() //非null就get 否則異常 ?//lambada // 根據(jù)是否為NULL進(jìn)行操作 ?// filter進(jìn)行以上對(duì)下進(jìn)行輸入 如果opt是一集合的話 , 無法進(jìn)行篩選 , 只能使用集合本身進(jìn)行filter 取到map orElseThrow() //是空就拋出自定義異常

    2024年02月05日
    瀏覽(18)
  • Java 8 中的 Stream API - map() 方法詳解

    摘要: Java 8 中的 Stream API 提供了一種新的處理集合和數(shù)組的方式,可以使代碼更加簡(jiǎn)潔、易讀,同時(shí)還可以提高性能。其中 map() 方法是比較常用的方法之一,它可以將 Stream 對(duì)象中的每個(gè)元素映射為另一個(gè)元素。本文將對(duì) Java 8 中的 Stream API 和 map() 方法進(jìn)行詳細(xì)介紹,并通

    2024年04月09日
    瀏覽(24)
  • Java Stream中的API你都用過了嗎?

    公眾號(hào)「架構(gòu)成長(zhǎng)指南」,專注于生產(chǎn)實(shí)踐、云原生、分布式系統(tǒng)、大數(shù)據(jù)技術(shù)分享。 在本教程中,您將通過大量示例來學(xué)習(xí) Java 8 Stream API。 Java 在 Java 8 中提供了一個(gè)新的附加包,稱為 java.util.stream 。該包由類、接口和枚舉組成,允許對(duì)元素進(jìn)行函數(shù)式操作。 您可以通過在

    2024年02月05日
    瀏覽(23)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包