Java中如何使用lambda实现懒加载?

当我们使用需要执行昂贵或缓慢方法的资源(例如数据库查询或 REST 调用)时,我们倾向于使用本地缓存或私有字段。

lambda 函数允许我们使用方法作为参数并推迟方法的执行或完全省略它。

在本教程中,我们将展示如何使用 lambda 函数延迟初始化字段,注意不是延迟方法执行,而是实现字段懒加载。

但是在实现懒加载之前,先了解一些lambda 函数推迟方法的执行。

Lambda推迟方法执行
在Java中,Lambda表达式可以用于推迟方法的执行,特别是在与函数式接口(Functional Interface)一起使用时。

函数式接口是只包含一个抽象方法的接口。通过Lambda表达式,你可以将这个抽象方法的执行推迟到Lambda表达式被调用的时候。下面是一个简单的示例:

@FunctionalInterface
interface MyFunctionalInterface {
    void myMethod();
}

public class LambdaDelayExecution {
    public static void main(String[] args) {
        // 使用Lambda表达式实现函数式接口
        MyFunctionalInterface myFunction = () -> {
            System.out.println(
"方法执行!");
        };

       
// 调用Lambda表达式中的方法
       
// 这里才会真正执行Lambda表达式中的myMethod方法
        myFunction.myMethod();
    }
}

在上述示例中,MyFunctionalInterface是一个函数式接口,它定义了一个名为myMethod的抽象方法。通过Lambda表达式,我们可以实现这个接口并在Lambda表达式中定义myMethod的具体实现。在main方法中,当调用myFunction.myMethod()时,Lambda表达式中的方法才会真正执行。


需要注意的是,Lambda表达式常常与Java中的Streams API一起使用,这样可以更方便地实现推迟执行的效果。例如:

import java.util.Arrays;

public class LambdaDelayExecution {
    public static void main(String[] args) {
        // 使用Streams API中的forEach方法,延迟执行
        Arrays.asList(
"One", "Two", "Three")
              .stream()
              .map(s -> {
                  System.out.println(
"Mapping: " + s);
                  return s.toUpperCase();
              })
              .forEach(s -> System.out.println(
"ForEach: " + s));
    }
}

在这个例子中,map方法中的Lambda表达式的执行被延迟到forEach方法调用时。这种延迟执行的方式可以有效地进行数据处理和转换。

lambda 函数懒加载
Lambda函数只提供推迟方法执行,不提供字段懒初始化方式,但是我们可以转个弯使用。

import java.util.Optional;
import java.util.function.Supplier;
import java.util.concurrent.atomic.AtomicReference;

public class LazyLoadingExample {
     private final AtomicReference<String> data = new AtomicReference<>();
     
    private Supplier<String> lazyField = () -> createSomeType();


    private String createSomeType() {
        System.out.println("只执行一次哦");
       return
"Created";
     
    }
    
      public String getLazyField() {
        if (data.get() == null) {
            synchronized (data) {
                if (data.get() == null) {
                    data.set(lazyField.get());
                }
            }
        }
        return data.get();
    }

   public static void main(String[] args) {
        LazyLoadingExample example = new LazyLoadingExample();
         
//在第一次访问时,实例被创建
        String lazyField = example.getLazyField();
       
//在后续访问时,不会再次创建实例
        String lazyFieldAgain = example.getLazyField();
      
    }
}

上面代码输出:

只执行一次哦

getLazyField()中使用了Double-Checked Locking(双重检查锁定)保证多线程安全性。

  • 我们需要想象多个线程同时调用getData()方法。线程确实会阻塞,并且执行将按顺序进行,直到data.get()调用不为 null。一旦数据字段初始化完成,多个线程就可以并发访问它。
  • 有人可能会认为getData()方法中的双 null 检查是多余的,但事实并非如此。事实上,外部 null 检查确保当data.get()不为 null 时,线程不会阻塞在同步块上。

结论
在本文中,我们展示了使用 lambda 函数延迟初始化字段的不同方法。通过使用这种方法,我们可以避免多次执行昂贵的调用并推迟它们。我们的示例可以用作本地缓存或Project Lombok的lazy-getter 的替代方案。