0%

JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jstat (JVM Statistics Monitoring Tool): 虚拟机统计信息监视工具

详细信息可参考:jstat doc

功能

用于监视虚拟机各种运行状态信息的命令行工具。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。

远程虚拟机进程

需要远程主机提供 RMI 支持,JDK 中提供了 jstatd 工具可以很方便地建立远程 RMI 服务器。

命令格式

jstat [ option vmid [interval[s|ms] [count]] ]

VMID 与 LVMID

如果是本地虚拟机进程,VMID 与 LVMID 是一致的;如果是远程虚拟机进程,那 VMID 的格式应当是:

[protocol:][//]lvmid[@hostname[:port]/servername]

interval 和 count

interval: 查询间隔。如果没有显示指定,默认单位为 ms。

count: 次数

如果省略这 2 个参数,说明只查询一次。

假设需要每 250 毫秒查询一次进程 2764 垃圾收集状况,一共查询 20 次,那命令应当是:

1
$ jstat -gc 2764 250 20

option

代表用户希望查询的虚拟机信息,主要分为三类: 类加载、垃圾收集、运行期编译状况

可以通过如下命令查看所有选项:

1
$ jstat -options

选项 作用
-class 监视类加载、卸载数量、总空间以及类装载所耗费的时间
-gc 监视 Java 堆状况,包括 Eden 区、2 个 Survivor 区、老年代、永久代等的容量,已用空间、垃圾收集时间合计等信息
-gccapacity 监视内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
-gcutil 监视内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因
-gcnew 监视新生代垃圾收集状况
-gcnewcapacity 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
-gcold 监视老年代垃圾收集状况
-gcoldcapacity 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity(x) 输出永久代使用到的最大、最小空间 (高版本已移除)
-compiler 输出即时编译器(JIT compiler,just-in-time compiler)编译过的方法、耗时等信息
-printcompilation 输出已经被即时编译的方法
-gcmetacapacity 输出 metaspace 的统计信息

执行样例

(旧版本可能会有 P (Permanent): 永久代)

-class

监视类加载、卸载数量、总空间以及类装载所耗费的时间。

1
$ jstat -class 5511

Loaded: 加载 class 数量
Bytes: 加载占用空间
Unloaded: 卸载 class 数量
Bytes: 卸载占用空间
Time: 执行类加载和卸载操作总耗时

-gc

监视 Java 堆状况,包括 Eden 区、2 个 Survivor 区、老年代、永久代等的容量,已用空间、垃圾收集时间合计等信息。

1
$ jstat -gc 5511

S0C (Survivor0 Capacity)、S1C (Survivor1 Capacity): S0 / S1 总空间 (KB)
S0U (Survivor0 Utilization)、S1U (Survivor1 Utilization): S0 / S1 已使用空间 (KB)
EC (Eden Capacity): Eden 总空间 (KB)
EU (Eden Utilization): Eden 已使用空间 (KB)
OC (Old Capacity): 老年代总空间 (KB)
OU (Old Utilization): 老年代已使用空间 (KB)
MC (Metaspace Capacity): 元数据区总空间 (KB)
MU (Metaspace Utilization): 元数据区已使用空间 (KB)
CCSC (Compressed Class Space Capacity): 压缩类空间大小 (KB)
CCSU (Compressed Class Space Utilization): 压缩类空间已使用大小 (KB)
YGC (Young GC / Minor GC): Young GC 次数
YGCT (Young GC Time): Young GC 耗时
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时

-gccapacity

监视内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间。

1
$ jstat -gccapacity 5511

NGCMN (New Generation Capacity Min): 新生代最小容量 (KB)
NGCMX (New Generation Capacity Max): 新生代最大容量 (KB)
NGC (New Generation Capacity): 新生代总空间 (KB)
S0C (Survivor0 Capacity)、S1C (Survivor1 Capacity): S0 / S1 总空间 (KB)
EC (Eden Capacity): Eden 总空间 (KB)
OGCMN (Old Generation Capacity Min): 老年代最小容量 (KB)
OGCMX (Old Generation Capacity Max): 老年代最大容量 (KB)
OGC (Old Generation Capacity)、OC (Old Capacity): 老年代总空间 (KB)
MCMN (Metaspace Capacity Min): 元数据区最小容量 (KB)
MCMX (Metaspace Capacity Max): 元数据区最大容量 (KB)
MC (Metaspace Capacity): 元数据区总空间 (KB)
CCSMN (Compressed Class Space Min): 压缩类空间最小容量 (KB)
CCSMX (Compressed Class Space Max): 压缩类空间最大容量 (KB)
CCSC (Compressed Class Space Capacity): 压缩类空间大小 (KB)
YGC (Young GC / Minor GC): Young GC 次数
FGC (Full GC): Full GC 次数

-gcutil

监视内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比。

1
$ jstat -gcutil 5511

S0 (Survivor0)、S1 (Survivor1): S0 / S1 使用比例
E (Eden): Eden 使用比例
O (Old): 老年代使用比例
M (Metaspace): 元数据区使用比例
CCS (Compressed Class Space): 压缩类空间使用比例
YGC (Young GC / Minor GC): Young GC 次数
YGCT (Young GC Time): Young GC 耗时
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时

-gccause

与 -gcutil 功能一样,但是会额外输出导致上一次垃圾收集产生的原因。(输出结果比 -gcutil 多了 LGCC 和 GCC)

1
$ jstat -gccause 5511

S0 (Survivor0)、S1 (Survivor1): S0 / S1 使用比例
E (Eden): Eden 使用比例
O (Old): 老年代使用比例
M (Metaspace): 元数据区使用比例
CCS (Compressed Class Space): 压缩类空间使用比例
YGC (Young GC / Minor GC): Young GC 次数
YGCT (Young GC Time): Young GC 耗时
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时
LGCC (Last GC Cause): 上次 GC 原因
GCC (GC Cause): 本次 GC 原因

-gcnew

监视新生代垃圾收集状况。

1
$ jstat -gcnew 5511

S0C (Survivor0 Capacity)、S1C (Survivor1 Capacity): S0 / S1 总空间 (KB)
S0U (Survivor0 Utilization)、S1U (Survivor1 Utilization): S0 / S1 已使用空间 (KB)
TT (Tenuring Threshold): 对象在新生代的存活次数
MTT (Max Tenuring Threshold): 对象在新生代存活的最大次数
DSS (Desired Survivor Size): 期望的幸存区大小 (KB)
EC (Eden Capacity): Eden 总空间 (KB)
EU (Eden Utilization): Eden 已使用空间 (KB)
YGC (Young GC / Minor GC): Young GC 次数
YGCT (Young GC Time): Young GC 耗时

-gcnewcapacity

监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间。

1
$ jstat -gcnewcapacity 5511

NGCMN (New Generation Capacity Min): 新生代最小容量 (KB)
NGCMX (New Generation Capacity Max): 新生代最大容量 (KB)
NGC (New Generation Capacity): 新生代总空间 (KB)
S0CMX (Survivor0 Capacity Max): S0 最大容量 (KB)
S0C (Survivor0 Capacity): S0 总空间 (KB)
S1CMX (Survivor1 Capacity Max): S1 最大容量 (KB)
S1C (Survivor1 Capacity): S1 总空间 (KB)
ECMX (Eden Capacity Max): Eden 最大容量 (KB)
EC (Eden Capacity): Eden 总空间 (KB)
YGC (Young GC / Minor GC): Young GC 次数
FGC (Full GC): Full GC 次数

-gcold

监视老年代垃圾收集状况。

1
$ jstat -gcold 5511

MC (Metaspace Capacity): 元数据区总空间 (KB)
MU (Metaspace Utilization): 元数据区已使用空间 (KB)
CCSC (Compressed Class Space Capacity): 压缩类空间大小 (KB)
CCSU (Compressed Class Space Utilization): 压缩类空间已使用大小 (KB)
OC (Old Capacity): 老年代总空间 (KB)
OU (Old Utilization): 老年代已使用空间 (KB)
YGC (Young GC / Minor GC): Young GC 次数
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时

-gcoldcapacity

监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间。

1
$ jstat -gcoldcapacity 5511

OGCMN (Old Generation Capacity Min): 老年代最小容量 (KB)
OGCMX (Old Generation Capacity Max): 老年代最大容量 (KB)
OGC (Old Generation Capacity)、OC (Old Capacity): 老年代总空间 (KB)
OC (Old Capacity): 老年代总空间 (KB)
YGC (Young GC / Minor GC): Young GC 次数
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时

-gcpermcapacity (x)

输出永久代使用到的最大、最小空间。(高版本已移除)

1
$ jstat -gcpermcapacity 5511

会提示:Unknown option: -gcpermcapacity

-compiler

输出即时编译器(JIT compiler,just-in-time compiler)编译过的方法、耗时等信息。

1
$ jstat -compiler 5511

Compiled: 编译任务数
Failed: 失败的编译任务数
Invalid: 无效的编译任务数
Time: 编译花费时间
FailedType: 上次编译失败的编译类型
FailedMethod: 上次编译失败的类名和方法

-printcompilation

输出已经被即时编译的方法。

1
$ jstat -printcompilation 5511

Compiled: 最近编译方法的数量
Size: 最近编译方法的字节码数量
Type: 最近编译方法的编译类型
Method: 标识最近编译方法的类名和方法名。这两个字段的格式与 HotSpot - XX:+PrintComplation 选项一致

-gcmetacapacity

输出 metaspace 的统计信息。

1
$ jstat -gcmetacapacity 5511

MCMN (Metaspace Capacity Min): 元数据区最小容量 (KB)
MCMX (Metaspace Capacity Max): 元数据区最大容量 (KB)
MC (Metaspace Capacity): 元数据区总空间 (KB)
CCSMN (Compressed Class Space Min): 压缩类空间最小容量 (KB)
CCSMX (Compressed Class Space Max): 压缩类空间最大容量 (KB)
CCSC (Compressed Class Space Capacity): 压缩类空间大小 (KB)
YGC (Young GC / Minor GC): Young GC 次数
FGC (Full GC): Full GC 次数
FGCT (Full GC Time): Full GC 耗时
GCT (GC Time): GC 总耗时


JDK 的 bin 目录下为 Java 开发人员提供了很多实用的小工具,很多场景下都会用到它们,比如:打包、部署、签名、调试、监控、运维等。本文介绍其中一款:

jps (JVM Process Status Tool): 虚拟机进程状况工具

功能

比较单一,列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main() 函数所在的类)名称以及这些进程的本地虚拟机唯一 ID(LVMID)。

LVMID 和 PID

LVMID (Local Virtual Machine Identifier): 本地虚拟机进程的唯一 ID

PID (Process Identifier): 操作系统的进程 ID

对于本地虚拟机进程来说,LVMID 与 PID 是一致的。

命令格式

1
jps [ options ] [ hostid ]

options

选项 作用
-q 只输出 LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类 main() 函数的参数
-l 输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径
-v 输出虚拟机进程启动时的 JVM 参数

hostid

jps 还可以通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态,参数 hostid 为 RMI 注册表中注册的主机名。

执行样例

1
$ jps -l


本文适用于 1.11 及以上版本,整理自 Flink 官网-配置 JobManager 内存

非本地执行

内存模型(Memory Model)

process_mem_model

配置选项(Configuration Options)

Overview

Overview

进程总内存(Total Process Memory)

jobmanager.memory.process.size = (none)

对于容器化部署模式(Containerized Deployment),这相当于申请的容器(Container)大小。

jobmanager.memory.flink.size = (none)

JVM Heap

JobManager 的 JVM 堆内存。

jobmanager.memory.heap.size = (none)

Off-Heap Memory

JobManager 的堆外内存。

jobmanager.memory.off-heap.size = 128 mb

JVM 元空间(JVM Metaspace)(Off-Heap)

Flink JVM 进程的 Metaspace。

jobmanager.memory.jvm-metaspace.size = 256 mb

JVM 开销(JVM Overhead)(Off-Heap)

用于其他 JVM 开销的本地内存,例如栈空间、垃圾回收空间等。

jobmanager.memory.jvm-overhead.(min/max/fraction) = (192 mb/1 gb/0.1)

JVM 参数(JVM Parameters)

Flink 进程启动时,会根据配置的和自动推导出的各内存部分大小,也可以显式地设置以下 JVM 参数:

JVM 参数 JobManager 取值
-Xmx 和 -Xms JVM 堆内存(JVM Heap)
-XX:MaxDirectMemorySize 堆外内存(Off-Heap Memory)
-XX:MaxMetaspaceSize JVM 元空间(JVM Metaspace)

建议(recommend)

  • 如果已经明确设置了 JVM 堆内存(jobmanager.memory.heap.size),建议不要再设置 进程总内存(jobmanager.memory.process.size)Flink 总内存(jobmanager.memory.flink.size),否则可能会造成内存配置冲突。

  • 如果遇到 JobManager 进程抛出 “OutOfMemoryError: Direct buffer memory” 的异常,可以尝试调大 堆外内存(jobmanager.memory.off-heap.size) 配置。

  • 如果同时配置了 Flink 总内存(jobmanager.memory.flink.size)JVM 堆内存(jobmanager.memory.heap.size),且没有配置 堆外内存(jobmanager.memory.off-heap.size),那么 堆外内存的大小 = Flink 总内存 - JVM 堆内存。 这种情况下,堆外内存 的默认大小将不会生效。

本地执行(Local Execution)

本地执行(Local Execution) 是指将 Flink 作为一个单独的 Java 程序运行在电脑本地而非创建一个集群(例如在 IDE 中)。

本地执行模式下不需要为 JobManager 进行内存配置,配置参数将不会生效。


本文适用于 1.10 及以上版本,整理自 Flink 官网-配置 TaskManager 内存

非本地执行

内存模型(Memory Model)

Simple Memory Model

simple_mem_model

Detailed Memory Model

detailed-mem-model

配置选项(Configuration Options)

Overview

Overview

进程总内存(Total Process Memory)

taskmanager.memory.process.size = (none)

对于容器化部署模式(Containerized Deployment),这相当于申请的容器(Container)大小。

taskmanager.memory.flink.size = (none)

JVM Heap
  • 框架堆内存(Framework Heap)

    Flink 框架使用的(内部数据结构及操作使用的)堆内存。即 TaskManager 本身所占用的堆上内存,不计入 Slot 资源中。

    taskmanager.memory.framework.heap.size = 128 mb

  • 任务堆内存 / 任务(算子)堆内存(Task Heap / Task (Operator) Heap Memory)

    Flink 应用的算子及用户代码执行所使用的堆内存。

    taskmanager.memory.task.heap.size = (none)

Off-Heap Memory
  • 托管内存(Managed Memory)

    由 Flink 分配和管理的用于排序、哈希表、缓存中间结果及 RocksDB State Backend 的本地内存。

    taskmanager.memory.managed.(size/fraction) = (none/0.4)

    当同时指定二者时,会优先采用 size 指定的大小。若二者均未指定,会根据默认占比进行计算。

  • 直接内存(Direct Memory)

    • 框架堆外内存(Framework Off-Heap)

      Flink 框架使用的(内部数据结构及操作使用的)堆外内存。即 TaskManager 本身所占用的堆外内存,不计入 Slot 资源中。

      taskmanager.memory.framework.off-heap.size = 128 mb

    • 任务堆外内存(Task Off-Heap)

      Flink 应用的算子及用户代码执行所使用的堆外内存。

      taskmanager.memory.task.off-heap.size = 0 bytes

    • 网络内存(Network)

      用于任务(task)之间数据传输的直接内存(例如网络传输缓冲区)。

      taskmanager.memory.network.(min/max/fraction) = (64 mb/1 gb/0.1)

JVM Specific Memory(Off-Heap)

运行 Flink 的 JVM 使用的内存。

JVM 元空间(JVM Metaspace)

Flink JVM 进程的 Metaspace。

taskmanager.memory.jvm-metaspace.size = 256 mb

JVM 开销(JVM Overhead)

用于其他 JVM 开销的本地内存,例如栈空间、垃圾回收空间等。

taskmanager.memory.jvm-overhead.(min/max/fraction) = (192 mb/1 gb/0.1)

JVM 参数(JVM Parameters)

Flink 进程启动时,会根据配置的和自动推导出的各内存部分大小,也可以显式地设置以下 JVM 参数:

JVM 参数 TaskManager 取值
-Xmx 和 -Xms 框架堆内存(Framework Heap) + 任务堆内存(Task Heap)
-XX:MaxDirectMemorySize 框架堆外内存(Framework Off-Heap) + 任务堆外内存(Task Off-Heap) + 网络内存(Network)
-XX:MaxMetaspaceSize JVM 元空间(JVM Metaspace)

建议(recommend)

  • 不建议同时设置 进程总内存(taskmanager.memory.process.size)Flink 总内存(taskmanager.memory.flink.size)。 这可能会造成内存配置冲突,从而导致部署失败。

  • 如果已经明确设置了 任务堆内存(taskmanager.memory.task.heap.size)托管内存(taskmanager.memory.managed.(size/fraction)),建议不要再设置 进程总内存(taskmanager.memory.process.size)Flink 总内存(taskmanager.memory.flink.size),否则可能会造成内存配置冲突。

  • 框架堆外内存(taskmanager.memory.framework.off-heap.size) 是一个进阶配置,建议仅在确定 Flink 框架需要更多的内存时调整该配置。

  • 通常情况下,不建议对 框架堆内存(taskmanager.memory.framework.heap.size)框架堆外内存(taskmanager.memory.framework.off-heap.size) 进行调整。 除非你非常肯定 Flink 的内部数据结构及操作需要更多的内存。

  • Flink 会负责管理 网络内存(taskmanager.memory.network.(min/max/fraction)),保证其实际用量不会超过配置大小。 因此,调整网络内存的大小不会对其他堆外内存有实质上的影响。

本地执行(Local Execution)

本地执行(Local Execution) 是指将 Flink 作为一个单独的 Java 程序运行在电脑本地而非创建一个集群(例如在 IDE 中)。

本地执行​时,只有下列配置会生效,其他配置参数不会起到任何效果:

组成部分 配置参数 本地执行时的默认值
任务堆内存(Task Heap) taskmanager.memory.task.heap.size 无穷大(Long.MAX_VALUE)
任务堆外内存(Task Off-Heap) taskmanager.memory.task.off-heap.size 无穷大(Long.MAX_VALUE)
托管内存(Managed Memory) taskmanager.memory.managed.size 128Mb
网络内存(Network) taskmanager.memory.network.(min/max) 64Mb

Note

  • 本地执行模式下,任务堆内存的大小与实际的堆空间大小无关。

  • 本地执行模式下,JVM 堆空间的实际大小不受 Flink 掌控,而是取决于本地执行进程是如何启动的。如果希望控制 JVM 的堆空间大小,可以在启动进程时明确地指定相关的 JVM 参数,即 -Xmx 和 -Xms。


今天立冬,祝大家立冬快乐哦!

1

在上篇文章(传送门)中,我们已经详细介绍并用两种数据结构实现了 Trie。实际上把上篇文章中的任何一版代码提交给 leetcode 都完全可以解答本题,所以本文的意义并不在于如何解答这道题。

写 java 的人,肯定都知道,面向对象的三大特征:封装、继承、多态。而本文就是我从官方题解中得到的一点点关于「封装」的体会,做个小小的分享。

问题

先回顾一下我们上篇文章中的代码,其实对于 Node 的封装做得并不好,可以说几乎就没有封装。问题如以下两点:

  1. 出于封装性和安全性的考虑,对于 Node 的两个成员变量,我们不应该直接访问,而应该通过成员方法去访问。

因为,首先,将成员变量设置为 public 权限,对于该成员变量的访问和修改的控制就不再由封装它的对象来控制,而是由调用它的类来控制,这样就破坏了 java 面向对象的封装性;其次,程序何时何地修改成员变量的值也很难控制和排查,从而影响安全性。

  1. 假如我们想达到这个目的:随意切换底层数据结构(比如从数组切换到 Map,或反之),但是不想影响到原本已经实现的代码功能。

如果要用上篇文章中的代码实现这个目的,那么大多数方法都需要修改一些实现细节。比如判断当前节点是否包含指向下一个字符的指针,获取下一个节点,往当前节点之后插入一个节点等。

修改的地方越多,出问题的概率就越大。最好能做到上层代码尽量不要动,这样的改动才是最放心的。

所以,我们可以把这些操作细节抽象出来,在 Node 中做一些封装,提供给上层代码使用。

对 Node 进行封装

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
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* 抽象类,可作为具体数据结构实现的父类
*/
public abstract class Node {

/**
* 该节点是否是字符串的结尾
*/
protected boolean isEndingChar;

protected void setEndingChar() {
isEndingChar = true;
}

protected boolean isEndingChar() {
return isEndingChar;
}

protected boolean notContainsKey(char c) {
return !this.containsKey(c);
}

/**
* 当前节点是否包含指向下一个字符的指针
*/
abstract boolean containsKey(char c);

/**
* 获取下一个节点
*/
abstract Node get(char c);

/**
* 往当前节点之后插入一个节点
*/
abstract void put(char c);
}

/**
* 数组实现
*/
public class ArrayTrieNode extends Node {

/**
* 指向后续节点的指针
*/
private final ArrayTrieNode[] next;

private ArrayTrieNode(boolean isEndingChar) {
this.isEndingChar = isEndingChar;
next = new ArrayTrieNode[26];
}

public ArrayTrieNode() {
this(false);
}

@Override
public boolean containsKey(char c) {
return next[c - 'a'] != null;
}

@Override
public Node get(char c) {
return next[c - 'a'];
}

@Override
public void put(char c) {
next[c - 'a'] = new ArrayTrieNode();
}
}

/**
* HashMap 实现
*/
public class HashMapTrieNode extends Node {

/**
* 指向后续节点的指针
*/
private final Map<Character, HashMapTrieNode> next;

private HashMapTrieNode(boolean isEndingChar) {
this.isEndingChar = isEndingChar;
next = new HashMap<>();
}

public HashMapTrieNode() {
this(false);
}

@Override
public boolean containsKey(char c) {
return next.containsKey(c);
}

@Override
public Node get(char c) {
return next.get(c);
}

@Override
public void put(char c) {
next.put(c, new HashMapTrieNode());
}
}

上层代码

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
/**
* Implement Trie (Prefix Tree)
* 实现 Trie(前缀树)
*/
public class Trie {

/**
* The root node.
*/
private final Node root;

/**
* Initialize the data structure.
*/
public Trie() {
// 修改处
root = new ArrayTrieNode();
// root = new HashMapTrieNode();
}

/**
* Inserts a word into the trie.
*/
public void insert(String word) {
Node node = root;
for (int i = 0; i < word.length(); i++) {
char curLetter = word.charAt(i);
if (node.notContainsKey(curLetter)) {
node.put(curLetter);
}
node = node.get(curLetter);
}
node.setEndingChar();
}

/**
* search a prefix or whole key in trie and returns the node where search ends
*/
private Node searchPrefix(String word) {
Node node = root;
for (int i = 0; i < word.length(); i++) {
char curLetter = word.charAt(i);
if (node.notContainsKey(curLetter)) {
return null;
}
node = node.get(curLetter);
}
return node;
}

/**
* Returns if the word is in the trie.
*/
public boolean search(String word) {
Node node = searchPrefix(word);
return node != null && node.isEndingChar();
}

/**
* Returns if there is any word in the trie that starts with the given prefix.
*/
public boolean startsWith(String prefix) {
Node node = searchPrefix(prefix);
return node != null;
}
}

通过对 Node 节点的封装,当需要切换底层数据结构时,只需要在 Trie 构造方法中初始化不同的底层数据结构实现即可,其它代码我们一行都不需要修改!

通过两篇文章的代码对比,可以看到,通过良好的封装,可以使代码的可读性大大提高,而且底层数据结构的变更完全不会给上层代码带来影响,这会带来不错的体验。