使用Java Stream将邻接表组装成树

描述

在关系型数据库中保存树形结构最简单、最广泛的设计方案是采用邻接表(Adjacency List),也就是在一行记录中,用一个parent_id字段来引用同一张表的其他记录,比如:

id name parent_id
0 中国 null
1 北京市 0
2 昌平区 1
3 顺义区 1
4 上海市 0
5 静安区 4
6 黄浦区 4

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/cities")
public class CityController
{
@GetMapping
public List<Item> showTree()
{
// 模拟从数据库查询的数据
List<Item> items = Arrays.asList(
new Item("0", "中国", null),
new Item("1", "北京市", "0"),
new Item("2", "昌平区", "1"),
new Item("3", "顺义区", "1"),
new Item("4", "上海市", "0"),
new Item("5", "静安区", "4"),
new Item("6", "黄浦区", "4")
);

List<Item> result = items.stream().filter(m -> m.getParentId() == null).peek(m -> m.setChildren(findChildren(m, items))).collect(Collectors.toList());
return result;
}

private List<Item> findChildren(Item item, List<Item> all)
{
return all.stream().filter(m -> Objects.equals(m.getParentId(), item.getId())).peek(m -> m.setChildren(findChildren(m, all))).collect(Collectors.toList());
}

public static class Item
{
private String id;
private String name;
private String parentId;

@Include.NON_EMPTY
private List<Item> children;

public String getId()
{
return id;
}

public void setId(String id)
{
this.id = id;
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}

public String getParentId()
{
return parentId;
}

public void setParentId(String parentId)
{
this.parentId = parentId;
}

public List<Item> getChildren()
{
return children;
}

public void setChildren(List<Item> children)
{
this.children = children;
}


public Item(String id, String name, String parentId)
{
this.id = id;
this.name = name;
this.parentId = parentId;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// curl -ivv http://localhost:7777/cities
[
{
"id": "0",
"name": "中国",
"parentId": null,
"children": [
{
"id": "1",
"name": "北京市",
"parentId": "0",
"children": [
{
"id": "2",
"name": "昌平区",
"parentId": "1",
"children": []
},
{
"id": "3",
"name": "顺义区",
"parentId": "1",
"children": []
}
]
},
{
"id": "4",
"name": "上海市",
"parentId": "0",
"children": [
{
"id": "5",
"name": "静安区",
"parentId": "4",
"children": []
},
{
"id": "6",
"name": "黄浦区",
"parentId": "4",
"children": []
}
]
}
]
}
]

解析

使用stream进行过滤后,将结果收集到一个List中,需要注意如果没有满足条件的数据,那么收集的结果就是一个空List,而不是null。

1
all.stream().filter(m -> Objects.equals(m.getParentId(), item.getId())).peek(m -> m.setChildren(findChildren(m, all))).collect(Collectors.toList());

这样就会出现叶子节点的children属性是一个空List:[],接口约定成这样是没有问题的,但是如果是要对接一些成熟的前端树组件,它们的默认行为是当children为[]时,会在节点前面显示为一个可展开的加号+,但是点击之后展开又没有子节点。

而如果接口返回的叶子节点的children属性是null,或者干脆不返回这个children属性,才会只显示一个叶子节点名(没有前面的展开符号)。

为了应对这样的情况,使用stream的collect方法就比较麻烦,但是如果是用在Spring MVC就容易太多。如果你的controller方法返回一个对象,那么Spring Boot框架会调用jackson这个JSON库来将你的对象序列化为一个JSON Object或JSON Array。

需要做的仅仅是在children属性上添加一个@Include.NON_EMPTY注解,这样当这个children属性是空List时,就会被忽略,不参与到序列化中:

1
2
@Include.NON_EMPTY
private List<Item> children;

参考