簡體   English   中英

我們什么時候應該在 Java 8 中使用供應商?

[英]When we should use Supplier in Java 8?

這段代碼有什么區別?

Supplier<LocalDate> s1 = LocalDate::now;
LocalDate s2 = LocalDate.now();

System.out.println(s1.get()); //2016-10-25
System.out.println(s2); //2016-10-25

我開始學習 Java 8 中的函數式接口,但不了解供應商的好處。 應該何時以及如何使用它們。 供應商是否提高了性能或抽象級別的好處?

感謝您的回答! 這不是重復的問題,因為我使用了搜索並沒有找到我需要的。

更新1:你是說這個?

    Supplier<Long> s1 = System::currentTimeMillis;
    Long s2 = System.currentTimeMillis();

    System.out.println(s1.get()); //1477411877817
    System.out.println(s2); //1477411877817
    try {
        Thread.sleep(3000l);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(s1.get()); //1477411880817 - different
    System.out.println(s2); //1477411877817

我將經歷一個場景,我們應該使用Supplier<LocalDate>而不是LocalDate

直接調用LocalDate.now()等靜態方法的代碼很難進行單元測試。 考慮一個場景,我們要對計算一個人的年齡的getAge()方法進行單元測試:

class Person {
    final String name;
    private final LocalDate dateOfBirth;

    Person(String name, LocalDate dateOfBirth) {
        this.name = name;
        this.dateOfBirth = dateOfBirth;
    }

    long getAge() {
        return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now());
    }
}

這在生產中運行良好。 但是單元測試要么必須將系統的日期設置為已知值,要么每年更新以期望返回的年齡增加 1,這兩種解決方案都非常棒。

更好的解決方案是讓單元測試注入已知日期,同時仍然允許生產代碼使用LocalDate.now() 也許是這樣的:

class Person {
    final String name;
    private final LocalDate dateOfBirth;
    private final LocalDate currentDate;

    // Used by regular production code
    Person(String name, LocalDate dateOfBirth) {
        this(name, dateOfBirth, LocalDate.now());
    }

    // Visible for test
    Person(String name, LocalDate dateOfBirth, LocalDate currentDate) {
        this.name = name;
        this.dateOfBirth = dateOfBirth;
        this.currentDate = currentDate;
    }

    long getAge() {
        return ChronoUnit.YEARS.between(dateOfBirth, currentDate);
    }

}

考慮這樣一個場景,自創建對象以來,該人的生日已經過去。 使用此實現, getAge()將基於 Person 對象的創建時間而不是當前日期。 我們可以通過使用Supplier<LocalDate>來解決這個問題:

class Person {
    final String name;
    private final LocalDate dateOfBirth;
    private final Supplier<LocalDate> currentDate;

    // Used by regular production code
    Person(String name, LocalDate dateOfBirth) {
        this(name, dateOfBirth, ()-> LocalDate.now());
    }

    // Visible for test
    Person(String name, LocalDate dateOfBirth, Supplier<LocalDate> currentDate) {
        this.name = name;
        this.dateOfBirth = dateOfBirth;
        this.currentDate = currentDate;
    }

    long getAge() {
        return ChronoUnit.YEARS.between(dateOfBirth, currentDate.get());
    }

    public static void main(String... args) throws InterruptedException {
        // current date 2016-02-11
        Person person = new Person("John Doe", LocalDate.parse("2010-02-12"));
        printAge(person);
        TimeUnit.DAYS.sleep(1);
        printAge(person);
    }

    private static void printAge(Person person) {
        System.out.println(person.name + " is " + person.getAge());
    }
}

輸出正確的是:

John Doe is 5
John Doe is 6

我們的單元測試可以像這樣注入“現在”日期:

@Test
void testGetAge() {
    Supplier<LocalDate> injectedNow = ()-> LocalDate.parse("2016-12-01");
    Person person = new Person("John Doe", LocalDate.parse("2004-12-01"), injectedNow);
    assertEquals(12, person.getAge());
}

它絕對不會提高性能。 您的問題與此類似:為什么我們使用變量? 我們可以簡單地在每次需要時重新計算所有內容。 對嗎?

如果你需要多次使用一個方法,但它有一個冗長的語法。

假設您有一個名為MyAmazingClass的類,其中有一個名為MyEvenBetterMethod (靜態)的方法,您需要在代碼的 15 個不同位置調用它 15 次。 當然,你可以做一些像......

int myVar = MyAmazingClass.MyEvenBetterMethod();
// ...
int myOtherVar = MyAmazingClass.MyEvenBetterMethod();
// And so on...

...但你也可以這樣做

Supplier<MyAmazingClass> shorter = MyAmazingClass::MyEvenBetterMethod;

int myVar = shorter.get();
// ...
int myOtherVar = shorter.get();
// And so on...

您混淆了函數式接口和方法引用。 Supplier只是一個接口,類似於Callable ,從 Java 5 開始你就應該知道了,唯一的區別是Callable.call允許拋出已檢查的Exception ,與Supplier.get不同。 所以這些接口會有類似的用例。

現在,這些接口也恰好是功能接口,這意味着它們可以作為方法引用來實現,指向一個現有的方法,該方法在調用接口方法時將被調用。

所以在 Java 8 之前,你必須寫

Future<Double> f=executorService.submit(new Callable<Double>() {
    public Double call() throws Exception {
        return calculatePI();
    }
});
/* do some other work */
Double result=f.get();

現在,你可以寫

Future<Double> f=executorService.submit(() -> calculatePI());
/* do some other work */
Double result=f.get();

Future<Double> f=executorService.submit(MyClass::calculatePI);
/* do some other work */
Double result=f.get();

何時使用Callable的問題根本沒有改變。

同樣,何時使用Supplier的問題不取決於你如何實現它,而是你使用哪個 API,即

CompletableFuture<Double> f=CompletableFuture.supplyAsync(MyClass::calculatePI);
/* do some other work */
Double result=f.join();// unlike Future.get, no checked exception to handle...

我將添加我的觀點,因為我對答案不滿意:當您想延遲執行時,供應商很有用。

無供應商

config.getLocalValue(getFromInternet() /*value if absent*/);

在調用 getLocalValue 之前,將調用 getFromInternet,但僅當本地值不存在時才會使用 getFromInternet() 的值。

現在,如果config.getLocalValue可以接受供應商,我們可以延遲此執行,而且如果存在本地值,我們將不會執行。

config.getLocalValue(() -> getFromInternet())

差異供應商使之成為可能: execute only when and if needed

供應商是否提高了性能或抽象級別的好處?

不,這不是為了提高性能。 Supplier用於延遲執行,即您指定無論何時使用都將運行的功能(代碼)。 以下示例演示了差異:

import java.time.LocalDateTime;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // Create a reference to the current date-time object when the following line is
        // executed
        LocalDateTime ldt = LocalDateTime.now();
        System.out.println(ldt);// Line-1

        // Create a reference to a functionality that will get the current date-time
        // whenever this functionality will be used
        Supplier<LocalDateTime> dateSupplier = LocalDateTime::now;

        // Sleep for 5 seconds
        Thread.sleep(5000);

        System.out.println(ldt);// Will display the same value as Line-1
        System.out.println(dateSupplier.get());// Will display the current date-time when this line will be executed

        // Sleep again for 5 seconds
        Thread.sleep(5000);

        System.out.println(ldt);// Will display the same value as Line-1
        System.out.println(dateSupplier.get());// Will display the current date-time when this line will be executed
    }
}

示例運行的輸出:

2021-04-11T00:04:06.205105
2021-04-11T00:04:06.205105
2021-04-11T00:04:11.211031
2021-04-11T00:04:06.205105
2021-04-11T00:04:16.211659

另一個有用的案例:

import java.util.List;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Hello", "B2C", "World", "Stack Overflow", "is", "a", "gr8", "platform");

        // A simple Stream for demo; you can think of a complex Stream with more
        // intermediate operations
        Stream<String> stream = list.stream()
                                    .filter(s -> s.length() <= 5)
                                    .map(s -> s.substring(1));

        System.out.println(stream.anyMatch(s -> Character.isLetter(s.charAt(0))));
        System.out.println(stream.anyMatch(s -> Character.isDigit(s.charAt(0))));
    }
}

輸出:

true
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.base/java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:528)
    at Main.main(Main.java:13)

輸出是不言自明的。 一個丑陋的解決方法可能是每次創建一個新的Stream ,如下所示:

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Hello", "B2C", "World", "Stack Overflow", "is", "a", "gr8", "platform");

        System.out.println(list.stream().filter(s -> s.length() <= 5).map(s -> s.substring(1))
                .anyMatch(s -> Character.isLetter(s.charAt(0))));
        
        System.out.println(list.stream().filter(s -> s.length() <= 5).map(s -> s.substring(1))
                .anyMatch(s -> Character.isDigit(s.charAt(0))));
    }
}

現在,看看使用Supplier可以多么干凈地做到這一點:

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Hello", "B2C", "World", "Stack Overflow", "is", "a", "gr8", "platform");

        Supplier<Stream<String>> streamSupplier = () -> list.stream()
                                                            .filter(s -> s.length() <= 5)
                                                            .map(s -> s.substring(1));

        System.out.println(streamSupplier.get().anyMatch(s -> Character.isLetter(s.charAt(0))));

        System.out.println(streamSupplier.get().anyMatch(s -> Character.isDigit(s.charAt(0))));
    }
}

輸出:

true
true

我相信您的查詢已經被提供的答案回答了。 這是我對供應商的理解。

它為我提供了默認的 Java 定義的接口,作為任何 lambda 方法引用目標返回時間的包裝器。 簡而言之,只要您編寫了沒有 args 並返回 Object 的方法,就有資格被供應商接口包裝。 沒有它,在預泛型中,您將返回作為對象類捕獲,然后進行轉換,在泛型中您將定義自己的 T(ype) 來保存返回值。 它只是提供了一個統一的返回類型泛型持有者,您可以在該持有者上調用 get() 從上面提到的 no arg 方法中提取返回的對象。

Joshua BlochEffective Java第 5 條中的解釋為我澄清了這一點:

在 Java 8 中引入的供應商接口非常適合表示工廠。 在輸入上接受供應商的方法通常應該使用有界通配符類型(條目 31)來約束工廠的類型參數,以允許客戶端傳入創建指定類型的任何子類型的工廠。 例如,下面是一種使用客戶提供的工廠生產每個瓷磚來制作馬賽克的方法:

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

您可能希望將資源工廠發送到將使用資源工廠創建對象的對象。 現在,假設您要發送不同的資源工廠,每個資源工廠在繼承樹的不同位置生成對象,但接收資源工廠的對象在任何情況下都必須能夠接收它們。

然后,您可以實現一個供應商,該供應商可以接受返回使用有界通配符擴展/實現某個類/接口的對象的方法,並將供應商用作資源工廠。

閱讀本書的第 5 項可能有助於完全理解用法。

這個例子說明了如何使用Supplier來提高性能,但Supplier本身不會提高性能。

/**
 * Checks that the specified object reference is not {@code null} and
 * throws a customized {@link NullPointerException} if it is.
 *
 * <p>Unlike the method {@link #requireNonNull(Object, String)},
 * this method allows creation of the message to be deferred until
 * after the null check is made. While this may confer a
 * performance advantage in the non-null case, when deciding to
 * call this method care should be taken that the costs of
 * creating the message supplier are less than the cost of just
 * creating the string message directly.
 *
 * @param obj     the object reference to check for nullity
 * @param messageSupplier supplier of the detail message to be
 * used in the event that a {@code NullPointerException} is thrown
 * @param <T> the type of the reference
 * @return {@code obj} if not {@code null}
 * @throws NullPointerException if {@code obj} is {@code null}
 * @since 1.8
 */
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
    if (obj == null)
        throw new NullPointerException(messageSupplier == null ?
                                       null : messageSupplier.get());
    return obj;
}

在這種方法中,供應商以一種只有對象真正為空的方式使用,然后獲取字符串。 因此,它可以提高操作的性能,但它不是Supplier而是它被如何使用。

暫無
暫無

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

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