七的博客

Java8函数式接口-Supplier

Java

Java8函数式接口-Supplier

Supplier 这个函数式接口, 应该是 Java8 里面几个通用接口中最不好理解的一个接口了,其他几个接口从名字中就可以大概猜的出来作用。

这个接口从名字中,还真是不太好猜,只能猜测到应该是跟供应或者生产有关系。在某单词软件中查询该单词的具体意思如下:

supplier,英语单词,主要用作名词,作名词时译为“供应厂商,供应国;供应者”。 [1]

所以这里如果需要将 Supplier 接口翻译成中文的话,怎么翻译都会有点不太准确。所以这里就直接不翻译了。

1. 通用 Supplier 接口定义

通用的 Supplier 接口主要分为以下几个:

  • Supplier 返回任意类型的结果。
  • BooleanSupplier 返回 boolean 类型的结果。
  • DoubleSupplier 返回 double 类型的结果。
  • IntSupplier 返回 int 类型的结果。
  • LongSupplier 返回 long 类型的结果。

其中 Supplier 接口接收泛型,意味着它可以返回任意类型对象。但是泛型是不接收基本数据类型的,为了解决让接口返回基本数据类型,因此有了下面几个 BooleanSupplier 等接口。

同时 BooleanSupplier 等接口的默认方法名字也不是 get() , 而是 getAsXxx(), 其中 Xxx 为基本类型的包装类型,比如 boolean 类型的方法名就是 getAsBoolean()

通过上面几个接口的讲解,你应该很容易就可以推测的出来,这种 Supplier 接口很容易就可以扩展出下面的接口:

  • 返回 float 类型的结果。
  • 返回 String 类型的结果。
  • 返回 …. 类型的结果。
  • ……

1.1 Supplier

源码:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

作用:不传入任何参数但会返回 一个任意类型 结果的操作。

1.2 BooleanSupplier

源码:

@FunctionalInterface
public interface BooleanSupplier {

    /**
     * Gets a result.
     *
     * @return a result
     */
    boolean getAsBoolean();
}

作用:不传入任何参数但会返回 一个 boolean 类型 结果的操作。

1.3 DoubleSupplier

源码:

@FunctionalInterface
public interface DoubleSupplier {

    /**
     * Gets a result.
     *
     * @return a result
     */
    double getAsDouble();
}

作用:不传入任何参数但会返回 一个 double 类型 结果的操作。

1.4 IntSupplier

源码:

@FunctionalInterface
public interface IntSupplier {

    /**
     * Gets a result.
     *
     * @return a result
     */
    int getAsInt();
}

作用:不传入任何参数但会返回 一个 int 类型 结果的操作。

1.5 LongSupplier

源码:

@FunctionalInterface
public interface LongSupplier {

    /**
     * Gets a result.
     *
     * @return a result
     */
    long getAsLong();
}

作用:不传入任何参数但会返回 一个 long 类型 结果的操作。

2. 互联网上对于 Supplier 的应用场景介绍

Supplier 接口特点就是没有入参,但是会有返回值。

无论是通过国内百度搜索,还是通过海外一个没有名字的搜索引擎去搜索 【Supplier 相关的中文关键字】文章,搜索出来的大部分都是比较简单的文档翻译跟例子。

特别是中文互联网上,有很多大部分文章都是抄来抄去,基本都是对 Supplier 的接口直白的翻译以及特别简单并且几乎不会出现在项目中的例子。

2.1 中文互联网情况

很多文章是为了演示 Supplier 这个接口的用法而写出来的例子, 这样的文章不要太多,随便列举几个例子 ( 为了不引战已经调整部分字符串内容 ):

public static void main(String[] args) {
    List < String > dataList = Arrays.asList("baiminyinwang", "zhangwuji", "zhangcuishan", "zhangsanfeng", "xiaozhao");

    Supplier < List < String >> supplier = () - > {
        List < String > filter = new ArrayList < > ();
        for (String data: dataList) {
            if (data.startsWith("zhang")) {
                filter.add(data);
            }
        }
        return filter;
    };

    List < String > result = supplier.get();
    System.out.println("Supplier:" + result);
}

这篇博客的例子就是从一个集合中过滤出内容是以 zhang 开头的元素,一眼看过去还没啥问题。仔细一看这不就是 filter ,直接用 Predicate 接口就可以通俗明了解决的事情,但是却为了举例而举例。

    public static void main(String[] args) {
        List < String > dataList = Arrays.asList("baiminyinwang", "zhangwuji", "zhangcuishan", "zhangsanfeng", "xiaozhao");

        final List < String > result = dataList.stream()
            .filter(data - > data.startsWith("zhang"))
            .collect(Collectors.toList());
    }

还有一些文章的例子就是演示返回一个固定的字符串:

final Supplier<String> generateString = () -> "返回一个固定的字符串!";

final String result = generateString.get(); 

这样解释又不说明应用场景的话,还不如直接定义一个字符串 final String result = " "; 就可以解决。

2.2 英文互联网情况

英文互联网优势在于英文这门语言全世界各地基本都在使用,尝试使用多种关键字组合搜索,很多帖子跟国内博客内容其实差不太多。

比如在一篇 Deep Dive into Supplier 的文章里面,标题是 深入 Supplier ,实际上点进去看例子:

Here’s a simple example of using a Supplier to generate a random number

import java.util.Random;
import java.util.function.Supplier;

public class SupplierExample {
    public static void main(String[] args) {
        // Create a Supplier to generate random numbers
        Supplier<Integer> randomSupplier = () -> new Random().nextInt(100);

        // Get a random number from the Supplier
        int randomNumber = randomSupplier.get();
        System.out.println("Random Number: " + randomNumber);
    }
}
import java.util.function.Supplier;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "Hello, Supplier!";
        
        String result = supplier.get();
        System.out.println(result);  // Output: Hello, Supplier!
    }
}

不过在 StackOverflow 上还是有很多人讨论这个,说明很多人对 Supplier 接口都有类似的疑问,比如下面这几篇:

其中有一个 BooleanSupplier 的例子:

static WeakHashMap<K, Reference<T>> cachemap = new WeakHashMap<>();
static T getCached(K key, BooleanSupplier isOK)
{
    synchronized(cachemap)
    {
        T item = cachemap.computeIfAbsent(key, k-> new SoftReference<>(new T(k, isOK.getAsBoolean()))).get();
        if (item == null) // Could be if reference has been GC'ed
        {
            item = new T(key, isOK.getAsBoolean());
            cachemap.put(key, new SoftReference<>(item));
        }
        return item;
    }
}

来源于: https://stackoverflow.com/questions/48352062/booleansupplier-usage-scenarios4

The reason I use BooleanSupplier is because often times, determination of “isOK” is a complex boolean statement including a complex Regex match. This function basically retrieves the cached value if previously created and it has not been garbage collected and only executes the “expensive” BooleanSupplier function if the item needs to be created. My code actually uses a RedundantLock but this is easier to read. This is also a good example of using WeakHashMap as a cache where T makes use of the key.

使用 BooleanSupplier 的原因是因为很多时候 ,“isOK” 是一个复杂的布尔语句,包括复杂的正则表达式匹配。

这个函数基本上获取之前创建的缓存值,并且该值尚未被垃圾收集,并且只有在需要时才执行 “昂贵的” BooleanSupplier 函数。

我的代码实际上使用了 RedundantLock,但这更容易阅读。这也是使用 WeakHashMap 作为 T 使用密钥的缓存的好例子。

从这几篇帖子中总结出来以下几点:

One of the advantages of the Supplier interface is providing a value in a lazy way.

Supplier 接口的优点之一是以惰性方式提供值。

We try to delay a supplier.get() call as far as possible, because it’s better to execute a resource-consuming operation (if you have such) at the moment when it really needs to be performed avoiding cases where the result of the operation can be ignored or might be skipped.

我们尽可能尝试延迟 supplier.get() 调用,因为最好执行在需要执行的时候再去执行消耗资源的操作。 这样可以避免出现以下结果的情况:这个操作可以被跳过的情况下。

3. JDK 官方是怎么用 Supplier 的

既然通过网上的资料比较难理解到 Supplier 具体的应用场景以及设计思路,那么我们直接直接去看看 JDK 官方是怎么用的,用在什么场景下,还有一些开源组件是怎么去使用的。

通过 Oracle 的文档我们找到了一篇 Uses of Interface java.util.function.Supplier ,访问地址是 https://docs.oracle.com/javase/8/docs/api/java/util/function/class-use/Supplier.html 。 文档主要描述了 java.util.function.Supplier 接口在 Java 8 中的使用情况。

使用到 Supplier 接口的包主要分布在下面几个钟:

  • java.lang
  • java.util
  • java.util.concurrent
  • java.util.logging
  • java.util.stream

3.1 java.lang 包

java.lang 包中 , ThreadLocal类的 withInitial() 方法使用了Supplier` 接口。

withInitial 方法的声明如下:

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

这个方法主要是使用 Supplier 来创建一个新的 ThreadLocal 实例。每当一个新线程访问这个 ThreadLocal 实例时 , 如果还没有给这个线程分配初始值 , 就会调用 supplier.get() 方法来获取一个初始值。实际是这么使用的:

final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认初始值");

final String value = threadLocal.get();

 // Output: 默认初始值
System.out.println(value); 

3.2 java.util 包

这个包下目前是有 Optional、OptionalLong,OptionalDouble,OptionalInt、Objects 几个类使用到。

3.2.1 Optional 相关类

OptionalorElseGetorElseThrow 方法有用到。

  • orElseGet 方法的签名:
  // 如果 Optional 中有值,则返回该值,否则调用 Supplier 并返回其结果。
     
  public T orElseGet(Supplier<? extends T> other) {
          return value != null ? value : other.get();
      }
  • orElseThrow 方法的签名:
  // 如果 Optional 中有值,则返回该值,否则调用 Supplier 获取一个异常并抛出。    
  public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
          if (value != null) {
              return value;
          } else {
              throw exceptionSupplier.get();
          }
      }

OptionalLong , OptionalDouble , OptionalIntorElseThrow 方法这些方法与 OptionalorElseThrow 方法差不多 , 只是针对基本类型的 Optional

3.2.2 Objects 类

ObjectsrequireNonNull 方法也用到:

    /**
     * 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.get());
        return obj;
    }

通过上面的方法注释部分:

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.

简单的总结下就是:

  • 不同于直接传入字符串消息的 requireNonNull(Object, String) 这个方法,这个方法允许把创建错误消息的工作推迟到检查完对象是否为 null 之后才进行。
  • 这样做在对象不是 null 的情况下会有性能上的优势,因为可以避免避免提前创建可能用不上的错误消息。
  • 但在使用这个方法时要注意,生成 messageSupplier 的成本要比直接创建字符串消息的成本低,否则就得不偿失了。

如果 obj 是 null,则调用 Supplier 获取自定义的异常信息并抛出 NullPointerException

只有当 objnull 时,才会调用 messageSupplier.get() 来生成异常消息。这避免了在对象不为 null 时创建不必要的字符串,从而提高了性能。

3.3 java.util.concurrent 包

这个包下只有 CompletableFuturesupplyAsync 方法使用了 Supplier 接口。

    /**
     * Returns a new CompletableFuture that is asynchronously completed
     * by a task running in the {@link ForkJoinPool#commonPool()} with
     * the value obtained by calling the given Supplier.
     *
     * @param supplier a function returning the value to be used
     * to complete the returned CompletableFuture
     * @param <U> the function's return type
     * @return the new CompletableFuture
     */
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }

    /**
     * Returns a new CompletableFuture that is asynchronously completed
     * by a task running in the given executor with the value obtained
     * by calling the given Supplier.
     *
     * @param supplier a function returning the value to be used
     * to complete the returned CompletableFuture
     * @param executor the executor to use for asynchronous execution
     * @param <U> the function's return type
     * @return the new CompletableFuture
     */
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }

这两个方法都是执行一个异步任务 , 然后通过调用给定的 Supplier 获取结果。

使用例子:

final CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        // ... 这里模拟业务逻辑
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "future 结果";
});

final String result = future.join();

 // Output: future 结果
System.out.println(result); 

3.4 java.util.logging 包

java.util.logging 包中 , Logger 类的多个日志记录方法使用了 Supplier 接口。

这些方法包括:

  • config
  • fine
  • finer
  • finest
  • info
  • log
  • logp
  • severe
  • warning

这些方法都接收一个 Supplier<String> 参数 , 用于在需要时惰性地构建日志消息。只有 当日志级别适合 时 , 才会调用 Supplier 来构建消息,这可以提高性能。

注意这里的日志级别适合,符合条件的时候才会去调用。

例子:

final Logger logger = Logger.getLogger("MyLogger");

logger.debug(() -> " 输出 debug 日志");

3.5 java.util.stream 包

java.util.stream 包中 , Supplier 接口被广泛使用,也就是我们常说的 Java8 流操作。

3.5.1 Collector 接口

Collector 接口的 supplier 方法返回一个 Supplier。这个 Supplier 用于创建一个新的可变结果容器。

    /**
     * A function that creates and returns a new mutable result container.
     *
     * @return a function which returns a new, mutable result container
     */
    Supplier<A> supplier();

这个方法返回一个 Supplier,用于创建一个新的可变结果容器。 这样开发可以自定义容器类型 , 然后提供一个相应的 Supplier 即可。

3.5.2 大量使用 Supplier 作为参数的方法

流操作中大量使用 Supplier 作为参数,包括下面这些:

  • StreamDoubleStreamIntStreamLongStreamcollect 方法
  • StreamSupportdoubleStreamintStreamlongStreamstream 方法
  • Streamgenerate 方法
  • CollectorsgroupingBygroupingByConcurrenttoCollectiontoConcurrentMaptoMap 方法
  • Collectorof 方法

4. 开源组件是怎么用 Supplier 的

除了看看 JDK 是怎么用的,我们也可以看看一些优秀的第三方开源组件是怎么使用的。稍微整理下了有这么几个典型的。

4.1 Guava

Google Guava 这个库提供了大量的工具类和辅助功能,使用 Supplier 的使用的地方在缓存和延迟加载中。

public final class Suppliers {
    
/**
   * Returns a supplier which caches the instance retrieved during the first call to {@code get()}
   * and returns that value on subsequent calls to {@code get()}. See: <a
   * href="http://en.wikipedia.org/wiki/Memoization">memoization</a>
   *
   * <p>The returned supplier is thread-safe. The delegate's {@code get()} method will be invoked at
   * most once. The supplier's serialized form does not contain the cached value, which will be
   * recalculated when {@code get()} is called on the reserialized instance.
   *
   * <p>If {@code delegate} is an instance created by an earlier call to {@code memoize}, it is
   * returned directly.
   */
  public static <T> Supplier<T> memoize(Supplier<T> delegate) {
    if (delegate instanceof NonSerializableMemoizingSupplier
        || delegate instanceof MemoizingSupplier) {
      return delegate;
    }
    return delegate instanceof Serializable
        ? new MemoizingSupplier<T>(delegate)
        : new NonSerializableMemoizingSupplier<T>(delegate);
  }
}


@GwtCompatible
@FunctionalInterface
public interface Supplier<T> extends java.util.function.Supplier<T> {
  /**
   * Retrieves an instance of the appropriate type. The returned object may or may not be a new
   * instance, depending on the implementation.
   *
   * @return an instance of the appropriate type
   */
  @CanIgnoreReturnValue
  T get();
}

这个方法创建了一个 Supplier , 它会保存 delegate 生成的值。 当再次请求相同的值时 , 直接返回保存的值 , 而不是再次调用 delegate

看下下面这个例子:

import com.google.common.base.Suppliers;

public class GuavaSupplierExample {
    public static void main(String[] args) {
        finalSupplier<String> supplier = () -> {
            System.out.println("从数据库中查询数据!");
            return "dbData";
        };

        final Supplier<String> memoizedSupplier = Suppliers.memoize(supplier);

        System.out.println(memoizedSupplier.get());
        System.out.println(memoizedSupplier.get());
    }
}

输出结果为:

从数据库中查询数据!
dbData
dbData

可以看到第二次已经直接从保存的值中直接获取。

4.2 Spring

  • ObjectProvider 作为 Spring 中 IoC 容器的一个关键抽象 , getIfAvailable(Supplier) 方就允许在找不到 bean 时使用 Supplier 提供一个默认实例。
  	/**
  	 * Return an instance (possibly shared or independent) of the object
  	 * managed by this factory.
  	 * @param defaultSupplier a callback for supplying a default object
  	 * if none is present in the factory
  	 * @return an instance of the bean, or the supplied default object
  	 * if no such bean is available
  	 * @throws BeansException in case of creation errors
  	 * @since 5.0
  	 * @see #getIfAvailable()
  	 */
  	default T getIfAvailable(Supplier<T> defaultSupplier) throws BeansException {
  		T dependency = getIfAvailable();
  		return (dependency != null ? dependency : defaultSupplier.get());
  	}
  • GenericApplicationContext 的 registerBean 方法允许直接传入一个 Supplier 来注册一个 bean。
  	/**
  	 * Register a bean from the given bean class, using the given supplier for
  	 * obtaining a new instance (typically declared as a lambda expression or
  	 * method reference), optionally customizing its bean definition metadata
  	 * (again typically declared as a lambda expression).
  	 * @param beanClass the class of the bean
  	 * @param supplier a callback for creating an instance of the bean
  	 * @param customizers one or more callbacks for customizing the factory's
  	 * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag
  	 * @since 5.0
  	 * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...)
  	 */
  	public final <T> void registerBean(
  			Class<T> beanClass, Supplier<T> supplier, BeanDefinitionCustomizer... customizers) {
  
  		registerBean(null, beanClass, supplier, customizers);
  	}

5. JDK 官方跟开源组件使用 Supplier 的共同点

  • 利用 Supplier 来实现控制流的延迟求值 , 推迟一些可能代价很高的操作到真正需要的时候。

  • 利用 Supplier 来表示某种【生产者】, 用它来封装对象的创建逻辑 , 屏蔽创建细节。

  • 利用 Supplier 来实现懒加载和缓存 , 需要的时候才去计算或创建对应的对象 , 计算完可以缓存起来供后续使用。

  • 利用 Supplier 来提供一些默认的【备选】行为 , 即在空值、找不到值的时候如何处理。

6. 项目中的实践

通过上面的一系列分析以及我个人在项目中的实践,大部分情况下还是用来延迟加载跟提供默认值等。

6.1 延迟加载

延迟加载也称作【惰性计算】,什么是【惰性计算】呢?

惰性计算就是在真正需要结果时才去实际执行计算,在定义的时候是不会去执行计算的。

这样可以避免不必要的计算,因为有些时候一些结果根本就没有被使用,计算出来也是对资源的浪费,还不如按需进行计算。

在电力采集项目中需要对采集到的数据进行一些复杂的计算 , 比如计算电力负荷曲线。

但并不是每次采集到数据都需要立即进行这种计算 , 可以等到需要使用计算结果的时候再进行。这时就可以用 Supplier 来封装这个计算过程:

public class PowerDataProcessor {
    public void process(PowerData data) {
        Supplier<LoadCurve> loadCurveSupplier = () -> {
            // 进行复杂的电力负荷曲线计算
            return calculateLoadCurve(data);
        };
        
        if (data.isValid()) {
            // 数据有效,可能需要使用电力负荷曲线
            LoadCurve loadCurve = loadCurveSupplier.get();
            
            // ... 使用 loadCurve 进行后续操作
        } else {
            
            // 数据无效,记录日志,但不需要计算电力负荷曲线
            log.warn("Invalid power data: " + data);
        }
    }
}

上面的代码使用 Supplier 可以更明确地表达延迟计算的意图。通过 Supplier 接口 , 明确地告诉阅读代码或者修改代码的人 , 这个计算过程是要延迟进行的。而使用普通方法 这个意图可能不那么明显。

6.2 提供默认值行为

在一些方法中 , 可以用 Supplier 参数来让调用者提供默认行为。

比如在 Stream 的 findFirst 操作中,可以提供一个 Supplier 来生成默认值:

public static <T> T findFirst(Stream<T> stream, Supplier<T> defaultValue) {
    return stream.findFirst().orElseGet(defaultValue);
}

7. Callable 跟 Supplier 的区别

看到 Supplier 的定义,联想到跟 Callable 的定义应该是一样的,都是无参带返回值。 那这两有什么区别。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

这两个的函数定义虽然是差不多,但是使用场景跟异常处理方面有着本质的差异。

从使用场景来说:

  • Supplier 接口通常用于延迟生成或提供值。在 Java 的 Stream API、Optional 类和其他许多延迟加载场景中有使用场景。

  • Callable 接口通常用于需要长时间执行的任务,也就是业务逻辑,并且这些任务可能会抛出异常。 常用于多线程中,提交任务后可以返回任务执行的结果。

从异常处理场景来说:

  • Supplierget() 方法没有声明检查性异常,所以是不允许抛出检查型异常(checked exceptions)的。但是如果是抛出 RuntimeException 是支持的。

  • Callablecall() 方法是声明了检查型异常。所以 Callable 在执行过程中运行抛异常,在异步任务中是特别有用的。

从上面的对比来说,最好根据不同的场景合理的划分使用这两个接口,如果非要强行使用也是支持的。

毕竟两个接口的声明都很相似,只要符合语法能通过编译程序都是可以正常执行。

参考链接