Groovy™ 记录
发布日期:2023-04-02 08:22PM
编程中常见的一种情况是需要将一组相关属性组合在一起。您可以使用数组、某种形式的元组或映射来组合此类属性。有些语言可能支持像结构体这样的构造。在 Java 中,将此类属性组合成一个类是很自然的。不幸的是,创建这样的类,一旦您添加了所有预期的方法和行为,可能会涉及大量的样板代码。
从 JDK16 开始(从 JDK14 开始预览),Java 引入了 records 作为声明“数据”类的紧凑形式。此类只包含“数据”(几乎没有其他)。Java 选择了保存不可变数据的非常常见的情况。在这种情况下,并遵循一些限制,Java 编译器生成此类的大部分样板变得相对容易。
本博客将探讨 Groovy 的记录实现。Groovy 支持与 Java 相同的功能,但增加了额外的增强和定制。Groovy 的实现建立在现有技术之上,例如编译时元编程(即 AST 转换),这些技术用于减少其他场景中的样板。
简介
首先,让我们看看创建记录的样子
record Point(int x, int y, String color) { }
我们正在分组的属性称为组件。在这种情况下是两个整数 x
和 y
,以及一个字符串 color
。
使用它类似于我们使用传统定义的 Point
类,该类具有与我们的记录定义相同的参数的构造函数
var bluePointAtOrigin = new Point(0, 0, 'Blue')
我们可能需要检查点的一个组件的值
assert bluePointAtOrigin.color() == 'Blue'
我们还可以打印出该点(它调用其 toString()
方法)
println bluePointAtOrigin
这将有以下输出
Point[x=0, y=0, color=Blue]
支持 Java 记录的所有功能。一个例子是紧凑构造函数。如果我们要确保颜色不留空白,我们可以使用紧凑构造函数形式添加检查,给出替代定义,例如
record Point(int x, int y, String color) {
Point { assert !color.blank }
}
更正式地说,记录是一个类,它
-
是隐式 final 的(因此不能被继承)
-
每个组件都有一个私有 final 字段,例如
color
-
每个组件都有一个同名访问器方法,例如
color()
-
有一个默认的
Point(int, int, String)
构造函数 -
有一个默认的
serialVersionUID
为0L
和特殊的序列化代码 -
有隐式的
toString()
、equals()
和hashCode()
方法 -
隐式扩展
java.lang.Record
类(因此不能扩展另一个类,但可以实现一个或多个接口)
可选增强功能
Groovy 记录默认有一个额外的命名参数样式构造函数
var greenPointAtOrigin = new Point(x:0, y:0, color:'Green')
默认情况下,Groovy 记录还会生成 getAt
、size
、toList
和 toMap
方法。getAt
方法提供 Groovy 的常规类数组索引。size
方法返回组件的数量。toList
方法返回组件值。toMap
方法返回组件值以及组件名称。以下是示例
assert bluePointAtOrigin.size() == 3
assert bluePointAtOrigin[2] == 'Blue'
assert bluePointAtOrigin.toList() == [0, 0, 'Blue']
assert bluePointAtOrigin.toMap() == [x:0, y:0, color:'Blue']
getAt
方法还通过多重赋值语句实现解构,如本示例所示
def (x, y, c) = bluePointAtOrigin
assert "$x $y $c" == '0 0 Blue'
很快,我们将看到 copyWith
,它对于从同一类型的另一个记录创建记录很有用。toMap
在从不同类型创建记录时可能很有用,如此处所示。在我们的示例中,我们推测在出版一本书的同一个月,我们可能希望发布一篇关于该书的文章用于营销目的
record Book(String name, String author, YearMonth published) {}
record Article(String name, String author, YearMonth published, String publisher) {}
def b = new Book('Groovy in Action', 'Dierk & Paul', YearMonth.of(2015, 06))
def a = new Article(*:b.toMap(), publisher: 'InfoQ')
如果不需要,可以通过在 RecordOptions
注解上将同名的各种注解属性设置为 false
来关闭这些可选增强功能。
另外两个方法 copyWith
和 components
默认未启用,但可以通过将相应命名的注解属性设置为 true
来启用,如下所示
@RecordOptions(components = true, copyWith = true)
record Point(int x, int y, String color) { }
copyWith
方法可以按如下方式使用
var redPointAtOrigin = bluePointAtOrigin.copyWith(color: 'Red')
assert redPointAtOrigin.toString() == 'Point[x=0, y=0, color=Red]'
这类似于 Kotlin 数据类的 copy
方法。
components
方法返回一个类型化的元组。当启用类型检查时,这特别有用,如下所示
@TypeChecked
String description(Point p) {
p.components().with{ "${v3.toUpperCase()} point at ($v1,$v2)" }
}
请注意,元组中的第三个元素类型为 String
,因此我们可以调用 toUpperCase
方法。
我们可以按如下方式使用此方法
assert description(redPointAtOrigin) == 'RED point at (0,0)'
这相当于 Kotlin 数据类的 componentN
方法。
内部细节
本节中的某些详细信息并非必须知道,但了解如何定制记录定义会很有用。
当我们像这样编写记录声明时
record Point(int x, int y, String color) { }
它等效于以下传统声明
@RecordType
class Point {
int x
int y
String color
}
您几乎永远不会以这种形式编写记录,但是如果您有一些尚未理解记录语法的遗留工具,它可能会很有用。
RecordType
注解是一个元注解(有时也称为注解收集器)。这意味着它是由其他注解组成的注解。不深入细节,本质上,编译器将上述注解扩展为以下内容(并且 RecordBase
进一步调用 ToString
和 EqualsAndHashCode
)
@RecordBase
@RecordOptions
@TupleConstructor(namedVariant = true, force = true, defaultsMode = AUTO)
@PropertyOptions
@KnownImmutable
@POJO
@CompileStatic
class Point {
int x
int y
String color
}
这意味着,如果您不喜欢通常通过记录获得的生成代码,您可以在多个地方以声明式方式更改行为。我们将在下一节介绍。
但要小心,如果您正在创建原生记录并尝试更改某些会违反 JDK 对记录的假设的内容,您很可能会收到编译器错误。
记录的声明式定制
我们之前研究了如何通过使用紧凑构造函数形式来确保不提供空的 color
。我们还有其他几种替代方案可以使用。如果我们要检查 color
不是 null 或空字符串,我们可以使用
@TupleConstructor(pre={ assert color })
record Point(int x, int y, String color) { }
或者,为了排除仅包含空白字符的颜色,并禁用命名参数样式构造函数,我们可以使用
@TupleConstructor(pre={ assert color && !color.blank }, namedVariant=false)
record Point(int x, int y, String color) { }
我们还可以使用声明式样式更改 toString()
方法
@ToString(excludes = 'color', cache = true)
record Point(int x, int y, String color) { }
assert new Point(0, 0, 'Gold').toString() == 'Point(0, 0)'
这里我们将 color
组件从 toString 值中排除,并缓存后续 toString 调用的结果。
模拟记录
Groovy 还为 JDK8+ 提供了模拟记录。模拟记录是类文件中不包含记录属性、不提供特殊记录序列化、也不扩展 java.lang.Record
类,但会遵循所有其他记录约定的类。这意味着即使您仍然停留在 JDK8 或 JDK11 上,您也可以使用 record
简写。
默认情况下,JDK8-15 提供模拟记录,JDK16+ 提供原生记录。您可以使用 RecordOptions
的 mode
注解属性强制编译器始终以模拟或原生记录为目标。如果您指定 NATIVE
模式并且使用的是早期 JDK 或针对早期字节码版本,您将收到编译错误。
将记录与其他 AST 转换结合使用
我们看到,我们可以通过使用构成 RecordType
元注解的注解的变体来定制生成的代码。我们还可以使用 Groovy 中可用的大多数普通 AST 转换。这里只是一些例子
我们之前看到了一个以 Point
作为参数的 description
方法。虽然我们通常希望记录只包含数据,但这种方法放在记录内部是有意义的。我们可以如下操作并利用 Memoized
缓存结果
record Point(int x, int y, String color) {
@Memoized
String description() {
"${color.toUpperCase()} point at ($x,$y)"
}
}
var pinkPointAtOrigin = new Point(x:0, y:0, color:'Pink')
assert pinkPointAtOrigin.description() == 'PINK point at (0,0)'
我们还有另一种通过使用 groovy-contracts
的设计契约功能来检查空白颜色的方法
@Requires({ color && !color.blank })
record Point(int x, int y, String color) { }
我们还可以如下制作易于排序的记录
@Sortable
record Point(int x, int y, String color) { }
var points = [
new Point(0, 100, 'red'),
new Point(10, 10, 'blue'),
new Point(100, 0, 'green'),
]
println points.toSorted(Point.comparatorByX())
println points.toSorted(Point.comparatorByY())
println points.toSorted(Point.comparatorByColor())
其输出如下
[Point[x=0, y=100, color=red], Point[x=10, y=10, color=blue], Point[x=100, y=0, color=green]] [Point[x=100, y=0, color=green], Point[x=10, y=10, color=blue], Point[x=0, y=100, color=red]] [Point[x=10, y=10, color=blue], Point[x=100, y=0, color=green], Point[x=0, y=100, color=red]]
用于减少样板代码的相关功能
尽管记录在减少 Java 世界中的样板代码方面迈进了一大步,但我们应该指出 Groovy 除了记录之外还有许多减少样板代码的功能。Groovy 已经有一个与记录非常相似的功能,即 @Immutable
转换。这提供了记录的大部分样板代码减少,但遵循 JavaBean 约定。
如果您不想要不可变性,您可以使用 @Canonical
,或者您可以混入 @ToString
、@EqualsAndHashCode
、@TupleConstructor
、@MapConstructor
等适当的转换。
以下是主要转换和提供功能的摘要
总结
让我们用功能摘要来结束我们对记录的介绍