簡體   English   中英

如何使用 Java 8 可選,如果三個都存在則執行一個動作?

[英]How to use Java 8 Optionals, performing an action if all three are present?

我有一些(簡化的)代碼使用 Java 可選:

Optional<User> maybeTarget = userRepository.findById(id1);
Optional<String> maybeSourceName = userRepository.findById(id2).map(User::getName);
Optional<String> maybeEventName = eventRepository.findById(id3).map(Event::getName);

maybeTarget.ifPresent(target -> {
    maybeSourceName.ifPresent(sourceName -> {
        maybeEventName.ifPresent(eventName -> {
            sendInvite(target.getEmail(), String.format("Hi %s, $s has invited you to $s", target.getName(), sourceName, meetingName));
        }
    }
}

不用說,這看起來和感覺都很糟糕。 但是我想不出另一種方法來以更少嵌套和更易讀的方式來做到這一點。 我考慮過流式傳輸 3 個 Optional,但放棄了做.filter(Optional::isPresent)然后.map(Optional::get)感覺更糟的想法。

那么是否有更好、更“Java 8”或“可選識字”的方式來處理這種情況(本質上是多個 Optional 都需要計算最終操作)?

我認為流式傳輸三個Optional是一種矯枉過正,為什么不簡單

if (maybeTarget.isPresent() && maybeSourceName.isPresent() && maybeEventName.isPresent()) {
  ...
}

在我看來,與使用流 API 相比,這更清楚地說明了條件邏輯。

使用輔助函數,事情至少變得不那么嵌套了:

@FunctionalInterface
interface TriConsumer<T, U, S> {
    void accept(T t, U u, S s);
}

public static <T, U, S> void allOf(Optional<T> o1, Optional<U> o2, Optional<S> o3,
       TriConsumer<T, U, S> consumer) {
    o1.ifPresent(t -> o2.ifPresent(u -> o3.ifPresent(s -> consumer.accept(t, u, s))));
}

allOf(maybeTarget, maybeSourceName, maybeEventName,
    (target, sourceName, eventName) -> {
        /// ...
});

明顯的缺點是您需要為每個不同數量的Optional單獨重載一個輔助函數

由於執行原始代碼是為了它的副作用(發送電子郵件),而不是提取或生成值,因此嵌套的ifPresent調用似乎是合適的。 原始代碼看起來並不太糟糕,實際上它似乎比提出的一些答案要好得多。 然而,語句 lambdas 和Optional類型的局部變量似乎確實增加了相當多的混亂。

首先,我將通過將原始代碼包裝在一個方法中、為參數提供好聽的名稱並組成一些類型名稱來自由地修改原始代碼。 我不知道實際的代碼是否是這樣的,但這對任何人來說都不應該感到驚訝。

// original version, slightly modified
void inviteById(UserId targetId, UserId sourceId, EventId eventId) {
    Optional<User> maybeTarget = userRepository.findById(targetId);
    Optional<String> maybeSourceName = userRepository.findById(sourceId).map(User::getName);
    Optional<String> maybeEventName = eventRepository.findById(eventId).map(Event::getName);

    maybeTarget.ifPresent(target -> {
        maybeSourceName.ifPresent(sourceName -> {
            maybeEventName.ifPresent(eventName -> {
                sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
                                                  target.getName(), sourceName, eventName));
            });
        });
    });
}

我嘗試了不同的重構,我發現將內部語句 lambda 提取到它自己的方法中對我來說最有意義。 給定源用戶和目標用戶以及一個事件——沒有可選的東西——它會發送關於它的郵件。 這是在處理完所有可選內容后需要執行的計算。 我還將數據提取(電子郵件、姓名)移到此處,而不是將其與外層中的 Optional 處理混合在一起。 同樣,這對我來說很有意義:將關於event 的郵件從發送到目標

void setupInvite(User target, User source, Event event) {
    sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
               target.getName(), source.getName(), event.getName()));
}

現在,讓我們處理可選的東西。 正如我上面所說, ifPresent是這里的方法,因為我們想要做一些有副作用的事情。 它還提供了一種從 Optional 中“提取”值並將其綁定到名稱的方法,但僅限於 lambda 表達式的上下文中。 由於我們要為三個不同的 Optional 執行此操作,因此需要嵌套。 嵌套允許內部 lambda 捕獲來自外部 lambda 的名稱。 這讓我們可以將名稱綁定到從 Optionals 中提取的值——但前提是它們存在。 這不能用線性鏈來完成,因為需要像元組這樣的中間數據結構來構建部分結果。

最后,在最里面的 lambda 中,我們調用上面定義的輔助方法。

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    userRepository.findById(targetId).ifPresent(
        target -> userRepository.findById(sourceID).ifPresent(
            source -> eventRepository.findById(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}

請注意,我已經內聯了 Optionals 而不是將它們保存在局部變量中。 這更好地揭示了嵌套結構。 如果其中一個查找沒有找到任何東西,它還提供了操作的“短路”,因為ifPresent只是對空的 Optional 不做任何事情。

不過,我的眼睛仍然有點濃。 我認為原因是這段代碼仍然依賴於一些進行查找的外部存儲庫。 將其與 Optional 處理混合在一起有點不舒服。 一種可能性是將查找提取到它們自己的方法findUserfindEvent 這些很明顯,所以我不會寫出來。 但如果這樣做了,結果將是:

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    findUser(targetId).ifPresent(
        target -> findUser(sourceID).ifPresent(
            source -> findEvent(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}

從根本上說,這與原始代碼沒有什么不同。 這是主觀的,但我想我更喜歡這個而不是原始代碼。 它具有相同的、相當簡單的結構,盡管是嵌套的,而不是典型的 Optional 處理線性鏈。 不同的是,查找是在 Optional 處理中有條件地完成的,而不是預先完成,存儲在局部變量中,然后只對 Optional 值進行有條件的提取。 此外,我已將數據操作(提取電子郵件和姓名、發送消息)分離到一個單獨的方法中。 這避免了將數據操作與 Optional 處理混合在一起,我認為如果我們處理多個 Optional 實例,這往往會使事情變得混亂。

這樣的事情怎么樣

 if(Stream.of(maybeTarget, maybeSourceName,  
                        maybeEventName).allMatch(Optional::isPresent))
  {
   sendinvite(....)// do get on all optionals.
  }

話雖如此。 如果您在數據庫中查找的邏輯只是發送郵件,那么如果maybeTarget.ifPresent()為false,那么獲取其他兩個值就沒有意義了,不是嗎?。 恐怕,這種邏輯只能通過傳統的 if else 語句來實現。

我認為你應該考慮采取另一種方法。

我首先不會在開始時向數據庫發出三個調用。 相反,我會發出第一個查詢,並且僅當結果存在時,我才會發出第二個查詢。 然后我會對第三個查詢應用相同的基本原理,最后,如果最后一個結果也存在,我會發送邀請。 當前兩個結果中的任何一個不存在時,這將避免對數據庫的不必要調用。

為了使代碼更具可讀性、可測試性和可維護性,我還將每個 DB 調用提取到其自己的私有方法,並將它們與Optional.ifPresent鏈接起來:

public void sendInvite(Long targetId, Long sourceId, Long meetingId) {
    userRepository.findById(targetId)
        .ifPresent(target -> sendInvite(target, sourceId, meetingId));
}

private void sendInvite(User target, Long sourceId, Long meetingId) {
    userRepository.findById(sourceId)
        .map(User::getName)
        .ifPresent(sourceName -> sendInvite(target, sourceName, meetingId));
}

private void sendInvite(User target, String sourceName, Long meetingId) {
    eventRepository.findById(meetingId)
        .map(Event::getName)
        .ifPresent(meetingName -> sendInvite(target, sourceName, meetingName));
}

private void sendInvite(User target, String sourceName, String meetingName) {
    String contents = String.format(
        "Hi %s, $s has invited you to $s", 
        target.getName(), 
        sourceName, 
        meetingName);
    sendInvite(target.getEmail(), contents);
}

第一種方法並不完美(它不支持懶惰 - 無論如何都會觸發所有 3 個數據庫調用):

Optional<User> target = userRepository.findById(id1);
Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);

if (Stream.of(target, sourceName, eventName).anyMatch(obj -> !obj.isPresent())) {
    return;
}
sendInvite(target.get(), sourceName.get(), eventName.get());

下面的例子有點冗長,但它支持懶惰和可讀性:

private void sendIfValid() {
    Optional<User> target = userRepository.findById(id1);
    if (!target.isPresent()) {
        return;
    }
    Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
    if (!sourceName.isPresent()) {
        return;
    }
    Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);
    if (!eventName.isPresent()) {
        return;
    }
    sendInvite(target.get(), sourceName.get(), eventName.get());
}

private void sendInvite(User target, String sourceName, String eventName) {
    // ...
}

如果您想堅持使用Optional並且不承諾立即使用該值,則可以使用以下方法。 它使用了來自 Apache Commons 的Triple<L, M, R>

/**
 * Returns an optional contained a triple if all arguments are present,
 * otherwise an absent optional
 */
public static <L, M, R> Optional<Triple<L, M, R>> product(Optional<L> left,
        Optional<M> middle, Optional<R> right) {
    return left.flatMap(l -> middle.flatMap(m -> right.map(r -> Triple.of(l, m, r))));
}

// Used as
product(maybeTarget, maybeSourceName, maybeEventName).ifPresent(this::sendInvite);

人們可以為兩個或多個Optional設想一種類似的方法,盡管不幸的是 java 還沒有通用的元組類型(還)。

好吧,我采用了與Federico相同的方法,只在需要時調用數據庫,它也很冗長,但很懶惰 我也簡化了一點。 考慮到您有以下 3 種方法:

public static Optional<String> firstCall() {
    System.out.println("first call");
    return Optional.of("first");
}

public static Optional<String> secondCall() {
    System.out.println("second call");
    return Optional.empty();
}

public static Optional<String> thirdCall() {
    System.out.println("third call");
    return Optional.empty();
}

我是這樣實現的:

firstCall()
       .flatMap(x -> secondCall().map(y -> Stream.of(x, y))
              .flatMap(z -> thirdCall().map(n -> Stream.concat(z, Stream.of(n)))))
       .ifPresent(st -> System.out.println(st.collect(Collectors.joining("|"))));

您可以創建一個基礎設施來處理可變數量的輸入。 盡管如此,為了這是一個好的設計,你的輸入不應該是Optional<?> 但是Supplier<Optional<?>>以便您可以在嘗試確定是否全部存在時縮短Optionals的不必要評估。

因此,最好在Optional周圍創建一個實用程序包裝器,它使用單例模式提供對評估值的透明訪問,如下所示:

    class OptionalSupplier {

        private final Supplier<Optional<?>> optionalSupplier;
        private Optional<?> evaluatedOptional = null;

        public OptionalSupplier(Supplier<Optional<?>> supplier) {
            this.optionalSupplier = supplier;
        }

        public Optional<?> getEvaluatedOptional() {
            if (evaluatedOptional == null)
                evaluatedOptional = optionalSupplier.get();

            return evaluatedOptional;
        }
    }

然后,您可以創建另一個類來處理這些包裝器的List ,並提供一個編程 API 來執行一個Function ,該Function將實際選項的評估值作為參數,進一步隱藏用戶參與該過程。 您可以重載該方法以執行具有相同參數的Consumer 這樣的類看起來像這樣:

    class OptionalSemaphores {

        private List<OptionalSupplier> optionalSuppliers;
        private List<Object> results = null;
        private boolean allPresent;

        public OptionalSemaphores(Supplier<Optional<?>>... suppliers) {
            optionalSuppliers = Stream.of(suppliers)
                    .map(OptionalSupplier::new)
                    .collect(Collectors.toList());

            allPresent = optionalSuppliers.stream()
                    .map(OptionalSupplier::getEvaluatedOptional)
                    .allMatch(Optional::isPresent);

            if (allPresent)
                results = optionalSuppliers.stream()
                        .map(OptionalSupplier::getEvaluatedOptional)
                        .map(Optional::get)
                        .collect(Collectors.toList());
        }

        public boolean isAllPresent() {
            return allPresent;
        }

        public <T> T execute(Function<List<Object>, T> function, T defaultValue) {
            return (allPresent) ? function.apply(results) : defaultValue;
        }

        public void execute(Consumer<List<Object>> function) {
            if (allPresent)
                function.accept(results);
        }
    }

最后,所有你剩下要做的就是創建(這個類的對象OptionalSemaphores使用) Supplier的第Optional S( Supplier<Optional<?>> )和調用任何一個重載的execute方法來運行(如果所有Optional s為存在) 帶有一個List其中包含來自Optional的相應評估值。 以下是一個完整的工作演示:

public class OptionalsTester {

    public static void main(String[] args) {
        Supplier<Optional<?>> s1 = () -> Optional.of("Hello");
        Supplier<Optional<?>> s2 = () -> Optional.of(1L);
        Supplier<Optional<?>> s3 = () -> Optional.of(55.87);
        Supplier<Optional<?>> s4 = () -> Optional.of(true);
        Supplier<Optional<?>> s5 = () -> Optional.of("World");
        Supplier<Optional<?>> failure = () -> Optional.ofNullable(null);
        Supplier<Optional<?>> s7 = () -> Optional.of(55);

        System.out.print("\nFAILING SEMAPHORES: ");
        new OptionalSemaphores(s1, s2, s3, s4, s5, failure, s7).execute(System.out::println);

        System.out.print("\nSUCCESSFUL SEMAPHORES: ");
        new OptionalSemaphores(s1, s2, s3, s4, s5, s7).execute(System.out::println);
    }

    static class OptionalSemaphores {

        private List<OptionalSupplier> optionalSuppliers;
        private List<Object> results = null;
        private boolean allPresent;

        public OptionalSemaphores(Supplier<Optional<?>>... suppliers) {
            optionalSuppliers = Stream.of(suppliers)
                    .map(OptionalSupplier::new)
                    .collect(Collectors.toList());

            allPresent = optionalSuppliers.stream()
                    .map(OptionalSupplier::getEvaluatedOptional)
                    .allMatch(Optional::isPresent);

            if (allPresent)
                results = optionalSuppliers.stream()
                        .map(OptionalSupplier::getEvaluatedOptional)
                        .map(Optional::get)
                        .collect(Collectors.toList());
        }

        public boolean isAllPresent() {
            return allPresent;
        }

        public <T> T execute(Function<List<Object>, T> function, T defaultValue) {
            return (allPresent) ? function.apply(results) : defaultValue;
        }

        public void execute(Consumer<List<Object>> function) {
            if (allPresent)
                function.accept(results);
        }
    }

    static class OptionalSupplier {

        private final Supplier<Optional<?>> optionalSupplier;
        private Optional<?> evaluatedOptional = null;

        public OptionalSupplier(Supplier<Optional<?>> supplier) {
            this.optionalSupplier = supplier;
        }

        public Optional<?> getEvaluatedOptional() {
            if (evaluatedOptional == null)
                evaluatedOptional = optionalSupplier.get();

            return evaluatedOptional;
        }
    }
}

GitHub 上的完整代碼

希望這會有所幫助。

如果將Optional視為方法返回值的標記,則代碼將變得非常簡單:

User target = userRepository.findById(id1).orElse(null);
User source = userRepository.findById(id2).orElse(null);
Event event = eventRepository.findById(id3).orElse(null);

if (target != null && source != null && event != null) {
    String message = String.format("Hi %s, %s has invited you to %s",
        target.getName(), source.getName(), event.getName());
    sendInvite(target.getEmail(), message);
}

Optional的重點不是你必須在任何地方使用它。 相反,它用作方法返回值的標記,以通知調用者檢查缺席。 在這種情況下, orElse(null)會處理這個問題,並且調用代碼完全意識到可能的空值。

return userRepository.findById(id)
                .flatMap(target -> userRepository.findById(id2)
                        .map(User::getName)
                        .flatMap(sourceName -> eventRepository.findById(id3)
                                .map(Event::getName)
                                .map(eventName-> createInvite(target, sourceName, eventName))))

首先,您還返回一個 Optional 。 最好先有一個方法來創建邀請,如果它不為空,您可以調用然后發送。

除此之外,它更容易測試。 使用 flatMap 您還可以獲得懶惰的好處,因為如果第一個結果為空,則不會評估其他任何結果。

當你想使用多個選項時,你總是應該使用 map 和 flatMap 的組合。

我也沒有使用 target.getEmail() 和 target.getName(),它們應該在 createInvite 方法中安全地提取,因為我不知道它們是否可以為空。

請記住,不應以這種方式使用異常,為了簡潔起見,您也可以考慮:

try {
  doSomething( optional1.get(), optional2.get(), optional3.get() );
} catch( NoSuchElementException e ) {
  // report, log, do nothing
}

請記住,您可以內聯定義ClassesRecords以保持 state 顯式和扁平化,而不是使用回調/閉包嵌套。 對於像這樣的小例子來說,這似乎有點矯枉過正,但當每個嵌套的“鏈”最終都在工作時,它確實很有幫助。

例如,給定您使用 lombok 的 3 個Optional

    @Value @With class Temp {User target; String source; String eventName;}

    maybeTarget
        .map(target -> new Temp(target, null, null))
        .flatMap(tmp -> maybeSourceName.map(tmp::withSource))
        .flatMap(tmp -> maybeEventName.map(tmp::withEventName))
        .ifPresent(tmp -> System.out.printf("Hi %s, %s has invited you to %s%n", tmp.target.getName(), tmp.source, tmp.eventName));

您可以使用記錄執行相同的操作,但您必須做更多的工作,因為您必須手動復制所有內容:

    record TempRecord(User target, String source, String eventName) {}

    maybeTarget
        .map(target -> new TempRecord(target, null, null))
        .flatMap(tmp -> maybeSourceName.map(source -> new TempRecord(tmp.target, source, null)))
        .flatMap(tmp -> maybeEventName.map(eventName -> new TempRecord(tmp.target, tmp.source, eventName)))
        .ifPresent(tmp -> System.out.printf("Hi %s, %s has invited you to %s%n", tmp.target.getName(), tmp.source, tmp.eventName));

我試圖保持數據不可變和功能純凈。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM