为什么你觉得 java 中的 Lambda 抽象?手把手带你自定义一个 lambda 看不看?

本文我将用一个例子自定义一个lambda,让你看清 java 内置的lambda是怎么一回事

lambda 的官方定义

用于表示匿名函数(匿名方法)。它使得函数式编程风格成为可能,能简洁地表示实现单个抽象方法的接口(即函数式接口)的实例。

就是说匿名函数,内部类的语法糖

听不懂?那就直接代入场景吧!

场景

需求:把下面这个方法改造成自定义 lambda 表达式 ,让每个查询接口调用时都能有缓存的功能,下面这是个搜索图片列表的接口,好比搜索全部图片、搜索单张图片也要有缓存功能,你会怎么做?

A . 将缓存功能这段代码复制到别的接口

B. 将缓存功能封装成方法,哪个接口要缓存功能就调用

答案:二个选项都能实现,如果是真实项目我选B,简单直接。而这里还有一个选项, lambda 。

 @PostMapping("/list/page/vo/cache")
    public BaseResponse<Page<PictureVO>> listPictureVOByPageWithCache(@RequestBody PictureQueryRequest pictureQueryRequest,
                                                                      HttpServletRequest request) {
        long current = pictureQueryRequest.getCurrent();
        long size = pictureQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 普通用户默认只能查看已过审的数据
        pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
        // 构建本地缓存 key
        String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
        String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
        String cacheKey = "caffeine:" + hashKey;
        // 从本地缓存中查询
        String cachedValue = localCache.getIfPresent(cacheKey);
        if (cachedValue != null) {
            // 如果缓存命中,返回结果
            Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
            return ResultUtils.success(cachedPage);
        }
        // 构建分布式缓存 key
        String redisKey = "redis:" + hashKey;
        // 从 Redis 缓存中查询
        ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
        cachedValue = valueOps.get(redisKey);
        if (cachedValue != null) {
            // 如果缓存命中,返回结果
            Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
            // 存入本地缓存
            String localCacheValue = JSONUtil.toJsonStr(cachedPage);
            localCache.put(cacheKey, localCacheValue);
            return ResultUtils.success(cachedPage);
        }

        // 【查询数据库】
        Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
                pictureService.getQueryWrapper(pictureQueryRequest));
        // 【转换封装类】
        Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
        // 存入 Redis 缓存
        String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
        // 5 - 10 分钟随机过期,防止雪崩
        int cacheExpireTime = 300 +  RandomUtil.randomInt(0, 300);
        valueOps.set(redisKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS);
        // 存入本地缓存
        String localCacheValue = JSONUtil.toJsonStr(pictureVOPage);
        localCache.put(cacheKey, localCacheValue);

        // 返回结果
        return ResultUtils.success(pictureVOPage);
    }

先不急改造,先理解一下函数式接口概念,再看一眼官方定义lambda,模仿改造!

函数式接口

java 8 之前方法只能接收变量,java 8 能接收方法作为参数啦!

一种特殊的接口,定义的语法跟接口很像,不过函数式接口在定义方法时能当作参数,还记得lambda 定义吗?传递行为(方法),那么dogAct() 方法调用的时候不正是传递行为(方法)

// 函数式接口
@FunctionalInterface
public interface Animal {
    /**
     * 动物的行为
     * @return Object
     */
    Object act();
}

// 小狗类
public class Dog {
    // 使用函数式接口定义方法的参数,方法类型就是函数式接口
    public Object dogAct(Animal animalAct) {
        // 方法体
        ...
    }
}

模仿官方使用 lambda 举例

我们自定义一个 ArrayList 的 sort 方法,自定义的方法名叫做 listSort(), 因为它的 sort 内部太复杂了,我们简化方法逻辑为插入排序

自定义的 listSort():

// 工具类
public class ListUtils {

    /**
     * 用插入排序对 List 进行原地排序
     * @param list 要排序的列表
     * @param comp 比较器,通过 Lambda 或方法引用传入
     * @param <T>  元素类型
     */
    public static <T> void listSort(List<T> list, Comparator<? super T> comp) {
        int n = list.size();
        for (int i = 1; i < n; i++) {
            T key = list.get(i);
            int j = i - 1;
            // 找到插入位置
            while (j >= 0 && comp.compare(list.get(j), key) > 0) {
                list.set(j + 1, list.get(j));
                j--;
            }
            list.set(j + 1, key);
        }
    }
}

使用 lambda 语法调用这个方法:

listSort(nums, (a, b) -> a - b); // 这就是lambda表达式,传递了a-b的行为

这个调用和使用 lambda 语法一模一样吧!官方:list.sort(list, (a, b) -> a - b) 

原理分析

lambda 表达式调用方关注行为,lambda 表达式的本质是传递行为函数式接口可以被传递,调用方可实现函数式接口,那不就是把方法体传递给工具类,工具类已经定义好整体的实现包含了调用函数式接口,工具类整体逻辑就完整了。

不明白?画图给你看看,这不就是方法体和形参反过来了而已

我的理解:listSort() 的插入排序代码中,我们想改变升序/降序,调用时修改方法体即可,所以方法体这部分是随用户需求【变化的部分】

要想自定义一个 lamba 表达式方法,就要抓住在你业务中「变化的部分」和「不变的部分」,把「变化的部分」当成行为传递,「不变的部分」接收函数式接口参数,并把通用逻辑写入方法体。

自定义 lambda

思路:

1. 我们定义 lambda 是为了封装通用(不变)的部分,方便在多处需要的地方简洁调用。

2. 我们发现了多处都需要某个方法,而大部分逻辑相同,只需要对方法小改动(变化)就足够。

3. 我们发现准确的分离出需要小改动(变化)的部分与不变的部分

4. 将变化的部分当作lambda 要传递的方法体,不变的部分当作一个新的封装的方法,包含了整体逻辑

按照这个思路,回到原来的场景,找出「变化的部分」与 「不变的部分」

对于普遍的接口来说,这个方法中 「变化的部分」是 【查询数据库】和【转换封装类】

解释:

  • 每个查询接口的过滤条件、排序逻辑、联表关联都不一样,都是由调用方(Controller/Service)根据具体需求来定义。
  • 各个业务模块对应的 VO 结构不同:有的要多级字段转换,有的要调用第三方服务补数据,有的要拼装安全签名,流程各不相同。

不变的部分: 缓存命中检查、回写流程、序列化/反序列化、统一包装返回……这些流程对任何业务场景都相同。

改造实施

1) 根据变化的部分定义函数式接口

@FunctionalInterface
public interface DataLoader<R> {
    /**
     * 如果缓存未命中,就执行这个方法去获取最新数据
     * @return 最新的数据结果
     */
    R load();
}

2) 根据不变的部分定义一个通用工具类, 工具类的方法是不变部分的流程与变化部分的流程

public class CacheTemplate {

    /**
     * 缓存模板
     *
     * @param localCache    本地缓存实例(如 Caffeine Cache)
     * @param redisOps      Redis 的 ValueOperations
     * @param cacheKey      本地缓存 key
     * @param redisKey      Redis 缓存 key
     * @param expireSeconds Redis 过期时间(可支持随机化)
     * @param loader        可变行为:缓存未命中时如何加载数据
     * @param <R>           返回值类型
     * @return R            最终结果
     */
    public static <R> R executeWithCache(
        Cache<String, String> localCache,
        ValueOperations<String, String> redisOps,
        String cacheKey,
        String redisKey,
        long expireSeconds,
        DataLoader<R> loader
    ) {
        // 1. 本地缓存
        String cached = localCache.getIfPresent(cacheKey);
        if (cached != null) {
            return JSONUtil.toBean(cached, new TypeReference<R>(){});
        }

        // 2. Redis 缓存
        cached = redisOps.get(redisKey);
        if (cached != null) {
            R result = JSONUtil.toBean(cached, new TypeReference<R>(){});
            localCache.put(cacheKey, JSONUtil.toJsonStr(result));
            return result;
        }

        // 3. 缓存未命中:调用可变部分
        R fresh = loader.load();

        // 4. 回写 Redis + 本地
        String json = JSONUtil.toJsonStr(fresh);
        redisOps.set(redisKey, json, expireSeconds, TimeUnit.SECONDS);
        localCache.put(cacheKey, json);

        // 5. 返回
        return fresh;
    }
}

注意:泛型根据调用方传入 loader 方法体时 return 的类型推断。

3) 原来的接口改造,我们改用 lambda 表达式调用

@PostMapping("/list/page/vo/cache")
public BaseResponse<Page<PictureVO>> listPictureVOByPageWithCache(
    @RequestBody PictureQueryRequest req,
    HttpServletRequest request
) {
    // ... 前置校验 + 构造 key 的代码保持不变 ...

    // 计算一个随机过期时间
    long expire = 300 + RandomUtil.randomInt(0, 300);

    // 调用缓存模板,传入“加载数据的行为”:
    Page<PictureVO> page = CacheTemplate.executeWithCache(
        localCache,
        stringRedisTemplate.opsForValue(),
        cacheKey,
        redisKey,
        expire,
        () -> {
            // 这一段 —— 只有它在变化 —— 由调用方(Controller)传入
            Page<Picture> picturePage = pictureService.page(
                new Page<>(current, size),
                pictureService.getQueryWrapper(req)
            );
            return pictureService.getPictureVOPage(picturePage, request);
        }
    );

    return ResultUtils.success(page);
}

这就完成啦,在任何接口中要用缓存+查询功能,只需要用这句lambda 表达式,非常的通用!

还是理解不透彻?上手实战一把,彻底理解!!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值