1. 概述
common.graph 是 Guava 中用于表示图(graph)结构数据 的 API 包,位于
com.google.common.graph。这里的"图"指由一组**节点(node)**和连接节点对的
**边(edge)**组成的数据结构,对应离散数学中"图"的概念。
该包的设计哲学与 JDK 的 java.util 集合框架(List / Map / Set)一脉相承:
提供一组通用的、类型安全的抽象接口,并附带经过充分测试的标准实现。使用者通过
**构建器(Builder)**而非直接 new 来创建实例,标准实现类本身不对外公开,从而
把"如何存储"与"如何使用"隔离开来。
模块的核心目标是:
- 以统一、易用的方式表达有向图 / 无向图 、带权图 、含平行边的图;
- 提供与集合框架一致的只读视图 与不可变实例语义;
- 对基于图的经典算法(遍历、可达性、环检测、传递闭包等)提供轻量支持。
源码中共有 50 余个类与接口,但对外暴露的核心只有三个抽象、三个构建器,以及
Traverser 与 Graphs 两个工具类。下文将围绕它们展开。
2. 核心概念
在深入 API 之前,先明确几个贯穿全文的基础概念。
2.1 节点与元素
节点类型用泛型 N 表示。节点必须非 null,且能够作为 Map 的键 ------即其
equals() / hashCode() 必须正确实现。图中的所有集合(nodes()、adjacentNodes()
等)都依赖节点的相等性。
2.2 边的两种身份模型
这是理解 Graph / ValueGraph / Network 三者差异的关键:
- 匿名边:边本身没有独立身份,只表示"节点 U 与节点 V 之间存在连接"。
- 带值边 :边除连接两节点外,还携带一个附加的值(泛型
V),例如距离、权重、容量。 - 带身份边 :边是一个独立对象(泛型
E),有自己唯一身份,可以像节点一样被
单独引用、存储、比较,且允许多条不同的边连接同一对节点(平行边)。
2.3 EndpointPair:边的端点对
EndpointPair<N> 是不可变的端点对,抽象地表示一条边的两个端点:
- 有向边 是有序对 (
ordered(source, target)),可调用source()/target(); - 无向边 是无序对 (
unordered(u, v),即[u, v]),调用source()/target()
会抛UnsupportedOperationException。
端点相等性遵循直觉:有序对按 (source, target) 比较;无序对按"包含相同两个节点"
比较;有序对永远不等于无序对。EndpointPair 也实现了 Iterable<N>,可像二元组一样遍历。
2.4 有向 / 无向、自环、平行边
- 有向图 / 无向图 :由
isDirected()决定。无向图中successors与predecessors
等价。 - 自环(self-loop) :端点相等的边(
u == v)。由allowsSelfLoops()控制是否允许。 - 平行边(parallel edges) :连接同一对节点的多条不同边。只有
Network支持 ;
Graph与ValueGraph在接口层面明确禁止平行边。
2.5 元素顺序 ElementOrder
图的许多访问器返回集合,这些集合的迭代顺序由 ElementOrder 决定:
| 类型 | 语义 | 底层 Map |
|---|---|---|
UNORDERED |
不保证任何顺序 | HashMap |
STABLE |
跨迭代/跨版本稳定(具体顺序见 javadoc) | LinkedHashMap |
INSERTION |
按元素加入顺序 | LinkedHashMap |
SORTED |
按给定 Comparator(或自然序) |
TreeMap |
图共有三类顺序可配:节点顺序 nodeOrder()(默认插入序)、边顺序 edgeOrder()
(仅 Network)、关联边顺序 incidentEdgeOrder()(默认无序,仅支持 UNORDERED
或 STABLE)。不可变图固定为 STABLE。
2.6 度(degree)
degree(node):关联边总数;自环计两次。无向图中等于邻接节点数加自环数。inDegree(node)/outDegree(node):有向图为入/出边数量;无向图中二者都等于degree。
3. 三类核心抽象与选型
模块对外提供三种抽象,按复杂度递增排列:Graph → ValueGraph → Network 。
设计者反复强调:在满足需求的前提下,优先选用最简单的那个。
3.1 Graph<N>:匿名边图
边没有任何附加信息,仅表示连接关系。Graph 显式不支持平行边 。
适用于只需要"谁和谁相连"的场景。
java
MutableGraph<String> graph = GraphBuilder.undirected().build();
graph.putEdge("A", "B");
3.2 ValueGraph<N, V>:带值边图
ValueGraph 在 Graph 之上为每条边附加一个非唯一的值 V(例如权重、容量、
代价)。它同样不支持平行边 ,但可以通过 asGraph() 退化成一个 Graph 视图。
适用于"边上有数值"的场景(最短路径权重、流量上限等)。
java
MutableValueGraph<String, Double> graph = ValueGraphBuilder.directed().build();
graph.putEdgeValue("A", "B", 12.5);
Double w = graph.edgeValueOrDefault("A", "B", 0.0);
3.3 Network<N, E>:带身份边图
Network 的边是独立的对象 E,拥有唯一身份,因而支持平行边 与自环 。
它的 edges() 返回 Set<E>(而非 EndpointPair),可以单独查询某条边连接了
哪两个节点(incidentNodes(E))。适用于"边本身有意义、需要被引用、或同一对节点
之间可能有多条边"的场景。
java
MutableNetwork<String, String> net = NetworkBuilder.directed()
.allowsParallelEdges(true).build();
net.addEdge("A", "B", "train-line-1");
net.addEdge("A", "B", "bus-line-7"); // 平行边,允许
3.4 选型建议
| 你的需求 | 选择 |
|---|---|
| 仅关心节点间是否相连 | Graph |
| 边上需要携带数值(权重/代价等) | ValueGraph |
| 需要引用边本身、或允许平行边 | Network |
| 只读地运行算法 | 使用非可变接口(Graph/ValueGraph/Network) |
| 多线程共享、或作为常量 | ImmutableGraph / ImmutableValueGraph / ImmutableNetwork |
4. 提供的功能
4.1 图级访问器
| 方法 | 说明 |
|---|---|
nodes() |
所有节点集合(按 nodeOrder() 顺序) |
edges() |
所有边。Graph/ValueGraph 返回 Set<EndpointPair<N>>;Network 返回 Set<E> |
isDirected() |
是否有向 |
allowsSelfLoops() |
是否允许自环 |
allowsParallelEdges() |
是否允许平行边(仅 Network) |
nodeOrder() / edgeOrder() / incidentEdgeOrder() |
各类元素的迭代顺序 |
4.2 元素级访问器
adjacentNodes(node):与node共享一条边的节点(=predecessors ∪ successors)。successors(node)/predecessors(node):沿出边 / 入边可达的节点。degree / inDegree / outDegree:各类"度"计数。hasEdgeConnecting(u, v):是否存在直接连接u→v的边。incidentEdges(node):与node关联的所有边。
针对 ValueGraph:
edgeValue(u, v)/edgeValueOrDefault(u, v, default):读取边值。asGraph():去掉边值的Graph视图。
针对 Network:
incidentNodes(edge):一条边连接的两个节点。inEdges(node)/outEdges(node):入边 / 出边集合。edgesConnecting(u, v)/edgeConnecting(u, v):连接u、v的边(集)。adjacentEdges(edge):与某条边共端点的其它边。
4.3 构建与修改
三种 *Builder(GraphBuilder / ValueGraphBuilder / NetworkBuilder)是创建实例
的唯一入口,配置项包括:
directed()/undirected():方向性(构造起点)。from(graph):基于已有图拷贝可查询属性,快速创建同构构建器。allowsSelfLoops(boolean)、allowsParallelEdges(boolean)(仅 Network)。expectedNodeCount(int)/expectedEdgeCount(int):性能提示,用于预设容量。nodeOrder(...)/edgeOrder(...)/incidentEdgeOrder(...):元素顺序。immutable():转为对应Immutable*的构建器,便于构造静态常量图。
修改操作集中在三个 Mutable* 子接口:
addNode(n):加入节点(已存在则无操作)。putEdge(u, v)(Graph)/putEdgeValue(u, v, value)(ValueGraph)/
addEdge(u, v, e)(Network):增边。若端点尚不存在会被自动补建。removeNode(n):删除节点及其所有关联边。removeEdge(...):删除边。
所有返回集合的访问器都给出的是不可修改的实时视图 :视图会随图的变化而更新,
但调用方不能修改视图本身。
4.4 不可变图
ImmutableGraph / ImmutableValueGraph / ImmutableNetwork 在构造后结构永远不变,
线程安全 ,并可通过 copyOf(...) 从任意可变实例拷贝:
java
ImmutableGraph<String> g = GraphBuilder.undirected()
.<String>immutable()
.putEdge("A", "B")
.putEdge("B", "C")
.build();
工具细节:若传入的已是 Immutable* 实例,copyOf 会直接返回原对象而不再拷贝;
构建器内部先维护一个 MutableGraph,build() 时再 copyOf 成不可变实例。
4.5 遍历 Traverser
Traverser 用于从起点(集)出发遍历可达节点,基于 SuccessorsFunction(一个只声明
successors(N) 的函数式接口,因此可直接传入 Graph 或用 lambda 描述任意后继关系):
forGraph(fn):通用图,可能含环、可能有多条路径,保证每个可达节点最多访问一次。forTree(fn):已知是树/森林(任意可达节点至多一条路径),不维护"已访问"集合,
空间更省O(H)(H 为待访问"前沿"规模)。
遍历顺序三种:
java
Traverser.forGraph(graph).breadthFirst(start); // 广度优先
Traverser.forGraph(graph).depthFirstPreOrder(start); // 深度优先(前序)
Traverser.forGraph(graph).depthFirstPostOrder(start); // 深度优先(后序)
返回的 Iterable 可重复迭代,且元素是按需惰性计算的(配合 Iterables.limit 可只取前 N 个)。
4.6 工具类 Graphs
Graphs 提供一批静态工具方法:
hasCycle(graph):是否存在环(含自环);对无向图有边数优化。transitiveClosure(graph, strategy):计算传递闭包,支持自环策略
(ADD_SELF_LOOPS_ALWAYS/ADD_SELF_LOOPS_FOR_CYCLES)。reachableNodes(graph, node):从node出发可达的节点集合(快照)。transpose(graph):返回所有边方向反转的视图(原图变化会反映到视图)。inducedSubgraph(graph, nodes):诱导子图(保留给定节点及其内部边)。copyOf(graph):可变副本。
5. 使用场景示例
5.1 社交关注关系(有向 Graph)
关注是单向的:A 关注 B 不代表 B 关注 A。
java
MutableGraph<String> follow = GraphBuilder.directed().build();
follow.putEdge("Alice", "Bob");
follow.putEdge("Bob", "Carol");
// 粉丝:predecessors("Bob") = {Alice}
// 关注:successors("Bob") = {Carol}
5.2 路网权重(有向 ValueGraph)
城市间道路的行驶耗时,边上带数值:
java
MutableValueGraph<String, Integer> road = ValueGraphBuilder.directed().build();
road.putEdgeValue("Beijing", "Tianjin", 30); // 分钟
road.putEdgeValue("Tianjin", "Shijiazhuang", 120);
int t = road.edgeValueOrDefault("Beijing", "Tianjin", 0);
5.3 多线路交通(有平行边的 Network)
两座城市间既通高铁也通普快,用平行边表达:
java
MutableNetwork<String, String> transit = NetworkBuilder.undirected()
.allowsParallelEdges(true).build();
transit.addEdge("Beijing", "Shanghai", "G1"); // 高铁
transit.addEdge("Beijing", "Shanghai", "T109"); // 普快
Set<String> lines = transit.edgesConnecting("Beijing", "Shanghai"); // {G1, T109}
5.4 依赖分析与拓扑(遍历 + 环检测)
检测模块依赖是否成环,并做拓扑序处理:
java
if (Graphs.hasCycle(dependencyGraph)) {
throw new IllegalStateException("存在循环依赖");
}
for (String m : Traverser.forGraph(dependencyGraph).depthFirstPostOrder(root)) {
process(m); // 保证被依赖者先处理
}
5.5 静态常量图(不可变)
可作为 static final 常量安全共享,如国家邻接关系:
java
static final ImmutableGraph<String> MAP = GraphBuilder.undirected()
.<String>immutable()
.putEdge("France", "Germany")
.putEdge("France", "Belgium")
.build();
5.6 递归结构的遍历(Traverser.forTree)
抽象语法树、组织架构等多为树/森林,可用 forTree 高效遍历:
java
Traverser.forTree(node -> ImmutableList.of(node.left(), node.right()))
.breadthFirst(root);
6. 设计实现
6.1 接口分层
模块通过一套内部接口把"共享行为"与"差异化行为"分层:
SuccessorsFunction<N> ─┐
PredecessorsFunction<N>├── ArchetypeGraph<N> ──┬── BaseGraph<N> ──┬── Graph<N>
│ └── ValueGraph<N,V>
└── Network<N,E>
SuccessorsFunction/PredecessorsFunction是只声明单方法的函数式接口(SAM),
被Traverser和asNetwork()复用,体现了"图本质上就是一组后继/前驱函数"的思想。ArchetypeGraph:三种图共有的nodes()、isDirected()、allowsSelfLoops()、
nodeOrder()、adjacentNodes/predecessors/successors、degree*等。BaseGraph:仅Graph与ValueGraph共享的额外方法(edges()、
incidentEdgeOrder()、asNetwork())。- 抽象基类
AbstractBaseGraph把edges()、degree()、hasEdgeConnecting()等
全部基于successors()派生实现 ;AbstractGraph/AbstractValueGraph/
AbstractNetwork则提供equals()/hashCode()/toString()的标准实现。
实现类不公开 (StandardMutableGraph 等均为包级 final 类),由 Builder 封闭创建,
这是典型的"面向接口编程 + 隐藏实现"的设计。
6.2 存储模型
Graph / ValueGraph :核心实现 StandardValueGraph 只维护一张映射
Map<N, GraphConnections<N, V>> nodeConnections,即"每个节点 → 它的邻接关系对象"。
边值就藏在 GraphConnections 内部。
一个值得注意的设计:StandardMutableGraph(即 Graph 的可变实现)本身并不单独
存储边 ,而是把边是否存在编码为一个特殊的 Presence.EDGE_EXISTS 值,作为
StandardMutableValueGraph<N, Presence> 的转发包装。换句话说,Graph 在内部被实现为
"边值无信息"的 ValueGraph------这减少了重复代码,也印证了三者的层次关系。
Network :StandardNetwork 维护两张映射:
Map<N, NetworkConnections<N, E>> nodeConnections:每个节点 → 它的邻接关系;Map<E, N> edgeToReferenceNode:每条边 → 一个"参考节点"。有向图中参考节点即
source,无向图中为任一端点。注释明确说明:选择只存参考节点而非EndpointPair,
是为了在平均度数较高时显著降低内存占用(约省 5%~20%+)。
6.3 邻接关系的编码(GraphConnections)
GraphConnections 负责"某个节点与哪些节点相连、边值是什么"。针对有向/无向各有实现。
有向图 DirectedGraphConnections 用一个 Map<N, Object> adjacentNodeValues 同时
存前驱和后继,值有三种编码状态:
PRED:哨兵对象,表示该节点仅是前驱(入边);V(边值类型):表示该节点仅是后继(出边),值即边值;PredAndSucc:包装类,表示该节点既是前驱又是后继,内部保留后继值。
再配合 predecessorCount / successorCount 两个计数器,就能在 O(1) 内区分
predecessors() 与 successors()、返回正确计数。当 incidentEdgeOrder 为 STABLE
时,额外维护一个 List<NodeConnection>(Pred/Succ 子类)来保持边的插入顺序 ,
因为单个 LinkedHashMap 无法区分"同一目标节点先作前驱、后作后继"的两条边。
把前驱/后继合并进一张 map,比"两张独立 map"更省内存,是该模块在时间与空间之间
的典型权衡。
无向图 UndirectedGraphConnections 则用单个 Map<N, V>------因为无向图中前驱与
后继等价,predecessors()、successors()、adjacentNodes() 都直接返回同一个
key 集合视图,实现非常简洁。
6.4 网络的邻接关系(NetworkConnections)
Network 需要把"边对象"作为一等公民,因此 NetworkConnections 的编码按
"有向/无向 × 是否允许平行边"分为四种实现:
- 有向、
DirectedNetworkConnections:持有inEdgeMap (E→source)与
outEdgeMap (E→target)两张映射,外加selfLoopCount; - 无向、平行边:
UndirectedMultiNetworkConnections(同一对节点可有多个边,map 的
值升级为边的集合); - 其余组合类推。
基类 AbstractDirectedNetworkConnections 把入边/出边 map 及自环计数统一管理,
派生的四种实现只关心"平行边时如何把单值扩展成集合"。
6.5 实时视图 vs 快照
这是一个需要重点理解的设计取舍:
- 实时视图 :
adjacentNodes()、successors()、inEdges()等返回的集合是
不可修改视图,会随原图变化而动态更新。但如果持有视图期间对应的节点被移除了 ,
该视图再次被访问会抛IllegalStateException(仅equals(view)与hashCode()
仍可安全调用,且节点重新加入后行为未定义)。这一失效机制由InvalidatableSet
实现------它包裹真实集合,并在每次访问前检查宿主元素是否仍在图中。 - 快照 :
reachableNodes()、transitiveClosure()、inducedSubgraph()、
copyOf()返回的是独立的新集合/新图,与原图此后互不影响。
权衡点在于:视图零拷贝、省内存,但生命周期脆弱;快照安全,但有拷贝成本。API 在设计上
把"免费的实时视图"作为默认,把"昂贵的快照"作为显式方法提供。
6.6 元素顺序与 createMap
ElementOrder.createMap(expectedSize) 根据类型返回不同底层 Map(见 2.5 表)。
StandardValueGraph / StandardNetwork 在构造时就用 nodeOrder().createMap(...) 创建
nodeConnections 容器;当容器是 TreeMap 时(即 SORTED 顺序),会改用带缓存的
MapRetrievalCache 以加速重复查找。
6.7 相等与哈希语义
相等只关心结构,不关心构建细节与顺序:
Graph.equals:isDirected()相同 且nodes()相同 且edges()相同。
是否允许自环、节点加入顺序等均不影响相等性。Graph.hashCode:edges().hashCode()。ValueGraph.equals:在Graph基础上,额外要求每条边的边值 也相等;
hashCode为"边→值"映射的哈希。Network.equals:要求边集合相同,且每条边的两端点一致;hashCode为
"边→端点对"映射的哈希。
度计数规则:自环在 degree 中计两次;无向图的 incidentEdges(node).size() 之外
还需加上自环数(见 AbstractBaseGraph.degree)。
6.8 不可变与线程安全
Immutable* 实例被标注为 @Immutable,内部以 StandardValueGraph /
StandardNetwork 作为不可变 backing 存储,线程安全 。copyOf 对已是不可变实例
会直接返回(零拷贝),对可变实例则重建一份不可变副本。构建器(ImmutableGraph.Builder)
内部实则持有一个 MutableGraph,build() 时再做 copyOf。
6.9 性能特征
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
nodes() / successors() 等集合访问器 |
O(1) | 返回视图,无拷贝 |
putEdge / addEdge / addNode |
O(1) | 平均情况 |
removeEdge |
O(1) | 平均情况 |
removeNode |
O(d) | d 为该节点度数,需先摘除所有关联边 |
Traverser 遍历 |
O(n) 时间 | forGraph 空间 O(n),forTree 空间 O(H) |
空间上与节点数、边数成正比。DirectedGraphConnections 把前驱/后继合并进单 map,
相比双 map 方案更省内存,是模块在空间上的刻意优化。
7. 小结
common.graph 用三档抽象(Graph → ValueGraph → Network)覆盖了从"仅连接关系"
到"带权边"再到"带身份、可平行边"的图建模需求,并通过 Builder 封装实现、用实时视图
与不可变实例分别满足灵活性与安全性。其内部以"节点 → 邻接关系对象"的映射为核心,
用哨兵/包装类与单一 map 巧妙编码有向图中的前驱/后继,在提供丰富 API 的同时保持
O(1) 的访问与修改开销。
实践建议:
- 能用简单抽象就用简单抽象 ,不要一上来就选
Network; - 只读算法优先使用非可变接口,跨线程或常量场景使用
Immutable*; - 注意集合访问器返回的是实时视图 ------若持有期间可能删除节点,请改用
copyOf/
reachableNodes等快照式方法; - 节点对象务必正确实现
equals()/hashCode(),它直接决定整个图的正确性。