1. Supplier接口:Java 8的懒加载神器

第一次看到Supplier接口时,我正被一个性能问题困扰:系统启动时需要加载几十个配置文件,但实际运行时可能只用得到其中三五个。当时用传统方式初始化所有配置,导致启动时间长达15秒。直到同事扔给我一行用Supplier改造的代码,启动时间直接缩短到3秒——这就是Supplier给我的下马威。

作为Java 8函数式编程四大金刚之一(Supplier、Consumer、Predicate、Function),Supplier的特殊之处在于它是个纯输出型选手。它的函数描述符是() -> T,就像个魔法口袋,不需要你给任何东西(无参数),却能随时掏出指定类型的对象(有返回值)。这种特性在下面这些场景特别管用:

  • 延迟加载:像开头说的配置加载场景,用Supplier包装后真正做到了"按需加载"
  • 资源池管理:数据库连接池可以用Supplier封装连接创建逻辑
  • 测试替身:轻松构造模拟数据生成器
  • 条件初始化:比如根据运行时环境决定初始化哪种实现类

来看个真实案例。我们有个电商促销系统,需要根据不同活动类型创建不同的策略处理器:

Map<String, Supplier<Strategy>> strategySuppliers = new HashMap<>();
strategySuppliers.put("flash_sale", FlashSaleStrategy::new);
strategySuppliers.put("coupon", CouponStrategy::new);

// 使用时才初始化具体策略
Strategy strategy = strategySuppliers.get(eventType).get();

这种写法比传统工厂模式简洁得多,而且新增策略类型时只需往Map里put一个新Supplier,完全符合开闭原则。

2. 从语法糖到性能优化:Supplier的四种打开方式

2.1 Lambda表达式:最灵活的配方

用Lambda实现Supplier就像写匿名类,但清爽十倍。去年做日志系统改造时,我发现用Lambda配合Supplier能让日志输出性能提升40%,关键就在这个延迟计算技巧:

Logger logger = LoggerFactory.getLogger("order");
Supplier<String> expensiveLog = () -> {
    // 模拟耗时操作
    Thread.sleep(1000);
    return "Order details: " + order.getDetails();
};

// 只有当日志级别为DEBUG时才执行计算
if(logger.isDebugEnabled()){
    logger.debug(expensiveLog.get());
}

这里有个坑要注意:Lambda表达式里如果使用外部变量,该变量必须是final或等效final的。我有次在循环里用Lambda就踩了这个坑:

List<Supplier<String>> suppliers = new ArrayList<>();
for(int i=0; i<5; i++){
    // 编译报错!i不是final
    suppliers.add(() -> "No." + i); 
}

解决方法要么用final局部变量中转,要么用Stream的range:

IntStream.range(0,5).forEach(i -> 
    suppliers.add(() -> "No." + i)
);

2.2 方法引用:现成零件的组装

方法引用让Supplier的使用更丝滑。我们团队有个工具类专门生成随机测试数据:

public class TestDataGenerator {
    public static String randomName() {
        return "User_" + UUID.randomUUID().toString().substring(0,8);
    }
    
    public LocalDate randomBirthday() {
        return LocalDate.now()
               .minusYears(ThreadLocalRandom.current().nextInt(18, 60))
               .minusMonths(ThreadLocalRandom.current().nextInt(12))
               .minusDays(ThreadLocalRandom.current().nextInt(28));
    }
}

// 静态方法引用
Supplier<String> nameSupplier = TestDataGenerator::randomName;

// 实例方法引用
TestDataGenerator generator = new TestDataGenerator();
Supplier<LocalDate> birthdaySupplier = generator::randomBirthday;

在Spring项目中,我常用方法引用把Bean的获取逻辑包装成Supplier:

@Configuration
public class AppConfig {
    @Bean
    public Supplier<PaymentService> paymentServiceSupplier() {
        return this::creditCardPaymentService;
    }
    
    @Bean
    public PaymentService creditCardPaymentService() {
        return new CreditCardPaymentService();
    }
}

2.3 构造方法引用:对象工厂的捷径

类名::new创建Supplier是我在实现对象池时的最爱。比如数据库连接池的简化实现:

public class ConnectionPool {
    private Queue<Supplier<Connection>> pool = new LinkedList<>();
    
    public ConnectionPool(int size) {
        IntStream.range(0, size)
                 .forEach(i -> pool.add(MySqlConnection::new));
    }
    
    public Connection getConnection() {
        return pool.poll().get();
    }
}

这里有个性能优化点:对于复杂对象,可以结合备忘录模式缓存构造结果:

Supplier<ExpensiveObject> supplier = () -> {
    ExpensiveObject obj = new ExpensiveObject();
    // 缓存构造结果
    supplier = () -> obj; 
    return obj;
};

2.4 特定类型Supplier:避免装箱拆箱

Java 8贴心地提供了IntSupplierLongSupplier等变体。在做高频交易系统时,这些特化接口能避免自动装箱带来的性能损耗:

// 普通Supplier会有装箱开销
Supplier<Integer> boxed = () -> 1024;

// IntSupplier直接返回int
IntSupplier unboxed = () -> 1024;

// 性能对比测试
long start = System.nanoTime();
for(int i=0; i<1_000_000; i++){
    boxed.get();
}
System.out.println("Boxed: " + (System.nanoTime()-start)/1_000_000 + "ns");

start = System.nanoTime();
for(int i=0; i<1_000_000; i++){
    unboxed.getAsInt();
}
System.out.println("Unboxed: " + (System.nanoTime()-start)/1_000_000 + "ns");

测试结果在我的笔记本上显示,原始类型Supplier比通用版本快约2.7倍。虽然单次调用差异只有几纳秒,但在高频交易场景积少成多就很可观了。

3. 实战进阶:用Supplier设计模式重构代码

3.1 实现真正的延迟初始化

传统的双重检查锁实现单例要写十多行代码,用Supplier三行搞定:

public class LazySingleton {
    private static final Supplier<LazySingleton> INSTANCE = () -> {
        LazySingleton instance = new LazySingleton();
        // 初始化操作
        return instance;
    };
    
    public static LazySingleton getInstance() {
        return INSTANCE.get();
    }
}

这里利用了Java语言规范对final变量的线程安全保证。更妙的是,这种写法天然支持异常处理:

Supplier<Config> configSupplier = () -> {
    try {
        return loadConfigFromRemote();
    } catch (IOException e) {
        return loadLocalBackupConfig();
    }
};

3.2 构建轻量级数据工厂

我们给客户做的报表系统需要支持动态数据源,用Supplier实现的工厂比抽象工厂模式简洁得多:

public class ReportGenerator {
    private Supplier<DataConnector> connectorSupplier;
    
    public ReportGenerator(Supplier<DataConnector> supplier) {
        this.connectorSupplier = supplier;
    }
    
    public void generate() {
        DataConnector connector = connectorSupplier.get();
        // 使用连接器获取数据...
    }
}

// 使用时
new ReportGenerator(MysqlConnector::new).generate();
new ReportGenerator(() -> new OracleConnector(config)).generate();

这种写法特别适合依赖注入框架。在Spring中可以直接用@Qualifier指定Supplier:

@Bean 
public Supplier<DataConnector> mysqlConnector() {
    return MysqlConnector::new;
}

@Bean
public ReportGenerator reportGenerator(
       @Qualifier("mysqlConnector") Supplier<DataConnector> supplier) {
    return new ReportGenerator(supplier);
}

3.3 实现条件化资源提供

在微服务架构中,我们常用Supplier实现降级逻辑。比如当主服务不可用时自动切换备用服务:

public class ServiceRouter {
    private Supplier<OrderService> primary = RemoteOrderService::new;
    private Supplier<OrderService> fallback = LocalOrderService::new;
    
    public OrderService getService() {
        try {
            OrderService service = primary.get();
            // 心跳检测
            service.ping();
            return service;
        } catch (Exception e) {
            return fallback.get();
        }
    }
}

更进一步,可以结合Memoization(记忆化)技术避免重复创建对象:

Supplier<OrderService> memoizedSupplier = Suppliers.memoize(() -> {
    System.out.println("Creating new service...");
    return new RemoteOrderService();
});

// 第一次调用会创建对象
OrderService s1 = memoizedSupplier.get(); 
// 后续调用返回缓存对象
OrderService s2 = memoizedSupplier.get();

4. 避坑指南:Supplier使用的七个注意事项

  1. 线程安全问题:像所有函数式接口一样,Supplier本身无状态,但要小心get()方法内部的共享状态。我有次在Supplier里用了SimpleDateFormat就遭遇了诡异的日期错乱问题。

  2. 空指针陷阱:Supplier可以返回null,但调用方容易忘记判空。建议用Optional包装:

    Supplier<Optional<User>> safeSupplier = () -> Optional.ofNullable(getUser());
    
  3. 性能监控:由于延迟执行的特性,Supplier内部的耗时操作容易被忽略。我们在生产环境用AOP给所有Supplier加了执行时间监控:

    @Around("execution(* java.util.function.Supplier.get(..))")
    public Object monitorSupplier(ProceedingJoinPoint pjp) {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long duration = System.currentTimeMillis() - start;
        if(duration > 100) {
            log.warn("Slow supplier: " + pjp.getSignature() + " took " + duration + "ms");
        }
        return result;
    }
    
  4. 异常处理:get()方法签名没有throws声明,所以检查异常需要特殊处理。推荐用Either模式:

    Supplier<Either<Exception, Result>> safeSupplier = () -> {
        try {
            return Either.right(riskyOperation());
        } catch (Exception e) {
            return Either.left(e);
        }
    };
    
  5. 记忆化副作用:memoize虽好,但要小心副作用。比如这个Supplier每次调用本应返回新随机数:

    Supplier<Integer> randomSupplier = Suppliers.memoize(
        () -> ThreadLocalRandom.current().nextInt()
    );
    // 会返回相同的随机数!
    randomSupplier.get(); 
    randomSupplier.get();
    
  6. 循环引用:在实现DI容器时遇到过Supplier导致的循环依赖:

    Supplier<A> aSupplier = () -> new A(bSupplier.get());
    Supplier<B> bSupplier = () -> new B(aSupplier.get());
    
  7. 调试困难:Lambda形式的Supplier在栈追踪中显示为lambda$xxx,可以用方法引用提升可调试性。

更多推荐