树结构是一种较为常见的数据结构,如功能权限树、企业的组织结构图、行政区划结构图、家族谱、信令消息树等,都表现为树型数据结构。
树结构数据的共性是树节点之间都有相互关系,对于一个节点对象,可以找到父节点、左邻节点、右邻节点、子节点列表。节点本身有其数据类型对象,不同类型的树,不同之处在于节点数据类型不同。
下面针对树型数据,用Java语言给出一种通用树结构数据定义,并提供常规的树节点操作方法。
树节点类TreeNode,其定义如下:
package com.abc.questInvest.vo; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import lombok.Data; /** * @className : TreeNode * @description : 树节点 * @summary : 节点数据类型,必须实现ITreeNodeData接口类的接口 * */ @Data public class TreeNode<T extends ITreeNodeData> implements Serializable { private static final long serialVersionUID = 1L; //节点数据对象 private T nodeData; //父节点对象 private TreeNode<T> parent; //子节点列表 private List<TreeNode<T>> children = new ArrayList<TreeNode<T>>(); //是否包含在树中,1表示包含,0表示不包含,此属性为附加属性,在完整树剪枝时使用 private Integer isIncluded = 0; }
树节点类TreeNode使用泛型T,来表示节点数据类型,规定T必需实现ITreeNodeData接口类,使用接口类而不是基类,是为了不限定节点数据的字段集,且没有多重继承的问题。另外TreeNode也需要实现Serializable接口类,提供节点数据的序列化方法。
TreeNode类提供下列属性字段:
TreeNode类,提供了父节点对象和子节点列表属性字段,从而具有向上搜索和向下搜索的能力。
TreeNode类,没有使用左邻节点、右邻节点属性字段,是考虑到兄弟节点的搜索不是很频繁,不用左邻节点、右邻节点属性字段,可以减少节点关系维护的复杂度。同级节点搜索,可以遍历父节点的子节点列表来实现。
树节点数据接口类,为ITreeNodeData,其规定了树节点数据对象类型,必需实现的接口方法。代码如下:
package com.abc.questInvest.vo; /** * @className : ITreeNodeData * @description : 树节点数据接口类 * */ public interface ITreeNodeData extends Cloneable{ //=============节点基本属性访问接口============================== //获取节点ID int getNodeId(); //获取节点名称 String getNodeName(); //获取父节点ID int getParentId(); //=============Cloneable类接口=================================== //克隆 public Object clone(); }
ITreeNodeData类,继承Cloneable,要求树节点数据对象类型必需实现克隆(clone)接口方法。目的是实现对象复制。
ITreeNodeData类定义了下列基本的接口方法:
对树节点类TreeNode,进行完善,提供必要的接口。代码如下:
package com.abc.questInvest.vo; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import lombok.Data; /** * @className : TreeNode * @description : 树节点 * @summary : 节点数据类型,必须实现ITreeNodeData接口类的接口 * */ @Data public class TreeNode<T extends ITreeNodeData> implements Serializable { private static final long serialVersionUID = 1L; //节点数据 private T nodeData; //父节点对象 private TreeNode<T> parent; //子节点 private List<TreeNode<T>> children = new ArrayList<TreeNode<T>>(); //是否包含在树中,1表示包含,0表示不包含,此属性为附加属性,在完整树剪枝时使用 private Integer isIncluded = 0; /** * * @methodName : addChildNode * @description : 添加子节点 * @param childNode : 子节点 * */ public void addChildNode(TreeNode<T> childNode) { childNode.setParent(this); children.add(childNode); } /** * * @methodName : removeChildNode * @description : 移除子节点,如果子节点在子节点列表中,则移除,否则无影响 * @param childNode : 子节点 * */ public void removeChildNode(TreeNode<T> childNode) { children.remove(childNode); } /** * * @methodName : clear * @description : 移除所有子节点 * */ public void clear() { children.clear(); } /** * * @methodName : getPrevSibling * @description : 取得左邻节点 * @return : 如果当前节点为第一个节点,则返回null,否则为前一个节点 * */ public TreeNode<T> getPrevSibling(){ if (parent == null) { //如果为根节点,则返回null return null; } List<TreeNode<T>> siblingList = parent.getChildren(); TreeNode<T> node = null; for (int i = 0; i < siblingList.size(); i++) { TreeNode<T> item = siblingList.get(i); if (item == this) { //找到当前节点 if (i > 0) { //当前节点不是第一个子节点 //取得前一个节点 node = siblingList.get(i-1); } break; } } return node; } /** * * @methodName : getNextSibling * @description : 取得右邻节点 * @return : 如果当前节点为最后一个节点,则返回null,否则为后一个节点 * */ public TreeNode<T> getNextSibling(){ if (parent == null) { //如果为根节点,则返回null return null; } List<TreeNode<T>> siblingList = parent.getChildren(); TreeNode<T> node = null; for (int i = 0; i < siblingList.size(); i++) { TreeNode<T> item = siblingList.get(i); if (item == this) { //找到当前节点 if (i < siblingList.size()-1) { //当前节点不是最后一个子节点 //取得后一个节点 node = siblingList.get(i+1); } break; } } return node; } /** * * @methodName : lookUpSubNode * @description : 在当前节点及下级节点中查找指定节点ID的节点 * @param nodeId : 节点ID * @return : 如果找到,返回对应树节点,否则返回null * */ public TreeNode<T> lookUpSubNode(int nodeId){ TreeNode<T> node = null; //检查当前节点 if (nodeData.getNodeId() == nodeId) { node = this; return node; } //遍历子节点 for(TreeNode<T> item : children) { node = item.lookUpSubNode(nodeId); if (node != null) { //如果节点非空,表示查找到了 break; } } return node; } /** * * @methodName : lookUpSubNode * @description : 在当前节点及下级节点中查找指定节点名称的节点 * @param nodeName : 节点名称 * @return : 如果找到,返回对应树节点,否则返回null * */ public TreeNode<T> lookUpSubNode(String nodeName){ TreeNode<T> node = null; //检查当前节点 if (nodeData.getNodeName().equals(nodeName)) { node = this; return node; } //遍历子节点 for(TreeNode<T> item : children) { node = item.lookUpSubNode(nodeName); if (node != null) { //如果节点非空,表示查找到了 break; } } return node; } /** * * @methodName : lookUpSuperNode * @description : 在当前节点及上级节点中查找指定节点ID的节点 * @param nodeId : 节点ID * @return : 如果找到,返回对应树节点,否则返回null * */ public TreeNode<T> lookUpSuperNode(int nodeId){ TreeNode<T> node = null; //检查当前节点 if (nodeData.getNodeId() == nodeId) { node = this; return node; } //查找父节点 if (parent != null) { node = parent.lookUpSuperNode(nodeId); } return node; } /** * * @methodName : lookUpSuperNode * @description : 在当前节点及上级节点中查找指定节点名称的节点 * @param nodeName : 节点名称 * @return : 如果找到,返回对应树节点,否则返回null * */ public TreeNode<T> lookUpSuperNode(String nodeName){ TreeNode<T> node = null; //检查当前节点 if (nodeData.getNodeName().equals(nodeName)) { node = this; return node; } //查找父节点 if (parent != null) { node = parent.lookUpSuperNode(nodeName); } return node; } /** * * @methodName : clone * @description : 复制树节点,包括所有子节点 * @return : 复制后的树节点 * */ @SuppressWarnings("unchecked") public TreeNode<T> clone(){ //复制当前节点数据信息 TreeNode<T> treeNode = new TreeNode<T>(); //节点数据 treeNode.setNodeData((T)this.nodeData.clone()); //是否包含 treeNode.setIsIncluded(this.isIncluded); //复制所有子节点 for(TreeNode<T> item : this.children) { //复制子节点 TreeNode<T> childNode = item.clone(); //加入子节点列表中 treeNode.addChildNode(childNode); } return treeNode; } /** * * @methodName : setChildrenIsIncluded * @description : 设置所有子节点的是否包含属性 * @param isIncluded : 节点是否包含 * */ public void setChildrenIsIncluded(Integer isIncluded) { //遍历所有子节点 for(TreeNode<T> item : this.children) { item.setIsIncluded(isIncluded); //子节点的子节点 for(TreeNode<T> subItem : item.getChildren()) { subItem.setChildrenIsIncluded(isIncluded); } } } /** * * @methodName : combineTreeNode * @description : 将结构完全相同的节点合并到本节点中,合并后的节点的isIncluded属性位|操作 * @param combineNode : 并入的节点 * */ public void combineTreeNode(TreeNode<T> combineNode) { //当前节点数据的isIncluded属性,使用位|操作 this.setIsIncluded(this.getIsIncluded() | combineNode.getIsIncluded()); //合并子节点 for (int i = 0; i < children.size(); i++) { TreeNode<T> item = children.get(i); TreeNode<T> combineItem = combineNode.getChildren().get(i); //合并子节点 item.combineTreeNode(combineItem); } } /** * * @methodName : arrange * @description : 对树进行剪枝处理,即所有isIncluded为0的节点,都被移除 * */ public void arrange() { //遍历子节点列表,如果子节点的isIncluded为0,则剪枝 //倒序遍历 for (int i = children.size() -1; i >=0; i--) { TreeNode<T> item = children.get(i); if (item.getIsIncluded() == 0) { //不包含,需要移除 children.remove(i); }else { //包含,当前节点不需要移除,处理其子节点列表 item.arrange(); } } } /** * * @methodName : getNodeList * @description : 获取包括自身及所有子节点的列表 * @param nodeList : 树节点列表,入口参数为null * @return : 树节点列表 * */ public List<TreeNode<T>> getNodeList(List<TreeNode<T>> nodeList){ if (nodeList == null) { //如果入口节点,则参数为null,需要创建 nodeList = new ArrayList<TreeNode<T>>(); } //加入自身节点 nodeList.add(this); //加入所有子节点 for(int i = 0; i < children.size(); i++) { TreeNode<T> childNode = children.get(i); childNode.getNodeList(nodeList); } return nodeList; } /** * * @methodName : loadData * @description : 将T类型对象的列表加载到树中,调用之前应确保节点的数据对象已创建, * 且节点ID设置为0 * @param inputList : T类型对象的列表 * @return : 错误的T类型对象的列表 * */ public List<T> loadData(List<T> inputList){ //错误的数据对象列表 List<T> errorList = new ArrayList<T>(); //建立节点ID与节点对象的映射表,表示节点加载过程当前已加载的节点集合 Map<Integer,TreeNode<T>> nodeMap = new HashMap<Integer,TreeNode<T>>(); //================================================================== //要考虑数据次序不一定保证父节点已先加载的情况 //清除数据 clear(); //先加入根节点 nodeMap.put(this.nodeData.getNodeId(), this); //父节点 TreeNode<T> parentNode = null; //遍历inputList,加载树 for(T item : inputList) { Integer parentId = item.getParentId(); if (nodeMap.containsKey(parentId)) { //如果父节点已加载,取得父节点对象 parentNode = nodeMap.get(parentId); //加载树节点 addTreeNode(parentNode,item,nodeMap); }else { //如果父节点未加载,则暂时作为游离的独立节点或子树 //加载树节点 addTreeNode(null,item,nodeMap); } } //处理游离的节点 for(TreeNode<T> node : nodeMap.values()) { if (node.getParent() == null && node.getNodeData().getNodeId() != 0) { //父节点为空,且非根节点 //取得父节点ID Integer parentId = node.getNodeData().getParentId(); if (nodeMap.containsKey(parentId)) { //如果父节点存在,,取得父节点对象 parentNode = nodeMap.get(parentId); //加入父节点中 parentNode.addChildNode(node); }else { //parentId对应的节点不存在,说明数据配置错误 errorList.add(node.getNodeData()); } } } return errorList; } /** * * @methodName : addTreeNode * @description : 加入树节点 * @param parentNode : 父节点 * @param dataInfo : 节点信息对象 * @param nodeMap : 节点ID与节点对象的映射表 * */ private void addTreeNode(TreeNode<T> parentNode, T dataInfo, Map<Integer,TreeNode<T>> nodeMap) { //生成树节点 TreeNode<T> treeNode = new TreeNode<T>(); //设置节点数据 treeNode.setNodeData((T)dataInfo); if(parentNode != null) { //父节点非空,加入父节点中 parentNode.addChildNode(treeNode); } //加入nodeMap中 nodeMap.put(dataInfo.getNodeId(), treeNode); } /** * * @methodName : toString * @description : 重载toString方法 * @return : 返回序列化输出的字符串 * */ @Override public String toString() { String sRet = ""; //根节点的数据部分不必输出 if (parent != null) { //非根节点 //输出节点开始符号 sRet = "{"; //输出isIncluded值,剪枝后的树,无需输出此字段 //sRet += "\"isIncluded\":" + isIncluded + ","; //输出当前节点数据 sRet += "\"nodeData\":" + nodeData.toString(); //与前一个节点分隔 sRet += ","; sRet += "\"children\":"; } //输出子节点 //子节点列表 sRet += "["; String sChild = ""; //遍历子节点 for(TreeNode<T> item : children) { //输出子节点数据 if (sChild.equals("")) { sChild = item.toString(); }else { sChild += "," + item.toString(); } } sRet += sChild; //结束列表 sRet += "]"; if (parent != null) { //输出节点结束符号 sRet += "}"; } return sRet; } }
TreeNode类提供下列接口方法:
树结构的节点数据,以权限管理的功能权限树为例,实体类为FunctionInfo。代码如下:
package com.abc.questInvest.entity; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Id; import com.abc.questInvest.vo.ITreeNodeData; import lombok.Data; /** * @className : FunctionInfo * @description : 功能节点信息 * */ @Data public class FunctionInfo implements Serializable,ITreeNodeData { private static final long serialVersionUID = 1L; //功能ID @Id @Column(name = "func_id") private Integer funcId; //功能名称 @Column(name = "func_name") private String funcName; //父节点ID @Column(name = "parent_id") private Integer parentId; //功能所在层级 @Column(name = "level") private Byte level; //显示顺序 @Column(name = "order_no") private Integer orderNo; //访问接口url @Column(name = "url") private String url; //dom对象的id @Column(name = "dom_key") private String domKey; // ================ 接口重载 ====================== //获取节点ID @Override public int getNodeId() { return funcId; } //获取节点名称 @Override public String getNodeName() { return funcName; } //获取父节点ID @Override public int getParentId() { return parentId; } //对象克隆 @Override public Object clone(){ FunctionInfo obj = null; try{ obj = (FunctionInfo)super.clone(); }catch(CloneNotSupportedException e){ e.printStackTrace(); } return obj; } @Override public String toString() { return "{" + "\"funcId\":" + funcId + "," + "\"funcName\":\"" + funcName + "\"," + "\"parentId\":" + parentId + "," + "\"level\":" + level + "," + "\"orderNo\":" + orderNo + "," + "\"url\":\"" + url + "\"," + "\"domKey\":\"" + domKey + "\"" + "}"; } }
FunctionInfo类对应数据库的功能树表function_tree,表结构如下:
DROP TABLE IF EXISTS `function_tree`; CREATE TABLE `function_tree` ( `func_id` INT(11) NOT NULL DEFAULT 0 COMMENT '功能ID', `func_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '功能名称', `parent_id` INT(11) NOT NULL DEFAULT 0 COMMENT '父功能ID', `level` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '功能所在层级', `order_no` INT(11) NOT NULL DEFAULT 0 COMMENT '显示顺序', `url` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '访问接口url', `dom_key` VARCHAR(80) NOT NULL DEFAULT '' COMMENT 'dom对象的id', `remark` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注', -- 记录操作信息 `operator_name` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '操作人账号', `delete_flag` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '记录删除标记,1-已删除', `create_time` DATETIME(3) NOT NULL DEFAULT NOW(3) COMMENT '创建时间', `update_time` DATETIME(3) DEFAULT NULL ON UPDATE NOW(3) COMMENT '更新时间', PRIMARY KEY (`func_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='功能表';
Dao类为FunctionTreeDao。代码如下:
package com.abc.questInvest.dao; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import com.abc.questInvest.entity.FunctionInfo; /** * @className : FunctionTreeDao * @description : function_tree表数据访问类 * */ @Mapper public interface FunctionTreeDao { //查询所有功能树表记录,按parent_id,order_no排序 @Select("SELECT func_id,func_name,parent_id,level,order_no,url,dom_key" + " FROM function_tree ORDER BY parent_id,order_no") List<FunctionInfo> selectAll(); }
注意,查询数据需要按parent_id,order_no排序,有助于提高加载速度。
Service类为FunctionTreeService。代码如下:
package com.abc.questInvest.service; import com.abc.questInvest.entity.FunctionInfo; import com.abc.questInvest.vo.TreeNode; /** * @className : FunctionTreeService * @description : 功能树服务 * */ public interface FunctionTreeService { /** * * @methodName : loadData * @description : 加载数据库中数据 * @return : 成功返回true,否则返回false。 * */ public boolean loadData(); /** * * @methodName : getFunctionTree * @description : 获取整个功能树 * @return : 完整的功能树 * */ public TreeNode<FunctionInfo> getFunctionTree(); }
Service实现类为FunctionTreeServiceImpl。代码如下:
package com.abc.questInvest.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.abc.questInvest.dao.FunctionTreeDao; import com.abc.questInvest.entity.FunctionInfo; import com.abc.questInvest.service.FunctionTreeService; import com.abc.questInvest.vo.TreeNode; import lombok.extern.slf4j.Slf4j; /** * @className : FunctionTreeServiceImpl * @description : FunctionTreeService实现类 * */ @Slf4j @Service public class FunctionTreeServiceImpl implements FunctionTreeService { //function_tree表数据访问对象 @Autowired private FunctionTreeDao functionTreeDao; //功能树对象 private TreeNode<FunctionInfo> functionTree = new TreeNode<FunctionInfo>(); /** * * @methodName : loadData * @description : 加载数据库中数据 * @return : 成功返回true,否则返回false。 * */ @Override public boolean loadData() { try { //查询function_tree表,获取全部数据 List<FunctionInfo> functionInfoList = functionTreeDao.selectAll(); //加锁保护,防止脏读 synchronized(functionTree) { //设置根节点 setRootNode(functionTree); List<FunctionInfo> errorList = functionTree.loadData(functionInfoList); if (errorList.size() > 0) { //有错误信息 //写日志 for(FunctionInfo item : errorList) { log.error("FunctionTree error with item : " + item.toString()); } //此时,functionTree是剔除了异常数据的功能树 //返回true或false,视业务需求而定 return false; } } }catch(Exception e) { log.error(e.getMessage()); e.printStackTrace(); return false; } return true; } /** * * @methodName : getFunctionTree * @description : 获取整个功能树 * @return : 完整的功能树 * */ @Override public TreeNode<FunctionInfo> getFunctionTree(){ return functionTree; } /** * * @methodName : setRootNode * @description : 设置根节点 * @param node : 输入的功能树根节点 * */ private void setRootNode(TreeNode<FunctionInfo> node) { node.setParent(null); //创建空节点数据 node.setNodeData(new FunctionInfo()); //约定根节点的节点ID为0 node.getNodeData().setFuncId(0); node.getNodeData().setFuncName("root"); //根节点总是包含的 node.setIsIncluded(1); } }
### 6.4、单元测试
对FunctionTreeService使用单元测试,观察效果。代码如下:
/** * @className : QuestInvestApplicationTest * @description : 启动测试类 * */ @RunWith(SpringRunner.class) @SpringBootTest public class QuestInvestApplicationTest { @Autowired ServletContext servletContext; @Autowired FunctionTreeService functionTreeService; @Test public void functionTreeServiceTest() { boolean bRet = false; bRet = functionTreeService.loadData(); if (bRet) { TreeNode<FunctionInfo> functionTree = functionTreeService.getFunctionTree(); System.out.println(functionTree); } } }
执行测试代码,可以看到输出的功能树数据,将之用网上的JSON查看器查看,可以看到下图的树型结构: