在日常的開發工作中,時常會遇到樹形結構的封裝,比如:樹形結構的菜單數據、部門數據等等。最近工作中,指標的樹形結構封裝場景頻繁,比如:校驗每個層級的指標權重之和要等於100,指標的滿樹校驗等,接下來我們就來看一下我的思路。
一、准備數據
(1)准備一個指標實體類

@Data public class Indicator { private String code; private String parentCode; private String name; private Integer weight; private List<Indicator> children = Lists.newArrayList(); public Indicator() { } public Indicator(String code, String parentCode, String name, Integer weight) { this.code = code; this.parentCode = parentCode; this.name = name; this.weight = weight; } }
(2)樹形結構封裝代碼【並提供幾個重載方法】

import com.beust.jcommander.internal.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import javax.annotation.Nonnull; import java.lang.reflect.Field; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * @author niubi */ @Slf4j public class TreeUtil { /** * 組裝樹結構 * * @param list . * @param fieldNameForIndex 主鍵名稱 * @param fieldNameForParentIndex parent主鍵名稱 * @param fieldNameForChildren children名稱 * @return . */ @SuppressWarnings(value = {"rawtypes", "unchecked"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex, @Nonnull String fieldNameForParentIndex, @Nonnull String fieldNameForChildren) { Objects.requireNonNull(list, "list is null"); Objects.requireNonNull(fieldNameForIndex, "fieldNameForIndex is null"); Objects.requireNonNull(fieldNameForChildren, "fieldNameForChildren is null"); Objects.requireNonNull(fieldNameForParentIndex, "fieldNameForParentIndex is null"); List root = new ArrayList(); Map map = new HashMap(); try { for (Object o : list) { Field indexField = o.getClass().getDeclaredField(fieldNameForIndex); if (!indexField.isAccessible()) { indexField.setAccessible(true); } Object index = indexField.get(o); map.put(index, o); } for (Object o : map.keySet()) { Object obj = map.get(o); Class<?> clazz = obj.getClass(); Field parentIndexField = clazz.getDeclaredField(fieldNameForParentIndex); if (!parentIndexField.isAccessible()) { parentIndexField.setAccessible(true); } Object parentIndex = parentIndexField.get(obj); Object parent = map.get(parentIndex); if (Objects.isNull(parent)) { root.add(obj); continue; } Class<?> clazz1 = parent.getClass(); Field childrenField = null; try { childrenField = clazz1.getDeclaredField(fieldNameForChildren); } catch (Exception e) { childrenField = clazz1.getSuperclass().getDeclaredField(fieldNameForChildren); } if (!childrenField.isAccessible()) { childrenField.setAccessible(true); } List children = (List) childrenField.get(parent); if (Objects.isNull(children)) { children = new ArrayList(); } children.add(obj); childrenField.set(parent, children); } } catch (Exception e) { log.error(e.getMessage(), e); root = null; } return Optional.ofNullable(root); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list) { return treeify(list, "id", "parentId", "children"); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex) { return treeify(list, fieldNameForIndex, "parentId", "children"); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex, @Nonnull String fieldNameForParentIndex) { return treeify(list, fieldNameForIndex, fieldNameForParentIndex, "children"); } }
(3)指標樹形結構代碼執行

public static void testTree1() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一級指標1", 50)); indicators.add(new Indicator("11", "1", "二級指標1", 100)); indicators.add(new Indicator("111", "11", "三級指標1", 100)); indicators.add(new Indicator("2", "0", "一級指標2", 50)); indicators.add(new Indicator("21", "2", "二級指標1", 50)); indicators.add(new Indicator("221", "2", "二級指標2", 50)); // 封裝樹形結構 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> indicatorTree = Lists.newArrayList(); if (treeify.isPresent()) { indicatorTree = treeify.get(); } System.out.println("指標樹形結構:" + indicatorTree); }
二、校驗指標樹形結構中任意節點下的子節點權重之和為 100
(1)方法一:使用遞歸方式進行逐層校驗

public static void testTree3() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一級指標1", 50)); indicators.add(new Indicator("11", "1", "二級指標1", 100)); indicators.add(new Indicator("111", "11", "三級指標1", 100)); indicators.add(new Indicator("2", "0", "一級指標2", 50)); indicators.add(new Indicator("21", "2", "二級指標1", 50)); indicators.add(new Indicator("221", "2", "二級指標2", 50)); List<Indicator> oneIndicators = indicators.stream().filter(user -> user.getParentCode().equals("0")).collect(Collectors.toList()); Integer sum = oneIndicators.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum != 100) { System.out.println("一級指標權重之和校驗失敗"); return; } // 逐個一級指標遞歸校驗權重之和為 100 for (Indicator user : oneIndicators) { Boolean flag = validData(user, indicators); if (!flag) { System.out.println("校驗失敗"); return; } } System.out.println("校驗成功"); } private static Boolean validData(Indicator indicator, List<Indicator> indicators) { // 過濾出當前指標的下級指標集合 List<Indicator> children = indicators.stream().filter(element -> element.getParentCode().equals(indicator.getCode())).collect(Collectors.toList()); if (CollectionUtils.isEmpty(children)) { return true; } Integer sum = children.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum != 100) { System.out.println("code = " + indicator.getCode() + ", parentCode = " + indicator.getParentCode() + ", 校驗失敗"); return false; } for (Indicator child : children) { validData(child, indicators); } return true; }
(2)方法二:使用非遞歸方式逐層校驗

public static void testTree2() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一級指標1", 50)); indicators.add(new Indicator("11", "1", "二級指標1", 100)); indicators.add(new Indicator("111", "11", "三級指標1", 100)); indicators.add(new Indicator("2", "0", "一級指標2", 50)); indicators.add(new Indicator("21", "2", "二級指標1", 50)); indicators.add(new Indicator("221", "2", "二級指標2", 50)); // 封裝樹形結構 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> userTree = Lists.newArrayList(); if (treeify.isPresent()) { userTree = treeify.get(); } // 判斷每一個節點下所有子節點的分數累加之和是否是100 Boolean flag = validAgeSum(userTree); if (flag) { System.out.println("校驗成功"); } else { System.out.println("校驗失敗"); } } private static Boolean validAgeSum(List<Indicator> userTree) { List<Indicator> children = userTree; // 頂層一級指標校驗 Integer sum1 = children.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum1 != 100) { return false; } // 非遞歸逐層每個根節點校驗 while(CollectionUtils.isNotEmpty(children)) { for (Indicator indicator : children) { if (CollectionUtils.isEmpty(indicator.getChildren())) { return true; } Integer sum2 = indicator.getChildren().stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum2 != 100) { System.out.println("code = " + indicator.getCode() + ", parentCode = " + indicator.getParentCode() + ", 校驗失敗"); return false; } } // 重新組裝子節點集合 children = children.stream().map(Indicator::getChildren).flatMap(Collection::stream).collect(Collectors.toList()); } return true; }
三、校驗指標樹形結構是否是一棵滿樹【滿樹:以任意一個一級指標節點開始,樹的高度等同】

/** * 封裝樹形結構 */ public static void testTree1() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一級指標1", 50)); indicators.add(new Indicator("11", "1", "二級指標1", 100)); indicators.add(new Indicator("111", "11", "三級指標1", 100)); indicators.add(new Indicator("2", "0", "一級指標2", 50)); indicators.add(new Indicator("21", "2", "二級指標1", 50)); indicators.add(new Indicator("211", "21", "san級指標1", 100)); indicators.add(new Indicator("221", "2", "二級指標2", 50)); indicators.add(new Indicator("222", "221", "三級指標1", 100)); // 封裝樹形結構 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> indicatorTree = Lists.newArrayList(); if (treeify.isPresent()) { indicatorTree = treeify.get(); } System.out.println("指標樹形結構:" + indicatorTree); // 判斷樹是否是等高 Boolean isFullTree = isFullTree(indicatorTree); System.out.println(isFullTree); } private static Boolean isFullTree(List<Indicator> userTree) { List<Indicator> children = userTree; while(CollectionUtils.isNotEmpty(children)) { // 獲取所有根節點下的子節點列表 List<List<Indicator>> lists = children.stream() .map(Indicator::getChildren) .map(userList -> Objects.isNull(userList) ? new ArrayList<Indicator>() : userList) .collect(Collectors.toList()); // 獲取所有根節點下的子節點非空數量 long count = lists.stream().filter(CollectionUtils::isNotEmpty).count(); if (count != 0 && count < children.size()) { // 去除特殊情況:最后一層葉子節點【count = 0】 return false; } children = lists.stream().flatMap(Collection::stream).collect(Collectors.toList()); } return true; }
推薦:hutool 工具類也是一款非常值得開發者去研究和借鑒,樹形結構的封裝也是很完美,而且還可以自行擴展字段信息;