Поддерживает ли Java 8 поддержку кэшей для поставщиков?

В библиотеке guava есть собственный Supplier, который не расширяет Java 8 Supplier. Кроме того, guava предоставляет кэш для поставщиков - Suppliers#memoize.

Есть ли что-то подобное, но для Java 8 поставщиков?

Ответ 1

Простейшим решением было бы

public static <T> Supplier<T> memoize(Supplier<T> original) {
    ConcurrentHashMap<Object, T> store=new ConcurrentHashMap<>();
    return ()->store.computeIfAbsent("dummy", key->original.get());
}

Однако самый простой не всегда самый эффективный.

Если вы хотите получить чистое и эффективное решение, прибегая к анонимному внутреннему классу для хранения изменчивого состояния, он окупится:

public static <T> Supplier<T> memoize1(Supplier<T> original) {
    return new Supplier<T>() {
        Supplier<T> delegate = this::firstTime;
        boolean initialized;
        public T get() {
            return delegate.get();
        }
        private synchronized T firstTime() {
            if(!initialized) {
                T value=original.get();
                delegate=() -> value;
                initialized=true;
            }
            return delegate.get();
        }
    };
}

В этом случае используется поставщик делегатов, который будет выполнять операцию в первый раз и впоследствии, заменит себя поставщиком, который безоговорочно вернет захваченный результат первой оценки. Поскольку он имеет семантику полей final, он может безоговорочно возвращаться без какой-либо дополнительной синхронизации.

Внутри метода synchronized firstTime() по-прежнему существует флаг initialized, потому что в случае одновременного доступа во время инициализации несколько потоков могут ждать в элементе методов до того, как делегат будет заменен. Следовательно, эти потоки должны обнаружить, что инициализация уже выполнена. Все последующие обращения будут читать новый поставщик делегатов и быстро получать значение.

Ответ 2

Нет встроенной функции Java для memoization, хотя ее не очень сложно реализовать, например, следующим образом:

public static <T> Supplier<T> memoize(Supplier<T> delegate) {
    AtomicReference<T> value = new AtomicReference<>();
    return () -> {
        T val = value.get();
        if (val == null) {
            val = value.updateAndGet(cur -> cur == null ? 
                    Objects.requireNonNull(delegate.get()) : cur);
        }
        return val;
    };
}

Обратите внимание, что существуют разные подходы к реализации. Вышеупомянутая реализация может вызвать делегата несколько раз, если замешанный поставщик запрашивал одновременно несколько раз из разных потоков. Иногда такая реализация предпочтительнее явной синхронизации с блокировкой. Если блокировка предпочтительнее, тогда можно использовать DCL:

public static <T> Supplier<T> memoizeLock(Supplier<T> delegate) {
    AtomicReference<T> value = new AtomicReference<>();
    return () -> {
        T val = value.get();
        if (val == null) {
            synchronized(value) {
                val = value.get();
                if (val == null) {
                    val = Objects.requireNonNull(delegate.get());
                    value.set(val);
                }
            }
        }
        return val;
    };
}

Также обратите внимание: как @LouisWasserman правильно упомянул в комментариях, вы можете легко преобразовать поставщика JDK в поставщика Guava и наоборот, используя ссылку на метод:

java.util.function.Supplier<String> jdkSupplier = () -> "test";
com.google.common.base.Supplier<String> guavaSupplier = jdkSupplier::get;
java.util.function.Supplier<String> jdkSupplierBack = guavaSupplier::get;

Так что это не большая проблема для переключения между функциями Guava и JDK.