一直以来对于泛型的理解都不是很深刻,今天在解决一个问题时比较好地运用了一下泛型,记录一下用法和解决问题的思路
0x01 问题导入
在SpringMVC中经常会有查询菜单树的需求,即需要查询下图所示的菜单
假设我们定义菜单实体类如下:
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_menu")
@Accessors(chain = true)
public class Menu{
//菜单ID
@TableId
private Long id;
//菜单名称
private String menuName;
//父菜单ID
private Long parentId;
//显示顺序
private Integer orderNum;
// 省略了一些不重要的属性,实体类根据数据库生成就行
@TableField(exist = false)
private List<Menu> children;
}
那么我们查询菜单树的接口怎么实现呢?
获取数据库中所有菜单可以直接调用MybatisPlus提供的CRUD接口,这里就不赘述了。
主要实现一下构建菜单树的逻辑:
// menus:所有的菜单项;parentId:菜单树第一层的父节点id,可以看作根节点id
private List<Menu> buildMenuTree(List<Menu> menus, Long parentId) {
return menus.stream()
.filter(menu -> menu.getParentId().equals(parentId)) // 找出第一层菜单
.map(menu -> menu.setChildren(getChildren(menu, menus))) // getChildren中会递归构建菜单树
.collect(Collectors.toList()); // 返回构建好的菜单树
}
// 构建menu的子菜单
private List<Menu> getChildren(Menu menu, List<Menu> menus) {
return menus.stream()
.filter(m -> m.getParentId().equals(menu.getId())) // 在menus找menu的子菜单
.map(m->m.setChildren(getChildren(m, menus))) // 递归调用查找下一层的子菜单
.collect(Collectors.toList()); // 最后返回的就是以menus为根节点的菜单树
}
// 使用方法
// menus是查询出来的所有菜单,0L是我设置的第一层节点的父节点ID
List<Menu> menuTree = buildMenuTree(menus, 0L);
现在我们已经实现了查询菜单树的功能,但现在这两个方法只能针对于Menu类构建树,那么如果有一天我们如果要查询用户树、文章树、标签树等等,岂不是又要重新写一遍这个逻辑代码?
这样显然不是很优雅,正确的打开方式是定义一个通用的方法,不管我们要查的是什么树,通通只要调用这个方法就ok
那么这个通用方法该如何实现呢?这里就需要依靠泛型啦
0x02 使用泛型写出通用方法
首先我们要知道,我们需要实现的方法中还调用了getId()、getParentId()、setChildren()这三个方法,如果直接写一个泛型方法是不行的,这里的解决方法是我们定义一个Tree接口,在接口中定义这三个方法,然后写泛型方法时限定泛型的类型为Tree接口的实现类,这样就可以啦
Tree接口:
public interface Tree<T> {
Long getId();
Long getParentId();
// 注意这里也要用泛型,因为children的类型应该是实现类的类型,实现类是不确定的
// 我这里方法的返回值为T是因为定义了链式方法@Accessors(chain = true)
T setChildren(List<T> children);
}
然后我们写一下TreeUtils类,在类中定义生成树的方法,如下所示:
public class TreeUtils{
// 注意这里的泛型类型参数必须与接口的实现类型保持一致,这样才符合泛型的约束规则
public static <T extends Tree<T>> List<T> getChildren(T menu, List<T> menus){
return menus.stream()
.filter(m -> m.getParentId().equals(menu.getId()))
.map(m -> {
m.setChildren(getChildren(m, menus));
return m;
})
.collect(Collectors.toList());
}
public static <T extends MenuTree<T>> List<T> buildTree(List<T> menus, Long parentId){
return menus.stream()
.filter(menu -> menu.getParentId().equals(parentId))
.map(menu->{
menu.setChildren(getChildren(menu, menus));
return menu;
})
.collect(Collectors.toList());
}
}
然后我们让Menu类实现Tree接口
@SuppressWarnings("serial")
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_menu")
@Accessors(chain = true)
public class Menu implements MenuTree<Menu>{
//菜单ID
@TableId
private Long id;
//菜单名称
private String menuName;
//父菜单ID
private Long parentId;
//显示顺序
private Integer orderNum;
// 省略了一些不重要的属性,实体类根据数据库生成就行
@TableField(exist = false)
private List<Menu> children;
}
这样就可以使用TreeUtils.buildTree()方法代替原来的方法啦,同时只要实现了Tree接口的类都可以这样生成树类型,经验证是可以使用的
0x03 总结
以上就是一个泛型实现通用方法的例子,从这个例子中我们可以总结出以下经验:
- 代码中有些重复的业务逻辑可以写一个工具类统一实现
- 工具类中一般都需要使用泛型,如果可以直接使用普通的泛型类的话就很简单,但如果有些时候我们的泛型方法需要调用一些特定的方法,这时候我们可以写一个接口声明这些特定的方法,然后使用带约束的泛型来实现泛型方法,就如同我们上面的代码所示。当一个类想要调用我们所写的通用方法时,只需要实现接口即可。