前言

在很多业务场景中,我们处理的层次化数据往往涉及到例如组织结构分类体系行政区划等,常常具备父子关系。然而,在一些情况下,数据可能是扁平化的,层级信息分散在不同字段中,没有直接的父子字段。

比如,一个记录可能包含省、市、县、乡等字段,但这些字段本身没有明确的父子关系。如何把这些扁平化的数据转换成树状结构呢?

本文将演示如何通过 Java 8 的流操作(Stream API),将扁平化的数据转换为清晰的树状层次结构。无论数据层级的数量多少,都能灵活地进行分组和转化。

示例数据

定义一个数据类

List<City> cities = Arrays.asList(
        new City(1, "广东省", "深圳市", "宝安区", "街道11"),
        new City(2, "广东省", "深圳市", "宝安区", "街道22"),
        new City(3, "广东省", "深圳市", null, null),
        new City(4, "广东省", "广州市", "番禺区", "街道1111"),
        new City(5, "江西省", "受伤市", "是啥县", "大幅范德萨街道")
);
// 原始数据类 City
class City {
    int id;
    String province;
    String city;
    String county;
    String township;

    City(int id, String province, String city, String county, String township) {
        this.id = id;
        this.province = province;
        this.city = city;
        this.county = county;
        this.township = township;
    }

    public String getProvince() {
        return province;
    }

    public String getCity() {
        return city;
    }

    public String getCounty() {
        return county;
    }

    public String getTownship() {
        return township;
    }
}

我们的目标是将这些扁平化的数据按不同层级(省、市、县、乡)进行分组和构建树状结构。

问题与挑战

面对扁平数据,我们有以下几个关键挑战:

1:如何按不同的层级(省、市、县、乡等)对数据进行分组?

2:层级的数量不固定,有时只需要按省市分组,有时需要更多层级,如何动态处理?

3:如何设计一个通用方法来兼容不同的场景,确保方法的复用性?

4:如何处理 null 值,避免分组时出现错误?

解决方案

1:数据建模

我们首先定义一个泛型的 TreeNode 类来表示树的节点。每个节点包含一个 name 和多个子节点 children

// 树状结构
public class TreeNode<T> {
    public String id;
    public String parentId;
    public String name;
    public T data;
    public List<TreeNode<T>> children;

    public TreeNode(String id, String parentId, String name, T data) {
        this.id = id;
        this.parentId = parentId;
        this.name = name;
        this.data = data;
        this.children = new ArrayList<>();
    }

    public void addChild(TreeNode<T> child) {
        this.children.add(child);
    }
}

2. 树形结构的构建

buildCityTree 方法通过递归和流操作来根据不同层级的字段将数据分组并构建树形结构。方法接收三个参数:数据列表、按层级分组的列函数、父节点的 ID。

/**
 *
 * @param data 扁平化的节点数据
 * @param columnFunctions 你需要分层的节点列(按你需要分层的顺序排好)
 * @param parentId 节点的父ID
 * @return
 * @param <T> 分好层的节点数据
 */
 private static <T> TreeNode<T> buildCityTree(List<T> data, List<Function<T, String>> columnFunctions, String parentId) {
        if (columnFunctions.isEmpty()) {
            return null;
        }

        // 按当前列进行分组,处理 null 值
        Map<String, List<T>> grouped = data.stream()
            .filter(item -> columnFunctions.get(0).apply(item) != null) // 过滤掉 null 值
            .collect(Collectors.groupingBy(
                item -> columnFunctions.get(0).apply(item)
            ));

        // 创建根节点
        TreeNode<T> root = new TreeNode<>(parentId != null ? parentId + "-root" : "root", parentId, "根目录名称", null);

        // 处理分组中的每个项
        for (Map.Entry<String, List<T>> entry : grouped.entrySet()) {
            // 生成唯一的 id,使用上层节点的 id 作为前缀
            String nodeId = root.id + "-" + UUID.randomUUID().toString().substring(0, 8);

            // 创建当前分组的节点
            TreeNode<T> node = new TreeNode<>(nodeId, root.id, entry.getKey(), null);
            root.addChild(node);

            // 递归处理下一列
            List<Function<T, String>> remainingColumns = columnFunctions.size() > 1
                ? columnFunctions.subList(1, columnFunctions.size())
                : Collections.emptyList();

            // 获取子树(递归调用)
            TreeNode<T> childTree = buildCityTree(entry.getValue(), remainingColumns, node.id);

            // 如果子树不为空,将其子节点添加到当前节点
            if (childTree != null) {
                node.children.addAll(childTree.children);
            }
        }

        return root;
    }

3. 测试用例

3.1:按【省、市、县、乡】分组
public class CityTree {
    public static void main(String[] args) {
        // 初始化数据
        List<City> cities = Arrays.asList(
            new City(1, "广东省", "深圳市", "宝安区", "街道11"),
            new City(2, "广东省", "深圳市", "宝安区", "街道22"),
            new City(3, "广东省", "深圳市", null, null),
            new City(4, "广东省", "广州市", "番禺区", "街道1111"),
            new City(5, "江西省", "受伤市", "是啥县", "大幅范德萨街道")
        );

        // 按省、市、县、乡分组  按你生成需要的列顺序,依次写入列
        List<Function<City, String>> columnFunctions = Arrays.asList(
            City::getProvince,  // 省级城市
            City::getCity,      // 市级城市
            City::getCounty,    // 县级城市
            City::getTownship   // 乡级城市
        );

        // 生成树状结构
        TreeNode<City> tree = buildCityTree(cities, columnFunctions,"0");

        // 打印树状结构
       printTree(tree, 0);
    }
       private static void printTree(TreeNode<City> node, int level) {
        System.out.println("  ".repeat(level) + node.name);
        for (TreeNode<City> child : node.children) {
            printTree(child, level + 1);
        }
    }
结果输出
广东省
  深圳市
    宝安区
      街道11
      街道22
    广州市
      番禺区
        街道1111
江西省
  受伤市
    是啥县
      大幅范德萨街道

Java实现将动态层级数据转换为树状结构_数据处理

2:按【市,县、乡】分组
public class CityTree {
    public static void main(String[] args) {
        // 初始化数据
        List<City> cities = Arrays.asList(
            new City(1, "广东省", "深圳市", "宝安区", "街道11"),
            new City(2, "广东省", "深圳市", "宝安区", "街道22"),
            new City(3, "广东省", "深圳市", null, null),
            new City(4, "广东省", "广州市", "番禺区", "街道1111"),
            new City(5, "江西省", "受伤市", "是啥县", "大幅范德萨街道")
        );

        // 按市、县、乡分组
        List<Function<City, String>> columnFunctions = Arrays.asList(
            City::getCity,      // 市级城市
            City::getCounty,    // 县级城市
            City::getTownship   // 乡级城市
        );

        // 生成树状结构
        TreeNode<City> tree = buildCityTree(cities, columnFunctions,"0");

        // 打印树状结构
        .........
    }
结果输出
根目录名称
  受伤市
    是啥县
      大幅范德萨街道
  深圳市
    宝安区
      街道22
      街道11
  广州市
    番禺区
      街道1111

Java实现将动态层级数据转换为树状结构_树状结构_02

4. 结语

通过使用 Java 8 中的流操作和递归,我们能够轻松地将扁平数据转换为层次化的树状结构。