目录
大家好,我是jstart千语。今天给大家来分享一下代码设计优化的问题,代码设计方面在评审时是非常重要的,大家在平时练习的时候可能不太会注意,只要结果运行出来就行了。但如果要维护好一个项目的话,代码还是要追求一些性能的,所以我建议大家在平时书写时也要注意一下优化,特别是涉及到多层循环时的问题。
一、问题引出
从数据库中分页查询图片时,得到如Page<Picture>。我们要进行转换成Page<PictureVO>,也就是要将Picture脱敏后转成VO类型返回给用户。
Picture和PictureVO的区别:
Picture里有userId属性。PictureVo里多了一个UserVO属性。
问题:
所以我们要将Picture转成PictureVO时,要将Picture的userId取出来,然后去数据库里面查询得出User,然后还要将User转成UserVO,最后再塞给PictureVO。
二、常规方法
我们很容易想到,就是将Page<Picture>取出List<Picture>,然后直接用stream流,依次拿出userId去数据库查询,最后将User收集起来。
示例代码如下:
public Page<PictureVO> PicturePageToVOPage(Page<Picture> picturePage) {
List<Picture> pictureList = picturePage.getRecords();
Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
if (CollUtil.isEmpty(pictureList)) {
return pictureVOPage;
}
//逐个查询数据库
List<User> userList = pictureList.stream().map(picture -> {
Long userId = picture.getUserId();
return userService.getById(userId);
}).collect(Collectors.toList());
// 将Picture初步转成PictureVO
List<PictureVO> pictureVOList = pictureList.stream()
.map(PictureVO::objToVo)
.collect(Collectors.toList());
//填充PictureVO里的UserVO属性
pictureVOList.forEach(pictureVO -> {
//找到与userId匹配的用户
Long userId = pictureVO.getUserId();
User user = userList.stream()
.filter(u -> u.getId().equals(userId))
.findFirst()
.orElse(null);
//将用户信息转为VO
if (user != null) {
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
pictureVO.setUserVO(userVO);
} else {
// 如果没有找到用户,抛出异常
throw new BusinessException(ResultEnum.SYSTEM_ERROR, "picture转VO错误");
}
});
//最后将完整信息放回到page对象中
pictureVOPage.setRecords(pictureVOList);
return pictureVOPage;
}
三、代码优化
问题:
(1) 从上面的代码可以看到,每次都遍历出userId之后就去查询数据库了。如果数据量很大的时候,就会造成数据的压力。
(2)并且在填充PictureVO里的UserVO属性时,先循环遍历了PictureVOList,然后又在循环里面继续使用stream流遍历了userList。相当于嵌套了两层循环。
那么优化的角度就是:
(1)如何减少访问数据库的次数?
(2)如何避免循环嵌套?
先看代码:
/**
* 获取分页图片封装(分页实体转分页VO)
*/
@Override
public Page<PictureVO> PicturePageToVOPage(Page<Picture> picturePage) {
List<Picture> pictureList = picturePage.getRecords();
//给返回值的Page对象赋值
Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
if (CollUtil.isEmpty(pictureList)) {
return pictureVOPage;
}
// 对象列表 => 封装对象列表
List<PictureVO> pictureVOList = pictureList.stream()
.map(PictureVO::objToVo) //picture转VO
.collect(Collectors.toList()); //收集成List
// 1. 取出pictureVOList中所有的用户id,并去重
Set<Long> userIdSet = pictureList.stream()
.map(Picture::getUserId)
.collect(Collectors.toSet());
/**
* User::getId 是一个方法引用,表示根据 User 对象的 getId() 方法返回的 ID 对用户进行分组。
* groupingBy() 会生成一个 Map,其中每个键是用户的 ID,值是该 ID 对应的 User 对象的列表。
* 例如,如果用户 ID 为 1 的用户有多个,返回的 Map<Long, List<User>> 中,
* 键为 1 的值将是一个包含所有 ID 为 1 的用户对象的列表。
*/
Map<Long, List<User>> userIdUserListMap = userService
.listByIds(userIdSet)//一次性查出userId集合中所有的用户User对象
.stream()
.collect(Collectors.groupingBy(User::getId));//根据User的id属性分组
// 2. 循环遍历pictureVOList,给里面的UserVO属性赋值
pictureVOList.forEach(pictureVO -> {
Long userId = pictureVO.getUserId();
User user = null;
//如果有这个userId对应的对象就赋值,一般都是有的,因为上面查的时候就是根据这些userId来查询的,除非上面转成set集合时没有放上去
if (userIdUserListMap.containsKey(userId)) {
user = userIdUserListMap.get(userId).get(0);//这里因为id在User里是唯一的,所以map里的第二个泛型是List里面其实也是只有一个数据
} else
throw new BusinessException(ErrorEnum.SYSTEM_ERROR, "picture转VO错误");
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);//User转UserVO
pictureVO.setUserVO(userVO);
});
pictureVOPage.setRecords(pictureVOList);//给分页对象赋值。
return pictureVOPage;
}
解释:
(1)上面的代码中,先将Picture对象里的userId统一收集起来,而且是收集到set集合里面,这样就天然地去重了。然后再统一地,一次性地携带集合去请求数据库,得到的也是一个user集合。这样就解决了大量请求数据库的情况。
(2)user集合再使用stream流进行分组,相当于一个map,key的话就使用userId,value就是user对象本身。这样到下面需要匹配userId对应的user时,就直接从map里面取就可以了,所以这就避免了再嵌套一层循环了。
总结:
经过两个方面的优化,这个接口的性能大大提高。这只是一个示例,在平时编写代码时可能会遇到更复杂的情况,但思想还是一样的。