在Java 8中,我们什么时候应该使用Supplier?

55

这段代码有什么不同之处?

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中的函数式接口,但不理解Supplier的好处。准确地说,应该何时以及如何使用它们。Supplier是否提高了性能或者在抽象级别上有什么好处?

谢谢您的答复!这并不是重复的问题,因为我已经搜索了,但没有找到我需要的。

更新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

7
s2会在赋值时记录"现在"的时间点。s1.get()会在调用get()方法时记录"现在"的时间点。如果你将Supplier传递到某个需要使用它的地方,那么这两个时间点可能不一样。如果稍后再次调用s1.get(),你将再次获得一个不同的时间。 - khelwood
2
当您需要将获取多个值的能力传递到另一个方法中时,请使用它。这几乎是所有有效用例的100%。 - Louis Wasserman
9个回答

40

我将演示一个场景,在这种情况下,我们应该使用 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());
    }
}

在生产环境中,这个方案运行良好。但单元测试要么需要将系统日期设置为已知值,要么每年更新以期望返回的年龄增加一岁,这两种解决方案都相当糟糕。

更好的解决方案是让单元测试注入一个已知日期,同时仍然允许生产代码使用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());
}

6
延迟执行是这个意思。 - Mushtaq Jameel
这是一个很好的例子,展示了如何通过使用 Supplier<T> 延迟执行 T 的某些函数或者按需使用它。太棒了! - Rany Albeg Wein

23

这绝对不会提高性能。你的问题类似于这个问题:为什么我们要使用变量?我们可以在每次需要时重新计算一切。对吗?

如果你需要经常使用某个方法,但它的语法太冗长。

假设你有一个名为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...

19
当然应该是Supplier<Integer>。这与性能或方法的冗长程度无关,而是有语义上的区别。 - biziclop

23

供应商会提高性能或者在抽象层面上带来好处吗?

不会。 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

11
你混淆了函数式接口和方法引用。Supplier只是一个接口,类似于Java 5以来就应该知道的Callable,唯一的区别是Callable.call允许抛出已检查的异常,而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...

6
我会补充我的观点,因为我对答案不满意: 当您想要延迟执行时,Supplier很有用。
没有Supplier。
config.getLocalValue(getFromInternet() /*value if absent*/);

在调用 getLocalValue 之前,将会调用 getFromInternet,但只有在本地值不存在时才会使用 getFromInternet() 的值。
现在,如果 config.getLocalValue 可以接受 supplier,我们可以延迟此执行,并且如果本地值存在,则不会执行。
config.getLocalValue(() -> getFromInternet())

不同之处 供应商使其成为可能:只在需要时执行


2
《Effective Java》(Java高效编程)的第5条中,它的解释让我明白了这一点:
Supplier接口是Java 8中引入的,非常适合表示工厂。输入Supplier的方法通常应使用有界通配符类型(Item 31)来约束工厂的类型参数,以允许客户端传递一个可以创建指定类型任何子类型的工厂。例如,下面是一个使用客户提供的工厂来生成每个图块的方法: Mosaic create(Supplier<? extends Tile> tileFactory) { ... } 你可能想要将资源工厂发送到一个对象中,该对象将使用资源工厂创建对象。现在,假设您想发送不同的资源工厂,每个资源工厂在继承树的不同位置生成对象,但接收资源工厂的对象必须能够在任何情况下接收它们。
然后,您可以实现一个供应商,它可以接受返回扩展/实现某个类/接口的对象的方法,使用有界通配符,并将供应商用作资源工厂。
阅读该书的第5条可能有助于完全理解用法。

1
我相信你的问题已经得到了之前提供的答案。这是我的理解供应商。
它为任何lambda方法引用目标返回时间提供默认的Java定义接口,充当包装器。简而言之,您在没有参数并返回对象的方法中编写的任何位置都符合供应商接口的包装要求。如果没有这个,在泛型之前,你会捕获返回对象类,然后强制转换,在泛型中,你会定义自己的T(类型)来保存返回值。它只是提供一个统一的返回类型通用持有者,您可以调用get()从上述无参方法中提取返回的对象。

1

这个接口的主要用途之一是实现延迟执行,即在需要时再执行。例如,Optional类有一个名为orElseGet的方法。如果optional没有数据,将触发此方法。以下是示例:

@Test public void supplierWithOptional(){ Supplier doubleSupplier = () -> Math.random(); Optional optionalDouble = Optional.empty(); System.out.println(optionalDouble.orElseGet(doubleSupplier)); }

来源:https://medium.com/swlh/understanding-java-8s-consumer-supplier-predicate-and-function-c1889b9423d


1
这是一个关于如何使用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;
}

在这种方法中,供应商的使用方式只有在对象实际上为null时才获取字符串。因此,它可以提高操作的性能,但它并不是Supplier,而是它的使用方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接