使用Java實現(xiàn)通用樹形結構構建工具類
完整代碼
package com.pig4cloud.pigx.common.core.util.tree; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * 通用樹結構構建工具類 * * <p>重要說明: * <ol> * <li>所有節(jié)點必須具有唯一ID</li> * <li>父節(jié)點不存在時自動成為根節(jié)點</li> * <li>節(jié)點排序依賴comparator實現(xiàn)</li> * <li>支持循環(huán)依賴檢測和錯誤路徑提示</li> * </ol> * * @param <T> 原始數(shù)據(jù)類型 * @param <K> 節(jié)點ID類型(建議使用包裝類型) */ public class TreeBuilder<T, K> { private final Function<T, K> idGetter; private final Function<T, K> parentIdGetter; private final ChildSetter<T> childSetter; private final Comparator<T> comparator; /** * 構造方法 */ public TreeBuilder(Function<T, K> idGetter, Function<T, K> parentIdGetter, ChildSetter<T> childSetter, Comparator<T> comparator) { this.idGetter = Objects.requireNonNull(idGetter, "ID獲取器不能為null"); this.parentIdGetter = Objects.requireNonNull(parentIdGetter, "父ID獲取器不能為null"); this.childSetter = Objects.requireNonNull(childSetter, "子節(jié)點設置器不能為null"); this.comparator = Objects.requireNonNull(comparator, "排序比較器不能為null"); } /** * 構建完整樹結構 */ public List<T> buildTree(List<T> items) { Objects.requireNonNull(items, "節(jié)點列表不能為null"); if (items.isEmpty()) return Collections.emptyList(); // 1. 構建數(shù)據(jù)索引 Map<K, T> nodeMap = createNodeMap(items); Map<K, List<T>> parentChildrenMap = items.stream() .collect(Collectors.groupingBy( parentIdGetter, LinkedHashMap::new, // 保持插入順序 Collectors.toList() )); // 2. 循環(huán)依賴檢測 detectCyclicDependencies(items, nodeMap); // 3. 構建樹結構 nodeMap.forEach((nodeId, node) -> { List<T> children = parentChildrenMap.getOrDefault(nodeId, Collections.emptyList()) .stream() .sorted(comparator) .collect(Collectors.toList()); childSetter.setChildren(node, Collections.unmodifiableList(children)); }); // 4. 獲取根節(jié)點(parentId為null或不存在于nodeMap) return items.stream() .filter(item -> isRootNode(item, nodeMap)) .sorted(comparator) .collect(Collectors.toList()); } /** * 判斷是否為根節(jié)點(抽離方法提升可讀性) */ private boolean isRootNode(T item, Map<K, T> nodeMap) { K parentId = parentIdGetter.apply(item); return parentId == null || !nodeMap.containsKey(parentId); } /** * 構建搜索結果樹 */ public List<T> buildSearchTree(List<T> allItems, Set<K> matchIds) { Objects.requireNonNull(allItems, "節(jié)點列表不能為null"); Objects.requireNonNull(matchIds, "匹配ID集合不能為null"); Set<K> relatedIds = findRelatedIds(allItems, matchIds); List<T> relatedItems = allItems.stream() .filter(item -> relatedIds.contains(idGetter.apply(item))) .collect(Collectors.toList()); return buildTree(relatedItems); } /** * 創(chuàng)建節(jié)點ID映射表(含重復檢測) */ private Map<K, T> createNodeMap(List<T> items) { Map<K, T> map = new LinkedHashMap<>(items.size()); for (T item : items) { K id = idGetter.apply(item); if (map.containsKey(id)) { throw new IllegalArgumentException(String.format( "發(fā)現(xiàn)重復節(jié)點ID: %s (沖突對象1: %s, 沖突對象2: %s)", id, map.get(id), item)); } map.put(id, item); } return map; } /** * 循環(huán)依賴檢測核心邏輯 */ private void detectCyclicDependencies(List<T> items, Map<K, T> nodeMap) { Set<K> verifiedNodes = new HashSet<>(); Map<K, K> idToParentMap = items.stream() .collect(Collectors.toMap(idGetter, parentIdGetter)); for (T item : items) { K currentId = idGetter.apply(item); if (verifiedNodes.contains(currentId)) continue; Set<K> path = new LinkedHashSet<>(); K tracingId = currentId; while (tracingId != null) { if (!path.add(tracingId)) { throw new CyclicDependencyException(buildCyclePath(path, tracingId)); } // 短路已驗證節(jié)點 if (verifiedNodes.contains(tracingId)) break; K parentId = idToParentMap.get(tracingId); if (parentId == null) break; // 直接循環(huán)檢測 if (parentId.equals(tracingId)) { throw new CyclicDependencyException("直接循環(huán)依賴: " + tracingId); } tracingId = parentId; } verifiedNodes.addAll(path); } } /** * 構造循環(huán)路徑描述 */ private String buildCyclePath(Set<K> path, K duplicateId) { List<K> pathList = new ArrayList<>(path); int index = pathList.indexOf(duplicateId); List<K> cycle = pathList.subList(index, pathList.size()); return "檢測到循環(huán)依賴鏈: " + cycle.stream() .map(Object::toString) .collect(Collectors.joining(" → ")); } /** * 查找相關ID集合(匹配節(jié)點+路徑節(jié)點) */ private Set<K> findRelatedIds(List<T> allItems, Set<K> matchIds) { Map<K, K> idToParentMap = allItems.stream() .collect(Collectors.toMap(idGetter, parentIdGetter)); return matchIds.stream() .flatMap(id -> traceAncestors(id, idToParentMap).stream()) .collect(Collectors.toSet()); } /** * 追溯父節(jié)點鏈 */ private Set<K> traceAncestors(K startId, Map<K, K> idToParentMap) { Set<K> ancestors = new LinkedHashSet<>(); K currentId = startId; while (currentId != null && ancestors.add(currentId)) { currentId = idToParentMap.get(currentId); } return ancestors; } /** * 自定義循環(huán)依賴異常 */ public static class CyclicDependencyException extends RuntimeException { public CyclicDependencyException(String message) { super(message); } } /** * 子節(jié)點設置接口 */ @FunctionalInterface public interface ChildSetter<T> { void setChildren(T parent, List<T> children); } /* 快捷構造方法 */ public static <T, K> TreeBuilder<T, K> create( Function<T, K> idGetter, Function<T, K> parentIdGetter, ChildSetter<T> childSetter, Comparator<T> comparator) { return new TreeBuilder<>(idGetter, parentIdGetter, childSetter, comparator); } public static <T, K extends Comparable<? super K>> TreeBuilder<T, K> createWithNaturalOrder( Function<T, K> idGetter, Function<T, K> parentIdGetter, ChildSetter<T> childSetter) { return new TreeBuilder<>( idGetter, parentIdGetter, childSetter, Comparator.comparing(idGetter, Comparator.nullsLast(Comparator.naturalOrder())) ); } }
一、設計思想與核心功能
本工具類采用泛型設計,可處理任意類型的節(jié)點數(shù)據(jù),具備以下核心能力:
- 多類型支持:通過泛型參數(shù)T(數(shù)據(jù)類型)和K(ID類型),支持各種業(yè)務場景
- 自動化構建:自動識別根節(jié)點、建立父子關系
- 安全防護:內(nèi)置循環(huán)依賴檢測、重復ID校驗
- 靈活擴展:支持自定義排序規(guī)則、子節(jié)點設置方式
- 高效查詢:提供子樹構建功能,適用于搜索場景
二、核心實現(xiàn)原理
1. 數(shù)據(jù)結構準備階段
Map<K, T> nodeMap = createNodeMap(items); Map<K, List<T>> parentChildrenMap = items.stream() .collect(Collectors.groupingBy(...));
- 節(jié)點映射表:通過ID快速定位節(jié)點,驗證ID唯一性
- 父子關系映射:預先生成父節(jié)點→子節(jié)點列表的關系字典
2. 循環(huán)依賴檢測算法
采用路徑追蹤法,時間復雜度O(n):
Set<K> path = new LinkedHashSet<>(); while (tracingId != null) { if (!path.add(tracingId)) { throw new CyclicDependencyException(...); } // 追溯父節(jié)點鏈 }
可檢測兩種異常情況:
- 直接循環(huán):父節(jié)點指向自身
- 間接循環(huán):A→B→C→A型循環(huán)鏈
3. 樹形結構構建
采用兩階段構建模式:
- 初始化所有節(jié)點的子節(jié)點列表
- 篩選根節(jié)點(父ID不存在或對應節(jié)點缺失)
4. 搜索子樹生成
通過ID回溯算法構建有效路徑:
Set<K> traceAncestors(K startId) { // 向上追溯所有祖先節(jié)點 }
確保搜索結果的完整樹形結構
三、關鍵代碼詳解
1. 節(jié)點排序實現(xiàn)
childSetter.setChildren(node, children.stream() .sorted(comparator) .collect(Collectors.toList()) );
支持兩種排序方式:
- 自然排序(createWithNaturalOrder)
- 自定義比較器(推薦業(yè)務相關排序)
2. 異常處理機制
自定義異常類型增強可讀性:
public class CyclicDependencyException extends RuntimeException { // 攜帶具體循環(huán)路徑信息 }
提供明確的錯誤定位信息:
檢測到循環(huán)依賴鏈: 1001 → 1002 → 1003 → 1001
3. 函數(shù)式接口應用
@FunctionalInterface public interface ChildSetter<T> { void setChildren(T parent, List<T> children); }
使用時可通過Lambda表達式實現(xiàn):
TreeBuilder<Department, Long> builder = new TreeBuilder<>(..., (parent, children) -> parent.setChildDepts(children));
四、使用示例
基礎用法
List<Menu> menus = getFromDB(); TreeBuilder<Menu, Integer> builder = TreeBuilder.create( Menu::getId, Menu::getParentId, (parent, children) -> parent.setChildren(children), Comparator.comparing(Menu::getSortOrder) ); List<Menu> tree = builder.buildTree(menus);
搜索場景應用
Set<Integer> matchIds = searchService.findIds("關鍵"); List<Menu> resultTree = builder.buildSearchTree(allMenus, matchIds);
五、注意事項
- ID規(guī)范:
- 必須實現(xiàn)有效的hashCode()和equals()
- 推薦使用包裝類型(避免Long與long的匹配問題)
- 對象狀態(tài):
- 原始數(shù)據(jù)對象應支持子節(jié)點集合設置
- 建議使用不可變集合防止意外修改
- 特殊場景:
- 空集合處理返回emptyList()
- 允許游離節(jié)點(父節(jié)點不存在時成為根節(jié)點)
- 性能考量:
- 萬級數(shù)據(jù)量建議分批處理
- 頻繁構建時可緩存nodeMap
以上就是使用Java實現(xiàn)通用樹形結構構建工具類的詳細內(nèi)容,更多關于Java構建樹形結構的資料請關注腳本之家其它相關文章!
相關文章
Java中for、while、do while三種循環(huán)語句的區(qū)別介紹
這篇文章主要介紹了Java中for、while、do while三種循環(huán)語句的區(qū)別介紹的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07詳解IDEA使用Maven項目不能加入本地Jar包的解決方法
這篇文章主要介紹了詳解IDEA使用Maven項目不能加入本地Jar包的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08阿里面試Nacos配置中心交互模型是push還是pull原理解析
這篇文章主要為大家介紹了阿里面試Nacos配置中心交互模型是push還是pull原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07Spring使用Configuration注解管理bean的方式詳解
在Spring的世界里,Configuration注解就像是一位細心的園丁,它的主要職責是在這個繁花似錦的園子里,幫助我們聲明和管理各種各樣的bean,本文給大家介紹了在Spring中如何優(yōu)雅地管理你的bean,需要的朋友可以參考下2024-05-05Idea Project文件目錄不見了,只剩External Libraries和imi文件的解決
這篇文章主要介紹了Idea Project文件目錄不見了,只剩External Libraries和imi文件的解決方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08spring boot 1.5.4 集成shiro+cas,實現(xiàn)單點登錄和權限控制
這篇文章主要介紹了spring boot 1.5.4 集成shiro+cas,實現(xiàn)單點登錄和權限控制,需要的朋友可以參考下2017-06-06TOMCAT內(nèi)存溢出及大小調整的實現(xiàn)方法
下面小編就為大家?guī)硪黄猅OMCAT內(nèi)存溢出及大小調整的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05