
提升你的 Java 技能:使用 Apache Groovy Maps
熟悉 Java map 的开发者可以从探索 Apache Groovy map 中获益。Groovy 通过提供额外的功能、简洁的语法和用于常见操作的内置方法,扩展了 Java map 的功能。反过来,这可以提高代码的可读性和生产力,同时减少样板代码。更重要的是,Groovy map 可以与现有的 Java 代码无缝集成,并且它们的动态类型和对特定用例(如配置文件和数据管道)的适用性巩固了它们作为在基于 Java 的项目中处理数据结构的强大工具的价值。(如果您尚未安装 Groovy,请阅读本系列的介绍。)
在我的上一篇文章中,我探讨了 Java 和 Groovy 中 list 的增强功能和差异。我还声称“list 是 Groovy 开始变得比 java.util.List
接口提供的基本功能更有趣的地方。” 我还声称 Groovy 中的 map 在扩展和重新解释 java.util.Map
的方式上甚至更有趣。 让我们回顾一下 map 的一些基本概念,然后看看 Groovy 对它们的支持。
首先,Java(和 Groovy)使用术语“map”来表示一种事物与另一种事物之间关系的定义。因此,在 Java 程序中,您会看到诸如 Map<Integer,String>
或 Map<String,MyClass>
,甚至 Map<Integer,Map<String,MyClass>>
之类的声明。所有这些都采用以下形式
Map<Key,Value>
这表明 Key
是键的类(或类型),即被查找的事物。 Value
是与每个键关联的值的类(或类型)。
学习 Groovy 中的 Map
的一个好的起点是查看一些基本的语法元素。这里有一些,类似于本系列前一篇文章中对 List
的介绍
1 def testMap = [:]
2 testMap[1] = 1
3 testMap[2] = "Hi there"
4 testMap[3] = java.time.LocalDate.of(2023,03,01)
5 testMap[15] = 2
6 testMap[-2] = 1.5
7 testMap.end = [3,4,5]
8 println "testMap $testMap"
9 println "testMap.class ${testMap.class}"
10 println "testMap.containsKey(2) ${testMap.containsKey(2)}"
第 1 行定义了变量 testMap
并将其设置为空 map。
第 2-4 行在第一到第三行的键处插入了 int
(Integer
)、String
和 java.time.LocalDate
类型的值,表明 Groovy 对特定类型的 map 值是不可知的。现在是提及 testMap[1] = 1
等同于 testMap.put(1,1)
的好时机。
第五行在键 15 处插入了 int
(Integer
) 值 2。与 list 不同,这没有任何“中间插入”副作用,因为 map 没有“缺少键”的概念。
第六行使用了一个负键,与 Groovy list 不同,它不会被解释为“从右边开始”,而是插入了十进制 (BigDecimal
) 值 1.5。
第七行使用点表示法而不是下标来插入值 [3, 4, 5]
(一个 List
)。这种表示法 testMap.end
等同于 testMap["end"]
,也等同于 testMap.get("end")
。
第八行打印出 list,第九行告诉你你创建了什么样的“野兽”。
第 10 行向您展示了 containsKey()
方法的用法,以找出 map 中是否定义了特定的键。回想一下,in
运算符用于 List
实例中,以确定 list 中是否存在这样的值。在 Map
实例上使用 in
几乎总是错误的,因为它在被测试的值是键或值时都将返回 true
。
运行此脚本会产生
$ groovy Groovy13a.groovy
testMap [1:1, 2:Hi there, 3:2023-03-01, 15:2, -2:1.5, end:[3, 4, 5]]
testMap.class null
testMap.containsKey(2) true
您应该注意,map 条目是按照插入顺序排列的,而不是按照某种键排序顺序排列的。这是因为 [:]
创建了 LinkedHashMap
的实例。 还要注意,testMap.class
产生了令人惊讶的 null
结果。 有什么猜测吗? 如果我提到 testMap.end
等同于 testMap["end"]
,也等同于 testMap.get("end")
,这会有帮助吗? 要找出 map 的类,您需要改为使用:直接调用 getClass()
。 这将告诉您该 map 是 java.util.LinkedHashMap
的实例。
与 List
接口的情况一样,Groovy 向 Map
接口添加了许多方法。 需要记住的重要一点是,其中一些方法会改变 map,而另一些方法则会从旧方法创建一个新 map。 发生这种情况是因为修改应用于这些方法。
例如,plus()
(可以通过 +
运算符访问)创建一个新 map,它就像两个 map 的“并集”:保留键的顺序。 使用 minus()
生成一个新的 map,显示两个 map 之间的“差异”。 这是通过从第一个 map 中删除第二个 map 的所有元素来实现的
1 def m1 = [1:1, 2:2, 3:3]
2 def m2 = [4:4, 5:5]
3 def m3 = [1:6, 6:6]
4 println "m1 $m1"
5 println "m2 $m2"
6 println "m3 $m3"
7 println "m1 + m2 ${m1 + m2}"
8 println "m1 + m3 ${m1 + m3}"
9 println "m1 - m2 ${m1 - m2}"
10 println "m1 - m3 ${m1 - m3}"
第一到第三行创建了三个 map m1
、m2
和 m3
。 请注意,m1
的第一个键等于 m3
的第一个键,但它们的值不同。
第 4-6 行打印出这些 map:没什么好惊讶的。
第 7-8 行说明了 +
运算符(即 plus()
方法)创建两个新 map,一个 map 是 m1
和 m2
的“并集”,另一个是 m1
和 m3
的“并集”。
第 9-10 行说明了 -
运算符(即 minus()
方法)创建两个 map 之间的“差异”。
让我们看看运行它时会发生什么
$ groovy Groovy13b.groovy
m1 [1:1, 2:2, 3:3]
m2 [4:4, 5:5]
m3 [1:6, 6:6]
m1 + m2 [1:1, 2:2, 3:3, 4:4, 5:5]
m1 + m3 [1:6, 2:2, 3:3, 6:6]
m1 - m2 [1:1, 2:2, 3:3]
m1 - m3 [1:1, 2:2, 3:3]
我想指出几件事。
首先,“并集”的概念需要在“集合并集”含义的上下文中考虑。 Map 键形成一个集合——map 中没有重复的键。 因此,m1
和 m3
的并集导致 m3
中键 1 的值替换 m1
中键 1 的值。 这是有道理的。
其次,m1
和 m3
之间的差异并没有从 m1
中消除条目 1:1。 Java(以及因此 Groovy),Map
的实例包含 MapEntry
的实例。 看起来 minus()
——差集运算符——仅从第一个 map 中删除等于第二个 map 中 MapEntry
的 MapEntry
。 回到并集操作,您可以推断 m1
中的 MapEntry
1:1 被 m3
中的 MapEntry
1:6 替换,因为键是相等的。
Groovy 为 Map
定义的其他一些有趣的方法包括
m1.asImmutable()
:创建m1
的不可变副本(与asUnmodifiable()
相同)m1.drop(n)
:创建m1
的副本,但不包含前n
个MapEntry
m1.take(n)
:创建m1
的副本,仅包含前n
个MapEntry
实例;m1.intersect(m2)
:创建一个新 map,其中包含在m1
和m2
中都找到的MapEntry
实例m1.sort()
:按键的顺序重新排序m1
(请注意,当键的类型不同时,这可能会产生一些奇怪的结果)m1.toSorted()
:创建m1
的副本,并按键的顺序排序
与 List
一样,请注意,有些方法会生成 Map
实例的修改副本。 有些方法会更改 Map
实例本身。 事后看来,应该有一个一致的命名约定来引起人们对这种差异的注意。
添加到 Map
接口的许多很酷的方法都接受 Closure
参数,以便对 list 执行有趣的操作。 您将在即将发布的关于 closure 的文章中看到其中一些内容。
现在是建议转到 Groovy Map 接口参考描述 以及阅读 Groovy 关于 Map 的文档 的好时机。
结论
Groovy 将平庸的 Java Map
实现提升到了卓越的水平。 它通过添加许多有用的方法并提供运算符和其他语法支持来实现这一点,这些运算符和其他语法支持以更易读和紧凑的方式使用其中一些常用方法。
与 Java 的一个主要区别是 Groovy 不会从 Map
实例应包含相同类型元素这一角度进行操作。 在未启用类型检查的情况下,Groovy 不会强制执行元素类型。 例如
1 def rn = new LinkedHashMap<Integer,String>()
2 rn[1] = "i"
3 rn[2] = "ii"
4 rn[3] = "iii"
5 rn["4"] = "iv"
6 println rn
尽管存在字符串键 “4”,但这仍然可以很好地编译和执行
$ groovy Groovy13c.groovy
[1:i, 2:ii, 3:iii, 4:iv]
结合点语法代替括号的语法使用,这其中的含义非常有趣。 Groovy 中 Map
的一种用法就像“动态结构”,取代了类定义的用法。 这对某些读者来说可能看起来有点难看。 但在正确的上下文中,它可以生成紧凑且更具可读性的代码,而无需周围数十行额外的类定义(甚至 Java 的新 record 定义)。
请继续关注下一个关于如何使用 Groovy closure 的教程。