使用Lambda表达式进行惰性字段初始化

57
我希望能够利用lambda表达式实现延迟初始化(或者叫做延迟求值),避免使用if语句。因此,我希望拥有与下面代码中Foo属性相同的行为,但是不需要使用if语句。
class A<T>{
    private T fooField;

    public T getFoo(){
        if( fooField == null ) fooField = expensiveInit();
        return fooField;
    }
}

忽略这个解决方案不能保证以下情况的安全使用:1)多线程;2)将null作为T的有效值。
因此,为了表达将fooField的初始化推迟到第一次使用时的意图,我想声明fooField的类型为Supplier<T>,如下所示:
class A<T>{
   private Supplier<T> fooField = () -> expensiveInit();

   public T getFoo(){
      return fooField.get();
   }
}

然后,在getFoo属性中,我只需返回fooField.get()。但现在我希望下一次对getFoo属性的调用可以避免expensiveInit(),并且只返回以前的T实例。

如何在不使用if的情况下实现这一点?

尽管命名约定和将->替换为=>,则该示例也可以在C#中使用。然而,.NET Framework 4版本已经提供了具有所需语义的Lazy<T>


只需像这篇文章一样操作即可:https://dev59.com/OGw15IYBdhLWcg3wFHpZ - Fals
这是针对 .Net 的。但是在 Java 中是否有任何等效的 Lazy 呢? - rodolfino
2
@rodolfino 我认为有些混淆的地方在于你的问题上既有C#标签又有Java-8标签。也许你可以编辑问题,使其更清晰,表明你想要一个模仿C#行为的Java-8解决方案。 - ryanyuyu
14
请参考Guava的Suppliers.memoize - Ben Manes
相关:https://dev59.com/-2sy5IYBdhLWcg3w0RTT - AlikElzin-kilaka
你也可以查看vavr解决方案。 - gce
15个回答

41

在您的实际Lambda中,您可以简单地使用新的Lambda更新 fooField ,例如:

class A<T>{
    private Supplier<T> fooField = () -> {
       T val = expensiveInit();
       fooField = () -> val;
       return val;
    };

    public T getFoo(){
       return fooField.get();
    }
}

这个解决方案与 .Net 的 Lazy<T> 一样,不是线程安全的,并且不能确保对 getFoo 属性的并发调用返回相同的结果。


30

Miguel Gamboa的答案所采用的方法是非常好的:

private Supplier<T> fooField = () -> {
   T val = expensiveInit();
   fooField = () -> val;
   return val;
};

对于一次性懒加载字段,它的效果很好。然而,如果需要初始化多个字段,就必须复制并修改样板文件。另一个字段必须像这样初始化:

private Supplier<T> barField = () -> {
   T val = expensiveInitBar();          // << changed
   barField = () -> val;                // << changed
   return val;
};

如果您可以在初始化后每次访问都多调用一个方法,我将按照以下方式完成。首先,我会编写一个高阶函数,返回包含缓存值的Supplier实例:

static <Z> Supplier<Z> lazily(Supplier<Z> supplier) {
    return new Supplier<Z>() {
        Z value; // = null
        @Override public Z get() {
            if (value == null)
                value = supplier.get();
            return value;
        }
    };
}

这里需要使用匿名类,因为它具有可变状态,即已初始化值的缓存。

然后,创建许多惰性初始化字段就变得非常容易:

Supplier<Baz> fieldBaz = lazily(() -> expensiveInitBaz());
Supplier<Goo> fieldGoo = lazily(() -> expensiveInitGoo());
Supplier<Eep> fieldEep = lazily(() -> expensiveInitEep());

注意:我在问题中看到规定“不使用if”。对我而言,这并不清楚,是因为需要避免运行时昂贵的if条件(实际上它很便宜),还是更多地避免在每个getter中重复if条件。我假设这是后者,我的提议解决了这个问题。如果您担心if条件的运行时开销,则还应考虑调用lambda表达式的开销。

1
@srborlongan 你是不是指的是 AtomicReference<Z>? - Stuart Marks
1
@srborlongan 这会起作用,但不幸的是updateAndGet()并没有我们想要的事务语义。它可以被多次调用,因此文档警告可能会产生副作用。虽然在OP的情况下不会发生这种情况,但仍然存在风险。此外,无条件的updateAndGet()每次都会产生volatile读写。也许不是很重要,但严格来说并不必要。 - Stuart Marks
1
@srborlongan 你也可以使用AtomicReference<Supplier<Z>>,将其初始化为重量级初始化程序,该程序将其设置为返回缓存的供应商。此时,AtomicRef只是一个引用的持有者;最好编写一个类并使用字段。 - Stuart Marks
1
@StuartMarks,很好地应用了我的方法。我很欣赏lazily实用函数的想法。 - Miguel Gamboa
2
@MiguelGamboa 看起来我们都在彼此的想法上建立! - Stuart Marks
显示剩余3条评论

23

参考Miguel Gamboa的解决方案,并尝试在不牺牲其优雅性的情况下最小化每个字段的代码,我得出了以下解决方案:

interface Lazy<T> extends Supplier<T> {
    Supplier<T> init();
    public default T get() { return init().get(); }
}
static <U> Supplier<U> lazily(Lazy<U> lazy) { return lazy; }
static <T> Supplier<T> value(T value) { return ()->value; }

Supplier<Baz> fieldBaz = lazily(() -> fieldBaz=value(expensiveInitBaz()));
Supplier<Goo> fieldGoo = lazily(() -> fieldGoo=value(expensiveInitGoo()));
Supplier<Eep> fieldEep = lazily(() -> fieldEep=value(expensiveInitEep()));

每个字段的代码仅比 Stuart Marks's solution稍微大一些,但它保留了原始解决方案的良好属性:在第一次查询之后,只有一个轻量级的Supplier,它无条件地返回已经计算出的值。


2
你的惰性函数真棒。我花了一些时间来理解它! - Miguel Gamboa
这种方法的一个缺点是在第一次初始化之前/之后,fieldBaz会更改实现。由于热点配置文件调用站点以查看调用站点中有多少不同的实现,我认为这导致调用站点上的代码稍微不那么乐观。本质上,获得漂亮的无条件供应商的成本可能是调用站点上略微更复杂的代码(尽管最终可能会有完全相同的1个隐藏分支,这可能与Stuart的解决方案相同)。 - BeeOnRope
@BeeOnRope:我怀疑在当前使用分层编译的JVM中,第一次调用的行为并没有任何意义。拥有“几乎单态”调用站点且很少更改是非常普遍的,因此必须由合理的优化器考虑。当更改仅在第一次调用时发生时,甚至不需要取消优化任何内容(除非expensiveInit…()实际上是一个微不足道的方法)。 - Holger
@Holger:的确,编译器在编译具有不同多态程度的站点方面表现良好,但如果只有一个类在调用站点上被“看到”,与2个,与3个或更多,则仍会产生_不同的_代码(这取决于频率)。不过你可能是对的,因为“其他”类型仅在第一次调用时存在,因此JIT甚至可能不知道曾经使用过其他类型,因为在那个时候没有统计数据。此外,双态代码可能与保守单态代码一样快(但稍微大一些)。 - BeeOnRope
就我个人而言,我已经使用JMH对这里的几乎所有方法进行了基准测试。它们都非常接近,大多数情况下比“默认”的非惰性方法返回static final对象慢一个周期(约0.3纳秒)。看起来有一半的成本与泛型和擦除有关 - T被擦除为Object,当基准测试方法返回时需要将其强制转换回Integer(在现实世界中也需要进行转换)。如果您声明一个类型特定的Lazy类,则可以消除此成本。总的来说,我会选择最简单的方法,可能是沿着Stuart的建议走。 - BeeOnRope
这里是基准测试,以防其他人想要尝试它。 - BeeOnRope

19

6

这样怎么样?你可以使用Apache Commons的LazyInitializer来实现类似的功能:https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/concurrent/LazyInitializer.html

private static Lazy<Double> _lazyDouble = new Lazy<>(()->1.0);

class Lazy<T> extends LazyInitializer<T> {
    private Supplier<T> builder;

    public Lazy(Supplier<T> builder) {
        if (builder == null) throw new IllegalArgumentException();
        this.builder = builder;
    }
    @Override
    protected T initialize() throws ConcurrentException {
        return builder.get();
    }
}

那不是懒加载。它将始终调用内部构建器。 - rodolfino
我已经测试过了,它可以正常工作。请注意,它继承自LazyInitializer类。 - Hidden Dragon
1
那是C#还是Java?我认为LazyInitializer是.Net标准库的一部分,但是你提供了一个Java示例,因为你使用了Override注释。 - rodolfino
1
@rodolfino 我刚从.Net转到Java,但这个是Java Apache库中的:https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/concurrent/LazyInitializer.html - Hidden Dragon
4
@HiddenDragon,你可能需要在答案中提到Apache Commons。 - Magnilex

3

已支持

通过创建一个小接口并结合Java 8中引入的2个新特性:

  • @FunctionalInterface 注释(允许在声明时分配一个lambda表达式)
  • default 关键字(定义一个实现,就像抽象类-但在接口中)

可以获得与C#中看到的相同的Lazy<T>行为


用法

Lazy<String> name = () -> "Java 8";
System.out.println(name.get());

Lazy.java(复制并粘贴此接口到某个可访问的位置)

import java.util.function.Supplier;

@FunctionalInterface
public interface Lazy<T> extends Supplier<T> {
    abstract class Cache {
        private volatile static Map<Integer, Object> instances = new HashMap<>();

        private static synchronized Object getInstance(int instanceId, Supplier<Object> create) {

            Object instance = instances.get(instanceId);
            if (instance == null) {
                synchronized (Cache.class) {
                    instance = instances.get(instanceId);
                    if (instance == null) {
                        instance = create.get();
                        instances.put(instanceId, instance);
                    }
                }
            }
            return instance;
        }
    }

    @Override
    default T get() {
        return (T) Cache.getInstance(this.hashCode(), () -> init());
    }

    T init();
}

在线示例 - https://ideone.com/3b9alx

以下代码片段演示了这个辅助类的生命周期。

static Lazy<String> name1 = () -> { 
    System.out.println("lazy init 1"); 
    return "name 1";
};
    
static Lazy<String> name2 = () -> { 
    System.out.println("lazy init 2"); 
    return "name 2";
};

public static void main (String[] args) throws java.lang.Exception
{
    System.out.println("start"); 
    System.out.println(name1.get());
    System.out.println(name1.get());
    System.out.println(name2.get());
    System.out.println(name2.get());
    System.out.println("end"); 
}

将输出

start
lazy init 1
name 1
name 1
lazy init 2
name 2
name 2
end

请查看在线演示 - https://ideone.com/3b9alx

4
用这种方式使用 hashCode() 作为 Map 的键是不安全的。两个对象可能会得到相同的 hashCode。 - Christoffer Hammarström
@ChristofferHammarström 你说得对,感谢评论。java.lang.System.identityHashCode(obj); 这个方法可以解决问题吗? - Jossef Harush Kadouri
IdentityHashMap中,只需使用对象本身作为键。 - Christoffer Hammarström
1
然而还有另一个问题,缓存失效。如何从地图中删除任何内容?看起来像是一个严重的内存泄漏问题。 - Christoffer Hammarström
1
@ChristofferHammarström 谢谢!我会切换到对象引用本身。关于缓存:有意保留存储的结果引用,直到关闭进程。我同意,不过这是需要根据具体情况考虑的事情。 - Jossef Harush Kadouri

1
你可以按照以下方式进行操作:
   private Supplier heavy = () -> createAndCacheHeavy();

   public Heavy getHeavy()
   {
      return heavy.get();
   }

   private synchronized Heavy createAndCacheHeavy()
   {
      class HeavyFactory implements Supplier
      {
         private final Heavy heavyInstance = new Heavy();

         public Heavy get()
         {
            return heavyInstance;
         }
      }

      if(!HeavyFactory.class.isInstance(heavy))
      {
         heavy = new HeavyFactory();
      }

      return heavy.get();
   }

我最近看到 Venkat Subramaniam 的一个想法。我从this page上复制了代码。

基本思路是,一旦调用 Supplier,它就会用一个更简单的工厂实现替换自己,并返回初始化的实例。

这适用于线程安全的懒汉式单例初始化,但显然也可以应用于普通字段。


1
这里有一种方法,如果你想向expensiveInit方法传递参数(在初始化函数式接口时没有),也可以使用。
public final class Cache<T> {
    private Function<Supplier<? extends T>, T> supplier;

    private Cache(){
        supplier = s -> {
            T value = s.get();
            supplier = n -> value;
            return value;
        };
    }   
    public static <T> Supplier<T> of(Supplier<? extends T> creater){
        Cache<T> c = new Cache<>();
        return () -> c.supplier.apply(creater);
    }
    public static <T, U> Function<U, T> of(Function<? super U, ? extends T> creater){
        Cache<T> c = new Cache<>();
        return u -> c.supplier.apply(() -> creater.apply(u));
    }
    public static <T, U, V> BiFunction<U, V, T> of(BiFunction<? super U, ? super V, ? extends T> creater){
        Cache<T> c = new Cache<>();
        return (u, v) -> c.supplier.apply(() -> creater.apply(u, v));
    }
}

使用方法与Stuart Marks的回答相同:

private final Function<Foo, Bar> lazyBar = Cache.of(this::expensiveBarForFoo);

1
如果您需要类似于C#中的Lazy行为的东西,它可以为您提供线程安全和保证始终获得相同值的保证,那么避免使用if没有直接的方法。
您需要使用一个易失字段和双重检查锁定。这是一个具有最低内存占用的类版本,可为您提供C#行为:
public abstract class Lazy<T> implements Supplier<T> {
    private enum Empty {Uninitialized}

    private volatile Object value = Empty.Uninitialized;

    protected abstract T init();

    @Override
    public T get() {
        if (value == Empty.Uninitialized) {
            synchronized (this) {
                if (value == Empty.Uninitialized) {
                    value = init();
                }
            }
        }
        return (T) value;
    }

}

这种使用方式并不太优雅。您需要像这样创建懒惰值:

final Supplier<Baz> someBaz = new Lazy<Baz>() {
    protected Baz init(){
        return expensiveInit();
    }
}

你可以通过添加像这样的工厂方法,在增加内存占用的代价下获得更多的优雅性:
    public static <V> Lazy<V> lazy(Supplier<V> supplier) {
        return new Lazy<V>() {
            @Override
            protected V init() {
                return supplier.get();
            }
        };
    }

现在您可以简单地创建线程安全的延迟值,就像这样:
final Supplier<Foo> lazyFoo = lazy(() -> fooInit());
final Supplier<Bar> lazyBar = lazy(() -> barInit());
final Supplier<Baz> lazyBaz = lazy(() -> bazInit());

1

好吧,我并不是真的建议没有 "if",但这是我的看法:

一个简单的方法是使用 AtomicReference(三元运算符仍然像 "if" 一样):

private final AtomicReference<Something> lazyVal = new AtomicReference<>();

void foo(){
    final Something value = lazyVal.updateAndGet(x -> x != null ? x : expensiveCreate());
    //...
}

但是,有时候我们可能并不需要整个线程安全的魔法。所以我会像Miguel一样加入一点小变化:

由于我喜欢简单的一行代码,我只需使用三元运算符(再次读起来就像一个“if”语句),但我会让Java的求值顺序自动设置字段:

public static <T> Supplier<T> lazily(final Supplier<T> supplier) {
    return new Supplier<T>() {
        private T value;

        @Override
        public T get() {
            return value != null ? value : (value = supplier.get());
        }
    };
}

gerardw在上面的现场修改示例中,可以进一步简化而不需要“if”。我们只需要再次利用上述“技巧”:赋值运算符的结果是分配的值,我们可以使用括号来强制评估顺序。因此,使用上述方法就是:

static <T> Supplier<T> value(final T value) {
   return () -> value;
}


Supplier<Point> p2 = () -> (p2 = value(new Point())).get();

请注意,如果不想失去惰性,就不能内联使用“value(...)”方法。

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