java - 组装树形结构

基本原理:将业务上的对象,转换成 TreeNode 对象,然后通过 TreeBuilder 对象,将数据组装成树结构。

顾虑

不少程序员,会排斥这种设计,我们的 TreeNode 能兼容所有设计嘛?

显然不能,包括我自己,曾经也是反对这种设计的。


但是,如果不形成标准,很多更好的idea,都无法实施。

比如:前端程序员,想设计一个配套的树形组件,如果你连关键字名称用 key 还是 id 都没确定,那事情都无法继续了。

树节点

import cn.seaboot.commons.core.CommonUtils;

import java.io.Serializable;
import java.util.List;

/**
 * 标准树节点
 * <p>
 * 命名方式较为大众化,能直接对接各类组件
 * <p>
 * 注意:不规范的树形结构,子节点指向父节点,打印的时候会形成无限递归
 *
 * @author Mr.css
 * @version 2024-04-26 10:50
 */
public class TreeNode<T> implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = -8373046449086958894L;
    /**
     * ID 关键字
     */
    private String id;
    /**
     * 父级 ID 关键字
     */
    private String pid;
    /**
     * 标签
     */
    private String label;
    /**
     * 图标
     */
    private String icon;
    /**
     * 是否选中
     */
    private Boolean checked;
    /**
     * 数据实体
     */
    private T data;
    /**
     * 父节点
     */
    private TreeNode<T> parent;
    /**
     * 子节点
     */
    private List<TreeNode<T>> children;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public TreeNode<T> getParent() {
        return parent;
    }

    public void setParent(TreeNode<T> parent) {
        this.parent = parent;
    }

    public List<TreeNode<T>> getChildren() {
        return children;
    }

    public void setChildren(List<TreeNode<T>> children) {
        this.children = children;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public Boolean getChecked() {
        return checked;
    }

    public void setChecked(Boolean checked) {
        this.checked = checked;
    }

    /**
     * 是否有父级
     *
     * @return if true
     */
    public boolean hasParent() {
        return this.parent != null;
    }

    /**
     * 是否有子节点
     *
     * @return if true
     */
    public boolean hasChildren() {
        return CommonUtils.isNotEmpty(children);
    }

    /**
     * 注意:如果子节点指向父级,会形成无限递归,此函数会造成堆栈溢出
     *
     * @return -
     */
    @Override
    public String toString() {
        return "TreeNode{" +
                "id='" + id + '\'' +
                ", pid='" + pid + '\'' +
                ", label='" + label + '\'' +
                ", icon='" + icon + '\'' +
                ", checked='" + checked + '\'' +
                ", data=" + data +
                ", parent=" + parent +
                ", children=" + children +
                '}';
    }
}

配套树形结构拼装工具

package cn.seaboot.commons.lang;

import cn.seaboot.commons.core.CommonUtils;
import cn.seaboot.commons.exception.BizException;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * E.G.:
 * List<Map<String,Object>> tree = new TreeBuilder()
 * .setParent(list)
 * .setChild(list)
 * .groupBy()
 * .getByMinPid();
 * <p>
 * 基于 Java8 封装的树形结构组装工具。
 * <p>
 * 其中,如果父节点如果为 null,通常属于根节点,算法上也是这么判断的。
 *
 * @author Mr.css
 * @version 2020-07-06 11:18
 */
public class TreeBuilder<T> {

    /**
     * 父级
     */
    private List<TreeNode<T>> parent;

    /**
     * 子级
     */
    private List<TreeNode<T>> child;


    /**
     * 数据映射
     */
    private Function<T, TreeNode<T>> mapper;

    /**
     * 数据映射函数,将数据转换成 TreeNode
     *
     * @param mapper 数据映射函数
     * @return this
     */
    public TreeBuilder<T> setMapper(Function<T, TreeNode<T>> mapper) {
        this.mapper = mapper;
        return this;
    }

    /**
     * 父节点列表
     *
     * @param p parent-list
     * @return this
     */
    public TreeBuilder<T> setParent(List<TreeNode<T>> p) {
        this.parent = p;
        return this;
    }

    /**
     * 子节点列表
     *
     * @param child child-list
     * @return this
     */
    public TreeBuilder<T> setChild(List<TreeNode<T>> child) {
        this.child = child;
        return this;
    }

    /**
     * 映射父节点列表
     *
     * @param p parent-list
     * @return this
     */
    public TreeBuilder<T> mapParent(List<T> p) {
        this.parent = p.stream().map(mapper).collect(Collectors.toList());
        return this;
    }

    /**
     * 映射成子节点列表
     *
     * @param child child-list
     * @return this
     */
    public TreeBuilder<T> mapChild(List<T> child) {
        this.child = child.stream().map(mapper).collect(Collectors.toList());
        return this;
    }

    public List<TreeNode<T>> getParent() {
        return parent;
    }

    public List<TreeNode<T>> getChild() {
        return child;
    }

    public Function<T, TreeNode<T>> getMapper() {
        return mapper;
    }

    /**
     * 数据分组
     *
     * @return TreeBuilder
     */
    public TreeBuilder<T> groupBy() {
        if (CommonUtils.isEmpty(parent) && CommonUtils.isEmpty(child)) {
            throw new IllegalArgumentException("Child list and parent list are both empty!");
        }
        if (CommonUtils.isEmpty(parent)) {
            this.parent = this.child;
        }
        if (CommonUtils.isEmpty(child)) {
            this.child = this.parent;
        }

        for (TreeNode<T> p : parent) {
            for (TreeNode<T> c : child) {
                // 此处可能发生空指针,注意初始化集合
                if (p.getId().equals(c.getPid())) {
                    // 子节点指向父节点
                    c.setParent(p);

                    // 父节点指向子节点
                    List<TreeNode<T>> list = p.getChildren();
                    if (CommonUtils.isEmpty(list)) {
                        list = new ArrayList<>();
                        p.setChildren(list);
                    }
                    list.add(c);
                }
            }
        }
        return this;
    }

    /**
     * 如果已知要取的节点,直接按照节点pId进行获取
     *
     * @param pid 值可能为空,数据库可能用null值表示顶级节点
     * @return tree
     */
    public List<TreeNode<T>> getByPid(@Nullable Object pid) {
        List<TreeNode<T>> res = new ArrayList<>();
        if (pid == null) {
            for (TreeNode<T> p : parent) {
                if (p.getPid() == null) {
                    res.add(p);
                }
            }
        } else {
            for (TreeNode<T> p : parent) {
                if (pid.equals(p.getPid())) {
                    res.add(p);
                }
            }
        }
        return res;
    }

    /**
     * 从结果集中取出 pid 最小的数据
     * <p>
     * 特例:如果存在 pid 为 null 的数据,则将该数据,作为最小的数据
     *
     * @return tree
     */
    public List<TreeNode<T>> getByMinPid() {
        String min = parent.get(0).getPid();
        String pid;
        for (TreeNode<T> p : parent) {
            pid = p.getPid();
            if (pid == null) {
                return this.getByPid(null);
            } else {
                if (pid.compareTo(min) < 0) {
                    min = pid;
                }
            }
        }
        return this.getByPid(min);
    }

    /**
     * 从组装的结果中取出Pid最大的数据
     * <p>
     * 特例:如果存在 pid 为 null 的数据,则将该数据,作为最大的数据
     *
     * @return tree
     */
    public List<TreeNode<T>> getByMaxPid() {
        String max = parent.get(0).getPid();
        String pid;
        for (TreeNode<T> p : parent) {
            pid = p.getPid();
            if (pid == null) {
                return this.getByPid(null);
            } else {
                if (pid.compareTo(max) > 0) {
                    max = pid;
                }
            }
        }
        return this.getByPid(max);
    }

    /**
     * 随机取出一个元素,反复遍历树,直到取出最顶级的父节点。
     * <p>
     * 树状结构越深,需要遍历的次数越多。
     *
     * @return tree
     */
    public List<TreeNode<T>> getRoot() {
        // 以第一个节点的 pid 作为顶级节点,不断递归检索
        TreeNode<T> top = parent.get(0);

        // 声明一个 list 记录已经查找过的节点,避免出现无限递归
        List<String> list = new ArrayList<>();
        list.add(top.getPid());

        while (top.hasParent()) {
            top = top.getParent();
            if (list.contains(top.getPid())) {
                // 出现无限递归的数据,异常退出
                throw new BizException("Circular dependencies:" + top);
            } else {
                // 继续下一次递归检索
                list.add(top.getPid());
            }
        }
        return this.getByPid(top.getPid());
    }
}

posted on 2019-07-11 16:28  疯狂的妞妞  阅读(4160)  评论(0编辑  收藏  举报

导航