Guava 图数据结构 graph 从基础概念到原理分析

1. 概述

common.graph 是 Guava 中用于表示图(graph)结构数据 的 API 包,位于

com.google.common.graph。这里的"图"指由一组**节点(node)**和连接节点对的

**边(edge)**组成的数据结构,对应离散数学中"图"的概念。

该包的设计哲学与 JDK 的 java.util 集合框架(List / Map / Set)一脉相承:

提供一组通用的、类型安全的抽象接口,并附带经过充分测试的标准实现。使用者通过

**构建器(Builder)**而非直接 new 来创建实例,标准实现类本身不对外公开,从而

把"如何存储"与"如何使用"隔离开来。

模块的核心目标是:

  • 以统一、易用的方式表达有向图 / 无向图带权图含平行边的图
  • 提供与集合框架一致的只读视图不可变实例语义;
  • 对基于图的经典算法(遍历、可达性、环检测、传递闭包等)提供轻量支持。

源码中共有 50 余个类与接口,但对外暴露的核心只有三个抽象、三个构建器,以及

TraverserGraphs 两个工具类。下文将围绕它们展开。


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() 决定。无向图中 successorspredecessors
    等价。
  • 自环(self-loop) :端点相等的边(u == v)。由 allowsSelfLoops() 控制是否允许。
  • 平行边(parallel edges) :连接同一对节点的多条不同边。只有 Network 支持
    GraphValueGraph 在接口层面明确禁止平行边。

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. 三类核心抽象与选型

模块对外提供三种抽象,按复杂度递增排列:GraphValueGraphNetwork

设计者反复强调:在满足需求的前提下,优先选用最简单的那个。

3.1 Graph<N>:匿名边图

边没有任何附加信息,仅表示连接关系。Graph 显式不支持平行边

适用于只需要"谁和谁相连"的场景。

java 复制代码
MutableGraph<String> graph = GraphBuilder.undirected().build();
graph.putEdge("A", "B");

3.2 ValueGraph<N, V>:带值边图

ValueGraphGraph 之上为每条边附加一个非唯一的值 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):是否存在直接连接 uv 的边。
  • 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):连接 uv 的边(集)。
  • adjacentEdges(edge):与某条边共端点的其它边。

4.3 构建与修改

三种 *BuilderGraphBuilder / 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 会直接返回原对象而不再拷贝;

构建器内部先维护一个 MutableGraphbuild() 时再 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),
    TraverserasNetwork() 复用,体现了"图本质上就是一组后继/前驱函数"的思想。
  • ArchetypeGraph:三种图共有的 nodes()isDirected()allowsSelfLoops()
    nodeOrder()adjacentNodes/predecessors/successorsdegree* 等。
  • BaseGraph:仅 GraphValueGraph 共享的额外方法(edges()
    incidentEdgeOrder()asNetwork())。
  • 抽象基类 AbstractBaseGraphedges()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
------这减少了重复代码,也印证了三者的层次关系。

NetworkStandardNetwork 维护两张映射:

  • 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()、返回正确计数。当 incidentEdgeOrderSTABLE

时,额外维护一个 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.equalsisDirected() 相同 nodes() 相同 edges() 相同。
    是否允许自环、节点加入顺序等均不影响相等性。
  • Graph.hashCodeedges().hashCode()
  • ValueGraph.equals:在 Graph 基础上,额外要求每条边的边值 也相等;
    hashCode 为"边→值"映射的哈希。
  • Network.equals:要求边集合相同,且每条边的两端点一致;hashCode
    "边→端点对"映射的哈希。

度计数规则:自环在 degree 中计两次;无向图的 incidentEdges(node).size() 之外

还需加上自环数(见 AbstractBaseGraph.degree)。

6.8 不可变与线程安全

Immutable* 实例被标注为 @Immutable,内部以 StandardValueGraph /

StandardNetwork 作为不可变 backing 存储,线程安全copyOf 对已是不可变实例

会直接返回(零拷贝),对可变实例则重建一份不可变副本。构建器(ImmutableGraph.Builder

内部实则持有一个 MutableGraphbuild() 时再做 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 用三档抽象(GraphValueGraphNetwork)覆盖了从"仅连接关系"

到"带权边"再到"带身份、可平行边"的图建模需求,并通过 Builder 封装实现、用实时视图

与不可变实例分别满足灵活性与安全性。其内部以"节点 → 邻接关系对象"的映射为核心,

用哨兵/包装类与单一 map 巧妙编码有向图中的前驱/后继,在提供丰富 API 的同时保持

O(1) 的访问与修改开销。

实践建议:

  1. 能用简单抽象就用简单抽象 ,不要一上来就选 Network
  2. 只读算法优先使用非可变接口,跨线程或常量场景使用 Immutable*
  3. 注意集合访问器返回的是实时视图 ------若持有期间可能删除节点,请改用 copyOf /
    reachableNodes 等快照式方法;
  4. 节点对象务必正确实现 equals() / hashCode(),它直接决定整个图的正确性。
相关推荐
long3161 小时前
枚举(Enums)
java·开发语言·数据库
m0_547486661 小时前
《数据结构与算法(Python语言版)》全套PPT课件2026
数据结构·算法
墨雨晨曦881 小时前
2026/08/15 spring AI学习总结
java·tomcat
wno7042 小时前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
个 人 练 习 生2 小时前
数据结构入门:算法复杂度
开发语言·数据结构·经验分享·学习·程序人生·算法
聊浮游2 小时前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea
MacroZheng2 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
SamDeepThinking3 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane19943 小时前
接口都能写默认实现了,为什么还需要抽象类
java·后端