玄星幻月001-树结构复制
前言:在实际开发中,可能会遇到一些通用的代码块,临时想比较费时耗力,如果做好记录就能很方便的拿来即用,省时省力。该系列将会记录本人项目或其他地方遇到比较好的例子来展开记录。玄星幻月:高中就开始喜欢这个词了,就以此作为系列名称!先定个小目标,数量突破三位数。(^_^)
背景:
树结构是在项目中比较常见的一种结构,向单项列表一样,每个节点属性都会存放着下一节点的id。有时候需要将一棵树复制并另存为另外的树,但是由于表主键的唯一性,如何保证复制后在每个节点主键变化的提前下又保证树的结构呢?
现在通过主键映射的方式来复制树结构,这样的好处是只需要按照原来的树结构映射替换它的主键,就可以保证树结构不变的又实现树的复制功能。
案例
节点信息
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class TreeNode {
/**
* 节点名称
*/
private String nodeName;
/**
* 节点Id
*/
private String nodeId;
/**
* 节点Id
*/
private String nodePid;
/**
* 为了方便测试,初始化一棵树
*
* @return List<TreeNode>
*/
public static List<TreeNode> createTreeA() {
TreeNode ziyang = new TreeNode("ziyang", "a1", "-1");
TreeNode ziyue = new TreeNode("ziyue", "a2", "a1");
TreeNode zixing = new TreeNode("zixing", "a3", "a2");
TreeNode zichen = new TreeNode("zichen", "a4", "a3");
return new ArrayList<>(Arrays.asList(ziyang, ziyue, zixing, zichen));
}
原树图示
此树结构有4层,跟节点是"ziyang",其父节点id为 -1 ,(父节点是 -1 表示根节点);下一节点是 “ziyue”,以此类推。
复制树示例【核心】
step1:生成Id映射Map
/**
* 生成Id映射Map
*
* @param treeA 原树节点集合
* @return 原树和新树Id映射Map
*/
private static Map<String, String> getOldNewMap(List<TreeNode> treeA) {
// 所有节点id集合
List<String> nodeIdList = treeA.stream().map(TreeNode::getNodeId).collect(Collectors.toList());
// 获取原树的跟节点id
String rootId = treeA.stream().filter(item -> item.getNodePid().equals("-1"))
.collect(Collectors.toList())
.get(0).getNodeId();
// 新树跟节点newId期前指定
// 根据实际情况调整
String newId = "b1";
Map<String, String> idMap = new HashMap<>(treeA.size());
for (String nodeId : nodeIdList) {
// 先找到根节点,(根节点的nodePid都是 -1)
if (nodeId.equals(rootId)) {
idMap.put(rootId, newId);
} else {
// 用 UUID 生成 新树和旧树的id映射关系
// 根据实际id策略生成,为了方便此处使用UUID
idMap.put(nodeId, UUID.randomUUID().toString());
}
}
return idMap;
}
step2:生成复制后的树结构
/**
* 生成复制后的树结构
*
* @param treeA 原树节点集合
* @param idMap 原树和新树Id映射Map,由 step1 生成
* @return 改变id的树节点集合
*/
private static List<TreeNode> getTreeB(List<TreeNode> treeA, Map<String, String> idMap) {
List<TreeNode> treeB = new ArrayList<>(treeA.size());
for (TreeNode treeNode : treeA) {
String id = idMap.get(treeNode.getNodeId());
// 可不判空,适用于根节点标记不是 nodePid
if (null != id) {
treeNode.setNodeId(id);
}
String pid = idMap.get(treeNode.getNodePid());
// 跟节点标记在父ID,即 nodePid = -1
// 跟节点的标志 nodePid = -1 不在映射中时不用重新复制,在 idMap 中也取不到,不用处理
if (null != pid) {
treeNode.setNodePid(pid);
}
treeB.add(treeNode);
}
return treeB;
}
新树结构
测试用例
@Test
public void testCopyTest() {
// 获取原树节点集合
List<TreeNode> treeA = TreeNode.createTreeA();
System.out.println("treeA : " + treeA);
// 生成原树和新树Id映射Map
Map<String, String> idMap = getOldNewMap(treeA);
// 获取复制后的树节点
List<TreeNode> treeB = getTreeB(treeA, idMap);
System.out.println("treeB : " + treeB);
}
后记:玄星幻月的第一篇完全是自己想的,遇到这个场景的时候也在网上搜过,但是没有找到类型的例子,也许用到的场景不是很多,但是感觉比较独特,就记录下来,若有更好的树复制方式,欢迎讨论,当然有其他比较有趣的例子,也可以共同探讨。(^_^)