本文我将用一个例子自定义一个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 表达式,非常的通用!
还是理解不透彻?上手实战一把,彻底理解!!
331

被折叠的 条评论
为什么被折叠?



