使用注解+反射实现通用List转树形结构

应用场景

在Web开发中,很多情况下我们需要将数据库的List转成一个树形结构的JSON返回给前端,比如平常用到的菜单列表、商品列表、权限列表,一般有一个主键ID和父节点ParentId来维持数据的父子节点关系,然后通过递归实现。

在实际开发中我们会遇到如下情况:

  1. 父子成员变量名称很有可能不一样
  2. 父子成员变量的类型可能是Long、String、Integer类型
  3. List里存放的对象不一样,递归的方法不能共享
  4. 数据量庞大的情况下递归时间过长

所以我们需要一个通用接口来实现树形转换问题。首先考虑到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的测试结果:
map结果

改进

  1. 通过反射获取值可以封装成一个方法
  2. 可以定义两个注解,以免开发者把id和parentId填错
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值