Java 基於反射的通用樹形結構工具類
在日常的開發中, 經常會遇到許多樹形結構的場景, 如選單樹, 部門樹, 目錄樹等. 而這些一般都會涉及到要將資料庫查詢出來的集合轉化為樹形結構的功能.
由於list
->tree
是一個比較通用的功能, 無非就是根據id
,pid
,children
這三個欄位進行轉換. 但由於欄位名可能不一致, 如選單裡可能叫menuId
, 而部門裡叫deptId
,所以我用反射來實現了一個通用的工具類, 來進行轉換.
工具類:
import org.springframework.util.StringUtils; import javax.validation.constraints.NotNull; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; public class TreeUtils { /** * 集合轉樹結構 * * @param collection 目標集合 * @param clazz集合元素型別 * @return 轉換後的樹形結構 */ public static <T> Collection<T> toTree(@NotNull Collection<T> collection, @NotNull Class<T> clazz) { return toTree(collection, null, null, null, clazz); } /** * 集合轉樹結構 * * @param collection 目標集合 * @param id節點編號欄位名稱 * @param parent父節點編號欄位名稱 * @param children子節點集合屬性名稱 * @param clazz集合元素型別 * @return轉換後的樹形結構 */ public static <T> Collection<T> toTree(@NotNull Collection<T> collection, String id, String parent, String children, @NotNull Class<T> clazz) { try { if (collection == null || collection.isEmpty()) return null;// 如果目標集合為空,直接返回一個空樹 if (StringUtils.isEmpty(id)) id = "id";// 如果被依賴欄位名稱為空則預設為id if (StringUtils.isEmpty(parent)) parent = "parent";// 如果依賴欄位為空則預設為parent if (StringUtils.isEmpty(children)) children = "children";// 如果子節點集合屬性名稱為空則預設為children // 初始化根節點集合, 支援 Set 和 List Collection<T> roots; if (collection.getClass().isAssignableFrom(Set.class)) { roots = new HashSet<>(); } else { roots = new ArrayList<>(); } // 獲取 id 欄位, 從當前物件或其父類 Field idField; try { idField = clazz.getDeclaredField(id); } catch (NoSuchFieldException e1) { idField = clazz.getSuperclass().getDeclaredField(id); } // 獲取 parentId 欄位, 從當前物件或其父類 Field parentField; try { parentField = clazz.getDeclaredField(parent); } catch (NoSuchFieldException e1) { parentField = clazz.getSuperclass().getDeclaredField(parent); } // 獲取 children 欄位, 從當前物件或其父類 Field childrenField; try { childrenField = clazz.getDeclaredField(children); } catch (NoSuchFieldException e1) { childrenField = clazz.getSuperclass().getDeclaredField(children); } // 設定為可訪問 idField.setAccessible(true); parentField.setAccessible(true); childrenField.setAccessible(true); // 找出所有的根節點 for (T c : collection) { Object parentId = parentField.get(c); if (isRootNode(parentId)) { roots.add(c); } } // 從目標集合移除所有根節點 collection.removeAll(roots); // 遍歷根節點, 依次新增子節點 for (T root : roots) { addChild(root, collection, idField, parentField, childrenField); } // 關閉可訪問 idField.setAccessible(false); parentField.setAccessible(false); childrenField.setAccessible(false); return roots; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } /** * 為目標節點新增孩子節點 * * @param node目標節點 * @param collection目標集合 * @param idFieldID 欄位 * @param parentField父節點欄位 * @param childrenField 位元組點欄位 */ private static <T> void addChild(@NotNull T node, @NotNull Collection<T> collection, @NotNull Field idField, @NotNull Field parentField, @NotNull Field childrenField) throws IllegalAccessException { Object id = idField.get(node); Collection<T> children = (Collection<T>) childrenField.get(node); // 如果子節點的集合為 null, 初始化孩子集合 if (children == null) { if (collection.getClass().isAssignableFrom(Set.class)) { children = new HashSet<>(); } else children = new ArrayList<>(); } for (T t : collection) { Object o = parentField.get(t); if (id.equals(o)) { // 將當前節點新增到目標節點的孩子節點 children.add(t); // 重設目標節點的孩子節點集合,這裡必須重設,因為如果目標節點的孩子節點是null的話,這樣是沒有地址的,就會造成資料丟失,所以必須重設,如果目標節點所在類的孩子節點初始化為一個空集合,而不是null,則可以不需要這一步,因為java一切皆指標 childrenField.set(node, children); // 遞迴新增孩子節點 addChild(t, collection, idField, parentField, childrenField); } } } /** * 判斷是否是根節點, 判斷方式為: 父節點編號為空或為 0, 則認為是根節點. 此處的判斷應根據自己的業務資料而定. * @param parentId父節點編號 * @return是否是根節點 */ private static boolean isRootNode(Object parentId) { boolean flag = false; if (parentId == null) { flag = true; } else if (parentId instanceof String && (StringUtils.isEmpty(parentId) || parentId.equals("0"))) { flag = true; } else if (parentId instanceof Integer && Integer.valueOf(0).equals(parentId)) { flag = true; } return flag; } }
選單實體類:
public class Menu implements Serializable { private static final long serialVersionUID = 5561561457068906366L; private Integer menuId; private Integer parentId; private String menuName; private String url; private List<Menu> children; public Integer getMenuId() { return menuId; } public void setMenuId(Integer menuId) { this.menuId = menuId; } public Integer getParentId() { return parentId; } public void setParentId(Integer parentId) { this.parentId = parentId; } public String getMenuName() { return menuName; } public void setMenuName(String menuName) { this.menuName = menuName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public List<Menu> getChildren() { return children; } public void setChildren(List<Menu> children) { this.children = children; } }
測試類:
@Test public void test() { List<Menu> menuList = new ArrayList<>(); menuList.add(new Menu(1, null, "節點1")); menuList.add(new Menu(2, null, "節點2")); menuList.add(new Menu(3, 1, "節點1.1")); menuList.add(new Menu(4, 1, "節點1.2")); menuList.add(new Menu(5, 3, "節點1.1.1")); Collection<Menu> menus = TreeUtils.toTree(menuList, "menuId", "parentId", "children", Menu.class); System.out.println(JSONUtil.toJsonStr(menus)); }
執行結果:
[ { "menuId": 1, "menuName": "節點1", "children": [ { "children": [ { "menuId": 5, "menuName": "節點1.1.1", "parentId": 3 } ], "menuId": 3, "menuName": "節點1.1", "parentId": 1 }, { "menuId": 4, "menuName": "節點1.2", "parentId": 1 } ] }, { "menuId": 2, "menuName": "節點2" } ]