树结构表示了层级关系,在一些目录或是管理层级中非常常用。本身树结构很简单,构建树的逻辑也不复杂,但是要优雅地使用还是需要一点点设计的。
树结构基本属性
首先我们建立树最基本的就是三个值:guid、parentGuid和children
guid用于定位节点,parentGuid定义节点关系,children存放层级数据。
定义接口
我们要生成树结构,那么每个节点就必然会涉及到上述的三个基本属性,所以我们规范出一个接口:
public interface Treeable<T> {
String getGuid();
String getParentGuid();
List<T> getChildren();
void setChildren(List<T> children);
}
其中的泛型T表示了节点类型,这样所有需要使用树结构的类只要去实现树接口,它就拥有了树节点的属性。为什么要规范结构而不是抽象类呢?我们后面再说。
树生成方法
有了树节点,我们就可以开始生成树结构了。
树的生成方法有很多,我这里介绍一个我常用的:
static <T extends Treeable<T>> List<T> buildTree(List<T> list) {
Map<String, T> map = new HashMap<>();
// 构造节点表
for (T t : list) {
map.put(t.getGuid(), t);
}
// 节点挂载
List<T> root = new ArrayList<>();
for (T t : list) {
String parentGuid = t.getParentGuid();
T parent = map.get(parentGuid);
if (parent == null) {
root.add(t);
} else {
List<T> children = parent.getChildren();
if (children == null) {
children = new ArrayList<>();
parent.setChildren(children);
}
children.add(t);
}
}
return root;
}
这个方法非常好理解,就是构建一个key为guid的表,方便我们拿节点。然后将有parentGuid的节点挂载到对应的节点children属性中,没有parentGuid的就是顶级节点。
我们再将所有的顶级节点收集到一个list中,这个list就是顶级目录列表了。
为了方便,我们可以把方法写在Treeable接口中,就像这样:
public interface Treeable<T> {
String getGuid();
String getParentGuid();
List<T> getChildren();
void setChildren(List<T> children);
/**
* 构建树(没有parentGuid的节点默认为父节点)
*
* @param list 原始列表
* @return 树结构列表
*/
static <T extends Treeable<T>> List<T> buildTree(List<T> list) {
Map<String, T> map = new HashMap<>();
// 构造表
for (T t : list) {
map.put(t.getGuid(), t);
}
List<T> root = new ArrayList<>();
for (T t : list) {
String parentGuid = t.getParentGuid();
T parent = map.get(parentGuid);
if (parent == null) {
root.add(t);
} else {
List<T> children = parent.getChildren();
if (children == null) {
children = new ArrayList<>();
parent.setChildren(children);
}
children.add(t);
}
}
return root;
}
}
使用
使用的话就非常简单了,第一步,定义树节点。假设我们有一个目录类Directory,我们就可以这样让它变成树节点:
public class DirectoryNode extends Directory implements Treeable<DirectoryNode>{
private List<DirectoryNode> children;
@Override
public List<DirectoryNode> getChildren() {
return children;
}
@Override
public void setChildren(List<DirectoryNode> children) {
this.children = children;
}
}
这里没有实现getGuid和getParentGuid的原因是Directory类中就已经存在这两个方法了。
第二步,生成树结构:
List<DirectoryNode> DirectoryNodes = getAllDirectoryNodes();
return Treeable.buildTree(DirectoryNodes);
优点
为什么不直接抽象出一个抽象方法,将节点的三个属性直接封装呢?因为抽象方法只能被继承,而Java只支持单继承模式,所以如果我们使用继承的方式去描述节点,那么这个节点就无法再继承其他的父类,损失了拓展性。
这样的方式可以让节点属性随着原型类的变化而动态变化,不必维护两个属性列表。