应用场景
在Web开发中,很多情况下我们需要将数据库的List转成一个树形结构的JSON返回给前端,比如平常用到的菜单列表、商品列表、权限列表,一般有一个主键ID和父节点ParentId来维持数据的父子节点关系,然后通过递归实现。
在实际开发中我们会遇到如下情况:
- 父子成员变量名称很有可能不一样
- 父子成员变量的类型可能是Long、String、Integer类型
- List里存放的对象不一样,递归的方法不能共享
- 数据量庞大的情况下递归时间过长
所以我们需要一个通用接口来实现树形转换问题。首先考虑到List里存放的对象不确定,所以我们需要用到泛型。由于变量类型、名称不确定,所以考虑到注解和反射实现。
注解
定义Tree注解类,@Target申明注解可使用的位置,FIELD指成员变量。@Retention申明注解生命周期在运行时也生效。
@Target({ElementType.FIELD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Tree {
/**
* id : 主键
* parentId : 父节点
*/
String value();
}
返回实体
node为当前节点对象,children为子节点集合
public class TreeEntity<T> {
T node;
List<TreeEntity<T>> children;
public T getNode() {
return node;
}
public void setNode(T node) {
this.node = node;
}
public List<TreeEntity<T>> getChildren() {
return children;
}
public void setChildren(List<TreeEntity<T>> children) {
this.children = children;
}
}
工具类
public class TreeUtil {
/**
* 通过递归实现,时间复杂度为N*N
* 10000条数据,随机四层树,测试用时约25700毫秒,数据正确的情况下无节点丢失
*/
public static<T> List<TreeEntity<T>> listToTree(List<T> list) throws IllegalAccessException {
if(list == null || list.size() == 0){
return null;
}
return backListToTree(list,"0");
}
/**
* 通过map实现,时间复杂度为3N
* 10000条数据,随机四层树,测试用时约90毫秒,数据正确的情况下无节点丢失
*/
public static<T> List<TreeEntity<T>> listToTreeByMap(List<T> list) throws IllegalAccessException {
//定义一个Map集合,将list放入map中,key为T的id节点,vlaue为TreeEntity<T>实体,该实体的node为T,子节点为null。
HashMap<Object,TreeEntity<T>> all_map = new HashMap<Object,TreeEntity<T>>(list.size());
for(T t : list){
//通过反射获取t的所有变量
Field[] fields = t.getClass().getDeclaredFields();
Field id_field = null;
for(Field field : fields){
if(field.getAnnotation(Tree.class)==null){
continue;
}else if("id".equals(field.getAnnotation(Tree.class).value())){
//找到有@Tree注解且注解的value值为id的变量
id_field = field;
}
}
if(id_field == null){
throw new RuntimeException("无法获取到id属性");
}
//开通变量权限
id_field.setAccessible(true);
//获取该变量的值,并统一处理为String
Object obj_id = id_field.get(t);
String id = obj_id == null? "0" : obj_id+"";
//组装成TreeEntity<T>实体放入map中
TreeEntity<T> node = new TreeEntity<T>();
node.setNode(t);
all_map.put(obj_id,node);
}
//遍历所有map,将所有有父子一来的实体连接起来
for(Map.Entry<Object,TreeEntity<T>> e : all_map.entrySet()){
//通过反射获取父子节点
T t = e.getValue().getNode();
Field[] fields = t.getClass().getDeclaredFields();
Field parent_field = null ;
Field id_field = null;
for(Field field : fields){
if(field.getAnnotation(Tree.class)==null){
continue;
}else if("parentId".equals(field.getAnnotation(Tree.class).value())){
parent_field = field;
}else if("id".equals(field.getAnnotation(Tree.class).value())){
id_field = field;
}
}
if(parent_field == null || id_field == null){
throw new RuntimeException("无法获取到父子属性");
}
parent_field.setAccessible(true);
id_field.setAccessible(true);
Object obj_id = id_field.get(t);
Object obj_parentId = parent_field.get(t);
String parent_id = obj_parentId == null? "0" : obj_parentId+"";
if("0".equals(parent_id)){
continue;
}
//如果该节点不是根节点,就找到他的父节点
TreeEntity<T> tree = all_map.get(obj_parentId);
if(tree == null){
throw new RuntimeException("父节点"+obj_parentId+"不存在");
}
if(tree.getChildren() == null){
tree.setChildren(new ArrayList<TreeEntity<T>>());
}
//并把该节点的内存地址负值给父节点的 List<TreeEntity<T>> children中。
tree.getChildren().add(all_map.get(obj_id));
}
//创建返回对象
List<TreeEntity<T>> back = new ArrayList<TreeEntity<T>>();
//再次遍历,取出所有根节点(由于上一次的循环中所有节点都已经通过指针连接完成,这样只需要取出根节点即可。)
for(Map.Entry<Object,TreeEntity<T>> e : all_map.entrySet()){
T t = e.getValue().getNode();
Field[] fields = t.getClass().getDeclaredFields();
Field parent_field = null ;
for(Field field : fields){
if(field.getAnnotation(Tree.class)==null){
continue;
}else if("parentId".equals(field.getAnnotation(Tree.class).value())){
parent_field = field;
}
}
if(parent_field == null){
throw new RuntimeException("无法获取到parentId属性");
}
parent_field.setAccessible(true);
Object obj_parentId = parent_field.get(t);
String parent_id = obj_parentId == null? "0" : obj_parentId+"";
if("0".equals(parent_id)){
back.add(e.getValue());
}
}
return back;
}
/**
* 获取节点数量
* @param list
* @return
* 该方式通过递归获取节点总是,用于下面的测试,防止节点丢失。这里也提供给开发中需要的地方。比如在插入的时候想知道树的节点数量,然后获取数据库序列号,给父子节点负值后批量插入。
*/
public static int getSize(List<TreeEntity<TestPO>> list){
int i = 0;
if(list == null || list.size() == 0){
return i;
}
for(TreeEntity<TestPO> tree : list){
i++;
if(tree.getChildren() != null && tree.getChildren().size()>0){
i =i + getSize(tree.getChildren());
}
}
return i;
}
private static<T> List<TreeEntity<T>> backListToTree(List<T> list, String _id) throws IllegalAccessException {
ArrayList<TreeEntity<T>> tree = new ArrayList<TreeEntity<T>>();
for(T t : list) {
//获取父节点
Field[] fields = t.getClass().getDeclaredFields();
Field parent_field = null ;
Field id_field = null;
for(Field field : fields){
if(field.getAnnotation(Tree.class)==null){
continue;
}else if("parentId".equals(field.getAnnotation(Tree.class).value())){
parent_field = field;
}else if("id".equals(field.getAnnotation(Tree.class).value())){
id_field = field;
}
}
if(parent_field == null || id_field == null){
throw new RuntimeException("无法获取到父子属性");
}
parent_field.setAccessible(true);
id_field.setAccessible(true);
String parent_id = "";
Object obj_parentId = parent_field.get(t);
parent_id = obj_parentId == null? "0" : obj_parentId+"";
if (parent_id.equals(_id)) {
//组装子节点
TreeEntity c_tree = new TreeEntity();
c_tree.setNode(t);
//获取ID
String id = "";
Object obj_id = id_field.get(t);
id = obj_id == null? "0" : obj_id+"";
//递归
List<TreeEntity<T>> back = backListToTree(list,id);
c_tree.setChildren(back);
tree.add(c_tree);
}
}
return tree;
}
}
实体类
只需要在两个父子变量上加注解即可。
public class TestPO {
@Tree("id")
private String code;
@Tree("parentId")
private String parentCode;
private String name;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getParentCode() {
return parentCode;
}
public void setParentCode(String parentCode) {
this.parentCode = parentCode;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试
public class TimeTest {
public static void main(String[] args) {
//定义10000个数
List<TestPO> list = new ArrayList<TestPO>();
for(int i = 0;i<10000;i++){
TestPO testPO = new TestPO();
testPO.setCode(i+1+"");
testPO.setName("测试"+i);
if(i<100) {
testPO.setParentCode("0");
}else if(i<200){
testPO.setParentCode(i-99+"");
}else if(i<300){
testPO.setParentCode(i-199+"");
}else if(i<400){
testPO.setParentCode(i-299+"");
}else if(i<500){
testPO.setParentCode(i-200+"");
}
else if(i<600){
testPO.setParentCode(i-300+"");
}
else if(i<700){
testPO.setParentCode(i-200+"");
}else if(i<10000){
testPO.setParentCode((int)(Math.random()*600)+5+"");
}
list.add(testPO);
}
//打乱顺序
ArrayList<TestPO> arr = new ArrayList<TestPO>();
while (list.size() > 0){
int index = (int)(Math.random()*list.size());
arr.add(list.get(index));
list.remove(index);
}
try {
long start = System.currentTimeMillis();
//List<TreeEntity<TestPO>> tree = TreeUtil.listToTreeByMap(arr);
List<TreeEntity<TestPO>> tree = TreeUtil.listToTree(arr);
long end = System.currentTimeMillis();
int size = TreeUtil.getSize(tree);
System.out.println("测试用时:"+(end-start)+",节点数量为"+size+"/"+arr.size());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
使用递归的测试结构:
使用map的测试结果:
改进
- 通过反射获取值可以封装成一个方法
- 可以定义两个注解,以免开发者把id和parentId填错