【问题标题】:Lazy field initialization with lambdas使用 lambda 进行延迟字段初始化
【发布时间】:2015-05-21 21:01:56
【问题描述】:

我想在不使用 if 语句并利用 lambdas 的情况下实现惰性字段初始化(或延迟初始化)。因此,我希望具有以下 Foo 属性的相同行为,但没有 if

class A<T>{
    private T fooField;

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

忽略这个解决方案不能保证安全使用的事实:1)多线程; 2) null 作为T 的有效值。

因此,为了表达fooField 的初始化被推迟到第一次使用的意图,我想声明Supplier&lt;T&gt; 类型的fooField,例如:

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

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

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

如果不使用if,我怎样才能做到这一点?

尽管有命名约定并将-&gt;替换为=&gt;,那么这个例子也可以在C#中考虑。但是,NET Framework 版本 4 已经提供了具有所需语义的 Lazy&lt;T&gt;

【问题讨论】:

标签: lambda java-8 lazy-initialization


【解决方案1】:

在您的实际 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&lt;T&gt; 那样是线程安全的,并且不能确保对 getFoo 属性的并发调用返回相同的结果。

【讨论】:

    【解决方案2】:

    Miguel Gamboa's answer 采用的方法很好:

    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 表达式的开销。

    【讨论】:

    • @srborlongan 你是说 AtomicReference 吗?
    • @srborlongan 它会起作用,但不幸的是 updateAndGet() 并没有我们想要的事务语义。它可以被多次调用,因此文档会警告副作用。在 OP 的情况下不会发生这种情况,但仍然如此。此外,无条件的 updateAndGet() 每次都会导致易失性读写。也许没什么大不了的,但绝对没有必要。
    • @srborlongan 您也可以使用 AtomicReference>,使用重量级初始化程序对其进行初始化,将其设置为下次返回缓存的供应商。此时 AtomicRef 只是一个引用的持有者;还不如只写一个类并使用一个字段。
    • @StuartMarks,很好地应用了我的方法。我很欣赏lazily 实用函数的想法
    • @MiguelGamboa 看起来我们都在借鉴彼此的想法!
    【解决方案3】:

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

    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,它无条件地返回已经计算的值。

    【讨论】:

    • 你的懒惰功能很棒。我不得不花点时间来处理它!
    • 这种方法的一个缺点是 fieldBaz 在第一次初始化之前/之后更改了实现。由于热点配置文件调用站点以查看调用站点上有多少不同的实现,我认为这会导致调用站点的代码不太乐观。从本质上讲,获得好的无条件供应商的成本可能是调用站点上稍微复杂一点的代码(尽管它最终可能恰好有 1 个隐藏分支,这可能与 Stuart 的解决方案相同)。
    • @BeeOnRope:我怀疑 first 调用的行为有什么意义,尤其是在当前使用分层编译的 JVM 中。具有“几乎单态”的调用站点很少更改,这是非常常见的,并且必须由合理的优化器考虑。当更改仅在第一次调用时发生时,甚至不会有任何需要去优化的东西(除非 expensiveInit…() 实际上是一个微不足道的方法)。
    • @Holger:确实,编译器擅长编译具有不同程度多态性的站点,但如果在一个呼叫站点,对 2 个,对 3 个或更多,等等(这取决于频率)。尽管您可能是对的,因为“其他”类型仅在第一次调用时出现,JIT 甚至可能不知道曾经使用过其他类型,因为当时没有保留统计信息。此外,无论如何,双态代码可能与保守的单态代码一样快(但要大一些)。
    • FWIW,我用 JMH 对这里的所有方法进行了基准测试。它们都非常接近,大多比返回static final 对象的“默认”非延迟方法慢大约 1 个周期(~0.3 ns)。大约一半的成本与泛型和擦除有关 - T 被擦除为 Object,当基准方法返回时需要将其转换回 Integer(在现实世界中也需要转换)。如果你声明一个特定类型的Lazy 类,你就可以减少这个成本。总的来说,虽然我会做任何最简单的事情,可能会按照 Stuart 的建议。
    【解决方案4】:

    Project Lombok 提供了一个 @Getter(lazy = true) 注释,这正是您需要的。

    【讨论】:

      【解决方案5】:

      这个怎么样?那么你可以使用来自 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();
          }
      }
      

      【讨论】:

      • 这不是懒惰。这将始终调用内部构建器。
      • 我已经测试过了,它可以工作。请注意,它继承自 LazyInitializer 类。
      • 是c#还是Java?我认为 LazyInitializer 是 .Net 标准库的一部分,但在这里您提供了一个 Java 示例,因为您使用的是 Override 注释。
      • @rodolfino 我刚从 .Net 切换到 Java,但这个来自 Java Apache 库:commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/…
      • @HiddenDragon 您可能应该在答案中提及 apache commons。
      【解决方案6】:

      支持,

      通过创建一个小界面并结合 java 8 中引入的 2 个新特性:

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

      有可能获得与在 C# 中看到的相同的 Lazy&lt;T&gt; 行为


      用法

      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

      下面的 sn-p 演示了这个帮助类的生命周期

      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

      【讨论】:

      • 以这种方式使用hashCode() 作为 Map 键是不安全的。两个对象可能会得到相同的 hashCode。
      • @ChristofferHammarström 你是对的,谢谢你的评论。 java.lang.System.identityHashCode(obj); 会成功吗?
      • 只需使用对象本身作为IdentityHashMap 中的键。
      • 还有另一个问题。缓存失效。如何从地图中删除任何内容?看起来像是严重的内存泄漏。
      • @ChristofferHammarström 谢谢!我将切换到对象引用本身。关于缓存:故意保留存储的结果引用,直到您关闭进程。我同意,但是这是每个用例都需要考虑的事情
      【解决方案7】:

      您可以按照以下方式做一些事情:

         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复制了代码。

      基本思想是,一旦调用了供应商,就会用一个返回初始化实例的更简单的工厂实现替换自己。

      这是在单例的线程安全延迟初始化的上下文中,但显然您也可以将其应用于普通字段。

      【讨论】:

        【解决方案8】:

        如果您想将参数(在初始化函数接口时没有)传递给您的 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);
        

        【讨论】:

          【解决方案9】:

          如果您需要类似于 C# 中 Lazy 的行为的东西,它可以为您提供线程安全并保证您始终获得相同的值,那么没有直接的方法可以避免 if

          您将需要使用 volatile 字段和双重检查锁定。这是为您提供 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());
          

          【讨论】:

            【解决方案10】:

            好吧,我真的不建议没有“如果”,但这是我对此事的看法:

            一种简单的方法是使用 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(...)”方法。

            【讨论】:

              【解决方案11】:

              这个怎么样。 一些 J8 功能 switcheroos 以避免每次访问时使用 ifs。 警告:不知道线程。

              import java.util.function.Supplier;
              
              public class Lazy<T> {
                  private T obj;
                  private Supplier<T> creator;
                  private Supplier<T> fieldAccessor = () -> obj;
                  private Supplier<T> initialGetter = () -> {
                      obj = creator.get();
                      creator = null;
                      initialGetter = null;
                      getter = fieldAccessor;
                      return obj;
                  };
                  private Supplier<T> getter = initialGetter;
              
                  public Lazy(Supplier<T> creator) {
                      this.creator = creator;
                  }
              
                  public T get() {
                      return getter.get();
                  }
              
              }
              

              【讨论】:

                【解决方案12】:

                Stuart Mark 的解决方案,带有显式类。 (我认为这是否“更好”是个人喜好。)

                public class ScriptTrial {
                
                static class LazyGet<T>  implements Supplier<T> {
                    private T value;
                    private Supplier<T> supplier;
                    public LazyGet(Supplier<T> supplier) {
                        value = null;
                        this.supplier = supplier;
                    }
                
                    @Override
                    public T get() {
                        if (value == null)
                            value = supplier.get();
                        return value;
                    }
                
                }
                
                Supplier<Integer> lucky = new LazyGet<>(()->seven());
                
                int seven( ) {
                    return 7;
                }
                
                @Test
                public void printSeven( ) {
                    System.out.println(lucky.get());
                    System.out.println(lucky.get());
                }
                

                }

                【讨论】:

                  【解决方案13】:

                  2 种解决方案,一个函数 then 和一个对象(代码相同),线程安全没有“if”使用正确的类型传播(这里没有解决方案关心)。

                  很短。更好的惰性字段支持,由运行时处理,最终会使这段代码过时......

                  用法:

                  // object version : 2 instances (object and lambda)
                  final Lazy<Integer, RuntimeException> lazyObject = new LazyField<>(() -> 1);
                  
                  // functional version : more efficient than object, 1 instance
                  // usage : wrap computed value using eval(arg), and set the lazy field with result
                  Lazy<Service, IOException> lazyFunc = lazyField(() -> this.lazyFunc = eval(new Service()));
                  
                  // functional final version, as field is final this is less efficient than object :
                  // 2 instances one "if" and one sync (that could still be avoided...)
                  final Lazy<Integer, RuntimeException> finalFunc = lazyField(() -> eval(1));
                  
                  // Here the checked exception type thrown in lambda can only be ServiceException
                  static Lazy<Integer, ServiceException> lazyTest = lazyField(() -> {throw new ServiceException();});
                  

                  首先我定义了一个带有异常的 lambda:

                  @FunctionalInterface
                  interface SupplierWithException<T, E extends Exception> {
                      T get() throws E;
                  }
                  

                  然后是惰性类型:

                  interface Lazy<T, E extends Exception> extends SupplierWithException<T, E> {}
                  

                  功能版:

                  它直接返回一个最终获得更少内存占用的 lambda,如果不用于上面示例中的最终字段。

                  static <T, E extends Exception> Lazy<T, E> lazyField(Lazy<Lazy<T, E>, E> value) {
                      Objects.requireNonNull(value);
                      Lazy<T, E>[] field = new Lazy[1];
                      return () -> {
                          synchronized(field) {
                              if(field[0] == null)
                                  field[0] = value.get();
                              return field[0].get();
                          }
                      };
                  }
                  
                  static <T, E extends Exception> Lazy<T, E> eval(T value) {
                      return () -> value;
                  }
                  

                  不能强制给出正确的值回调,至少它总是返回相同的结果但可能无法避免“如果”(如在最终字段的情况下)。

                  对象版本:

                  从外面完全安全。

                  public final class LazyField<T, E extends Exception> implements Lazy<T, E> {
                  
                      private Lazy<T, E> value;
                  
                      public LazyField(SupplierWithException<T, E> supplier) {
                          value = lazyField(() -> new Lazy<T, E>() {
                              volatile Lazy<T, E> memBarrier;
                              @Override
                              public T get() throws E {
                                 value = memBarrier = eval(supplier.get());
                              }
                          });
                      }
                  
                      @Override
                      public T get() throws E {
                          return value.get();
                      }
                  }
                  

                  字段值的读取是无序的,但使用 volatile memBarrier 字段可确保写入该字段的值的顺序。如果在惰性值被有效设置后调用,则在此字段中设置的初始 lambda 也会返回初始化的惰性值。

                  享受

                  【讨论】:

                    【解决方案14】:

                    这是一个使用 Java 代理(反射)和 Java 8 供应商的解决方案。

                    * 因为使用了Proxy,所以发起的对象必须实现传递的接口。

                    * 与其他解决方案的不同之处在于从使用上对启动的封装。您开始直接使用DataSource,就好像它已被初始化一样。它将在第一个方法的调用时被初始化。

                    用法:

                    DataSource ds = LazyLoadDecorator.create(() -> initSomeDS(), DataSource.class)
                    

                    幕后:

                    public class LazyLoadDecorator<T> implements InvocationHandler {
                    
                        private final Object syncLock = new Object();
                        protected volatile T inner;
                        private Supplier<T> supplier;
                    
                        private LazyLoadDecorator(Supplier<T> supplier) {
                            this.supplier = supplier;
                        }
                    
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            if (inner == null) {
                                synchronized (syncLock) {
                                    if (inner == null) {
                                        inner = load();
                                    }
                                }
                            }
                            return method.invoke(inner, args);
                        }
                    
                        protected T load() {
                            return supplier.get();
                        }
                    
                        @SuppressWarnings("unchecked")
                        public static <T> T create(Supplier<T> supplier, Class<T> clazz) {
                            return (T) Proxy.newProxyInstance(LazyLoadDecorator.class.getClassLoader(),
                                    new Class[] {clazz},
                                    new LazyLoadDecorator<>(supplier));
                        }
                    }
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2020-05-10
                      • 1970-01-01
                      • 2011-06-04
                      • 2020-03-19
                      • 1970-01-01
                      相关资源
                      最近更新 更多