1. 介绍
本篇内容为Groovy学习笔记第二十五篇。主要内容为Groovy孵化功能(incubating)的学习。
孵化功能是还没有确定的可以理解为正在开发中的一些功能。在之后的版本迭代过程中可能孵化失败,可能会有重大修改。
2. 记录类-关键字 record
记录类,或者简称为记录,是一种特殊的类,用于对普通数据聚合建模。它们提供了比普通类更紧凑的语法和更少的代码。Groovy已经有了@Immutable
和@Canonical
这样的AST转换,它们已经显著地减少了代码,但是记录已经在Java中引入,并且Groovy中的记录类被设计成与Java记录类保持一致。使用关键字为:record
。
假设我们想要创建一个表示电子邮件消息的Message
记录。出于本例的目的,让我们简化这样的消息,只包含一个发电子邮件地址、一个到电子邮件地址和一个消息体。我们可以如下定义这样的记录:
record Message(String from, String to, String body) { } //使用record 关键字,创建了一个记录类
我们将像使用普通类一样使用record
类,如下所示:
def msg = new Message('zin@zinyan.com', 'yan@zinyan.com', 'Hello!')
println(msg) //输出:Message[from=zin@zinyan.com, to=yan@zinyan.com, body=Hello!]
简化的代码使我们不用定义显式字段如:getter
和toString
、equals
和hashCode
方法。事实上,它是以下大致等价物的简写:
final class Message extends Record {
private final String from
private final String to
private final String body
private static final long serialVersionUID = 0
/* constructor(s) */
final String toString() { /*...*/ }
final boolean equals(Object other) { /*...*/ }
final int hashCode() { /*...*/ }
String from() { from }
// other getters ...
}
请注意记录获取器的特殊命名约定。它们与字段的名称相同(而不是常用的JavaBean惯例,即用“get
”前缀大写)。术语组件通常用于记录,而不是指记录的字段或属性。因此,我们的Message
记录包含from
、to
和body
组件。
像在Java中一样,我们可以通过编写自己的代码来重写通常隐式提供的方法:
代码语言:javascript复制record Point3D(int x, int y, int z) {
String toString() {
"Point3D[coords=$x,$y,$z]"
}
}
println(new Point3D(10,20,30)) //输出:Point3D[coords=10,20,30]
还可以以正常方式将泛型与记录一起使用。例如,考虑以下Coord
记录定义:
//创建了一个Coord的记录类,并且使用了继承Number的抽象类进行入参。
record Coord<T extends Number>(T v1, T v2){
double distFromOrigin() {
//Math.sqrt 是个开平方公式 下面的代码内容为:v1的平方 v2的平方的和进行开方运算
Math.sqrt(v1()**2 v2()**2 as double)
}
}
def r1 = new Coord<Integer>(3, 4)
println(r1.distFromOrigin()) //输出:5.0
def r2 = new Coord<Double>(6d, 2.5d)
println(r2.distFromOrigin()) //输出:6.5
2.1 特性
记录具有两个明显的特性:
- 隐式构造函数。上面的示例中我们就可以直接省略构造函数的创建。
- 可序列化
记录具有隐式构造函数。这可以通过提供自己的构造函数以正常方式重写-如果这样做,需要确保设置了所有字段。然而,为了简洁起见,在省略了普通构造函数的参数声明部分的情况下,可以使用紧凑的构造函数语法。对于这种特殊情况,仍然提供了正常的隐式构造函数,但通过紧凑构造函数定义中提供的语句进行了扩充:
代码语言:javascript复制public record Warning(String message) {
public Warning { //创建一个紧凑的构造函数
Objects.requireNonNull(message) //判断message是否为空
message = message.toUpperCase() //将message 全部转换为大小字母
}
}
def w = new Warning('Hi zinyan.com')
println w.message() //输出:HI ZINYAN.COM
类似上面的示例,根本不用在构造方法中填写入参。
还有一个特性就是:可序列化。Groovy本地化代码遵循适用于Java记录的特殊可序列化约定。Groovy类记录类遵循正常的Java类可序列化约定。
2.2 @RecordType
在上面我们创建了一个Message记录,并大致使用了它的方法。也介绍了它和Java中的记录类保持一致。
ps:java是在14的时候添加了record的预览版本经过14,15,16三个大版本的迭代,在17的时候正式上线。
事实上,Groovy经历了一个中间阶段,其中记录关键字被class
关键字和附带的@RecordType
注解替换:
原先的Groovy的记录类是通过以下方法创建的:
代码语言:javascript复制import groovy.transform.RecordType
@RecordType
class Message {
String from
String to
String body
}
之后@RecordType被改为元注解,并扩展为其组成子注解,如@TupleConstructor
、@POJO
、@RecordBase
等。在某种意义上,这是一个经常被忽略的实现细节。但是,如果您希望自定义或配置记录实现,我们可能希望返回@RecordType
样式或使用一个组成子注释来扩充记录类。
2.3 Groovy的增强
上面介绍的信息,和java中的记录类可以说差不多,共性也是一样的。而有别于Java的记录类,Groovy提供了部分的功能增强。
2.3.1 参数默认值
Groovy支持构造函数参数的默认值。此功能也适用于以下记录定义中所示的记录。例如:
代码语言:javascript复制record ColoredPoint(int x, int y = 0, String color = 'white') {}
在定义ColoredPoint
记录类的时候,给y
值和color
值添加了默认值。
不使用时的参数(从右侧删除一个或多个参数)将替换为默认值,如下例所示:
代码语言:javascript复制def x = new ColoredPoint(5, 5, 'black')
//因为记录类自动重写了toString方法,所以我们可以直接打印
println x //输出 :ColoredPoint[x=5, y=5, color=black]
def i = new ColoredPoint(5, 5)
println i //输出 :ColoredPoint[x=5, y=5, color=white]
def n=new ColoredPoint(5,'green') //非法传值
println n //groovy.lang.GroovyRuntimeException: Could not find matching constructor for:
def y=new ColoredPoint(5)
println y //ColoredPoint[x=5, y=0, color=white]
该处理遵循常规Groovy约定的构造函数默认参数,本质上自动为构造函数提供以下方法:
代码语言:javascript复制ColoredPoint(int, int, String)
ColoredPoint(int, int)
ColoredPoint(int)
PS:所以
new ColoredPoint(5,'green')
的写法就会触发异常
也可以使用命名参数(默认值也适用于此处):
代码语言:javascript复制def n=new ColoredPoint(x:5,color:'green') //使用命名参数就不会出现异常了
println n //输出:ColoredPoint[x=5, y=0, color=green]
也可以禁用默认参数处理,如下所示:
代码语言:javascript复制import groovy.transform.TupleConstructor
import groovy.transform.DefaultsMode
@TupleConstructor(defaultsMode=DefaultsMode.OFF)
record ColoredPoint2(int x, int y, String color) {}
def demo = new ColoredPoint2(4, 5, 'red')
println demo //输出 ColoredPoint2[x=4, y=5, color=red]
这将根据Java的默认值生成一个构造函数。我们如果创建对象的时候传值缺少参数就会出现错误。示例:
代码语言:javascript复制def demo = new ColoredPoint2( 5, 'red')
println demo
将会输出:
代码语言:javascript复制Caught: groovy.lang.GroovyRuntimeException: Could not find matching constructor for: ColoredPoint2(Integer, String)
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: ColoredPoint2(Integer, String)
at zinyan.run(DSJ.groovy:6)
可以强制所有属性具有默认值,如下所示:
代码语言:javascript复制import groovy.transform.TupleConstructor
import groovy.transform.DefaultsMode
@TupleConstructor(defaultsMode=DefaultsMode.ON)
record ColoredPoint3(int x, int y = 0, String color = 'white') {}
def demo = new ColoredPoint3(y: 5)
println demo //输出 ColoredPoint3[x=0, y=5, color=white]
def demo1 = new ColoredPoint3(5)
println demo1 //输出 ColoredPoint3[x=5, y=0, color=white]
任何没有显式初始值的属性/字段都将被赋予参数类型的默认值(null,或为零/假)。
2.3.2 声明式toString
方法自定义
根据Java,我们可以通过编写自己的方法自定义记录的toString
方法。如果喜欢更具声明性的样式,也可以使用Groovy的@ToString
转换来覆盖默认记录类的toString
。例如:
import groovy.transform.ToString
// leftDelimiter左边分隔符,rightDelimiter 右边分隔符,过滤掉null的值,includeNames是否显示名称,nameValueSeparator 名称和参数的分隔符
@ToString(ignoreNulls=true, cache=false, includeNames=true,
leftDelimiter='{', rightDelimiter='}', nameValueSeparator=':')
record Point(Integer x, Integer y, Integer z=null) { }
println new Point(1024,2048) // 输出 :Point{x:1024, y:2048}
@ToString 还有更多的一些参数。这里不展开详细介绍了。想了解更多的方法可以通过:https://docs.groovy-lang.org/docs/latest/html/gapi/ 查找相关API。
2.3.3 获取记录组件值数据以及相关方法的禁用
我们可以从记录中获取组件值,列表如下:
代码语言:javascript复制//创建一个Point 记录对象
record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
def (x, y, c) = p.toList()
assert x == 100
assert y == 200
assert c == 'green'
我们也可以使用@RecordOptions(toList=false)
禁用此功能。示例如下:
import groovy.transform.RecordOptions
@RecordOptions(toList=false)
record Point(int x, int y, String color) { }
默认情况下,我们可以通过toMap获取map数据列表。示例如下:
代码语言:javascript复制record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
assert p.toMap() == [x: 100, y: 200, color: 'green']
可以通过:@RecordOptions(toMap=false)
禁止掉toMap方法。
除此外,还有size
,mode
,getAt
,copyWith
,components
等参数值,输入值都是boolean。示例:
@RecordOptions(toList=false,toMap=false,size=false,mode=false,getAt=false,copyWith=false,components=false)
@RecordOptions
除了在记录类中使用,在Groovy的其他类中也是可以使用的。
唯一需要注意的就是,@RecordOptions
是Groovy SDK 4.0.0
版本才提供的注解对象。在低版本中没有。
例如getAt方法的作用:
代码语言:javascript复制record Point(int x, int y, String color) { }
def p = new Point(100, 200, 'green')
assert p[1] == 200
如果禁用getAt
方法。那么就不能直接使用p[1]
进行获取数据了。
其他的几种禁用方式都大同小异。
2.4 Groovy的可选功能
默认情况下,可选功能都是关闭状态。
2.4.1 复制-Copying
在某些组件已更改的情况下制作记录的副本可能很有用。这可以使用可选的copyWith
方法完成,该方法接受命名参数。根据提供的参数设置记录组件。对于未提及的组件,使用原始记录组件的(浅)副本。
代码语言:javascript复制PS:
copyWith
方法可以通过@RecordOptions(copyWith=true)
开启
import groovy.transform.RecordOptions
@RecordOptions(copyWith=true) //开启copy功能
record Fruit(String name, double price) {}
def zinyan = new Fruit('Zinyan', 1024)
println zinyan //输出 :Fruit[name=Zinyan, price=1024.0]
def Zstudent = zinyan.copyWith(name: 'Z同学') //进行复制操作
println Zstudent //输出 :Fruit[name=Z同学, price=1024.0]
println zinyan //输出:Fruit[name=Zinyan, price=1024.0]
2.4.2 深层不变性
与Java一样,记录在默认情况下提供了浅的不可变性。Groovy的@Immutable
转换为一系列可变数据类型执行防御性复制。记录可以利用这种防御性复制获得如下的深度不变性。
简单理解就是,原先java中将参数传进方法或者类中,只是一个浅拷贝。而添加@Immutable
之后,可以将入参进行深拷贝。
两者之间已经不是一个对象了。示例如下:
代码语言:javascript复制import groovy.transform.ImmutableProperties
@ImmutableProperties
record Shopping(List items) {}
def items = ['zin', 'yan']
def shop = new Shopping(items)
items << 'com' //集合对象,添加数据
println shop.items() //输出:[zin, yan]
println items // 输出:[zin, yan, com]
这些示例说明了Groovy的记录功能背后的原理,它提供了三个级别的便利性:
- 使用
record
关键字以获得最大的代码简洁性。 - 支持使用声明性注解的低代码定制。
- 在需要完全控制时允许正常方法实现。
也就是说,在使用各种低代码的同时,也支持我们自定义方法实现完全控制。
2.4.3 以类型化元组的形式获取记录的组件
可以以类型化元组的形式获取记录的组件:
通过注解@RecordOptions(components=true)
开启。默认是false状态。
import groovy.transform.*
@RecordOptions(components=true)
record Point(int x, int y, String color) { }
@CompileStatic
def method() {
def p1 = new Point(100, 200, 'green')
def (int x1, int y1, String c1) = p1.components()
//如果不了解Groovy中字符串拼接逻辑可以通过:https://zinyan.com/?p=388 了解
println "x1=$x1,y1=$y1,c1=$c1" //输出:x1=100,y1=200,c1=green
def p2 = new Point(10, 20, 'blue')
def (x2, y2, c2) = p2.components()
//** 代表了幂运算 下面示例中就是20的2次方
println "参数1=${x2*10}, 参数2=${y2**2} ,参数3=${c2.toUpperCase()}" //输出:参数1=100, 参数2=400 ,参数3=BLUE
def p3 = new Point(1, 2, 'red')
println "${p3.components() instanceof Tuple3}" //输出:ture
}
method()
Groovy的TupleN
类数量有限。如果记录中有大量组件,则可能无法使用此功能。
Tuple从0-16 一共17个对象。所以几乎大部分情况下都能够满足。通常情况下应该不可能有那么多的值
2.5 与Java的区别
上面介绍过,java也有Redord 记录类。
Groovy支持创建类似记录的类以及本地记录。类似记录的类不会扩展Java的Record
类,Java不会将此类类视为记录,但在其他方面会具有类似的属性。
@RecordOptions
注解(@RecordType
的一部分)支持mode
注解属性,该属性可以采用三个值之一(默认值为AUTO
):
- NATIVE:生成类似于Java的类。在早于JDK16的JDK上编译时产生错误。
- EMULATE:为所有JDK版本生成类似记录的类。
- AUTO:为JDK16 生成一个本机记录,并以其他方式模拟该记录。
使用record关键字还是@RecordType
注解与模式无关。
模式使用示例为:
代码语言:javascript复制import groovy.transform.RecordTypeMode
import groovy.transform.RecordOptions
@RecordOptions(mode = RecordTypeMode.NATIVE)
record Point(int x, int y, String color) { }
3. 密封-sealed关键字
密封类、接口和特性(traits)限制了哪些子类可以扩展/实现它们。在密封类出现之前,类层次结构设计者有两个主要选择:
- 设置一个类关键字为
final
不允许扩展。 - 将类设为
public
和非final
,以允许任何人扩展。
与这些要么全有要么全无的选择相比,密封类提供了一个中间地带。
密封类也比以前用于实现中间地带的其他技巧更灵活。例如,对于类层次结构,访问修饰符(如protected
和package privat
e)提供了一些限制继承层次结构的能力,但通常以灵活使用这些层次结构为代价。
密封类的层次结构在已知的类、接口和特性(traits)的层次结构中提供完整的继承,但在层次结构之外禁用或只提供受控的继承。
例如,假设我们要创建一个仅包含圆和正方形的形状层次。我们还希望形状界面能够引用层次结构中的实例。我们可以创建如下层次结构:
代码语言:javascript复制sealed interface ShapeI permits Circle,Square { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }
密封关键字:
sealed
..permits
上面就是创建了一个密封接口,这个必须Groovy SDK 4.0.0版本才能支持。
Groovy还支持另一种注释语法。
代码语言:javascript复制import groovy.transform.Sealed
//创建了一个密封接口,该接口必须是Circle 或者Square的才能使用
@Sealed(permittedSubclasses=[Circle,Square]) interface ShapeI { }
final class Circle implements ShapeI { }
final class Square implements ShapeI { }
有关于permittedSubclasses关键字的作用我在前面的traits中有进行过介绍,可以参考https://zinyan.com/?p=455
我们可以有一个ShapeI
类型的引用,由于permits
,它可以指向Circle
或Square
,因为我们的类是final
类,所以我们知道将来不会在我们的层次结构中添加其他类。至少在不更改permits
和重新编译的情况下不会。
通常,我们可能希望像这里这样立即锁定类层次结构的某些部分,在这里我们将子类标记为final
,但其他时候我们可能希望允许进一步的受控继承。
示例:
代码语言:javascript复制//创建一个密封类,仅仅许可Circle,Polygon,Rectangle这三个子类
sealed class Shape permits Circle,Polygon,Rectangle { }
//创建final 的Circle 类,继承 Shape
final class Circle extends Shape { }
//创建一个非密封的类,继承Polygon
class Polygon extends Shape { }
//创建一个非密封的类RegularPolygon
non-sealed class RegularPolygon extends Polygon { }
//创建一个不可变的类Hexagon 继承Polygon类
final class Hexagon extends Polygon { }
//创建一个密封类,继承Shape 仅许可Square类
sealed class Rectangle extends Shape permits Square{ }
//创建一个Square静态类,继承Rectangle
final class Square extends Rectangle { }
代码语言:javascript复制PS:如果是想在低版本中使用密封,但是没有sealed和non-sealed关键字,可以使用
@NonSealed
和@Sealed
代替
import groovy.transform.Sealed
import groovy.transform.NonSealed
@Sealed(permittedSubclasses=[Circle,Polygon,Rectangle]) class Shape { }
final class Circle extends Shape { }
class Polygon extends Shape { }
@NonSealed class RegularPolygon extends Polygon { }
final class Hexagon extends Polygon { }
@Sealed(permittedSubclasses=Square) class Rectangle extends Shape { }
final class Square extends Rectangle { }
下面介绍一下上面的继承逻辑和密封效果。
在本例中,我们允许的Shape
子类是Circle
、Polygon
和Rectangle
。
而Circle类是final标识的。所以它不能进行扩展。
Polygon
隐式声明是非密封的(没有声明默认就是非密封),RegularPolygon
显式标记为非密封(使用关键字non-sealed
或者@NonSealed
)这意味着我们的层次结构可以通过子类进一步扩展。
例如:Polygon
→RegularPolygon
和RegularPolygon
→ Hexagon
所示。
然后创建的Rectangle是密封的,只允许一种控制的方法进行扩展就是Square
。
密封类用于创建类似枚举的相关类,这些类需要包含特定于实例的数据。示例如下,创建关于天气的三个枚举对象:
代码语言:javascript复制//创建了一个枚举对象Weather
enum Weather { Rainy, Cloudy, Sunny }
//初始化集合对象,插入了三个枚举值
def forecast = [Weather.Rainy, Weather.Sunny, Weather.Cloudy]
println forecast //输出:[Rainy, Sunny, Cloudy]
但我们现在希望在天气预报中添加特定于天气的实例数据。我们可以这样改变我们的抽象:
代码语言:javascript复制import groovy.transform.Immutable
//创建一个抽象的密封类 Weather
sealed abstract class Weather { }
//创建三个实体类 继承该密封抽象类
@Immutable(includeNames=true)
class Rainy extends Weather {
Integer expectedRainfall //定义自己的属性
}
@Immutable(includeNames=true)
class Sunny extends Weather {
Integer expectedTemp //定义自己的属性
}
@Immutable(includeNames=true)
class Cloudy extends Weather {
Integer expectedUV //定义自己的属性
}
def forecast = [new Rainy(12), new Sunny(35), new Cloudy(6)]
println forecast //输出:[Rainy(expectedRainfall:12), Sunny(expectedTemp:35), Cloudy(expectedUV:6)]
密封层次结构在指定整数或抽象数据类型(ADT)时也很有用,如下例所示:
代码语言:javascript复制import groovy.transform.Immutable
import groovy.transform.Canonical
//创建一个密封接口 Tree ,同时支持泛型T传参
sealed interface Tree<T> {}
@Singleton //声明单例
final class Empty implements Tree {
String toString() { 'Empty' }
}
//创建一个Canonical声明的final类Node
@Canonical
final class Node<T> implements Tree<T> {
T value
Tree<T> left, right
}
//创建一个类
Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
println tree //输出 :Node(42, Node(0, Empty, Empty), Empty)
密封的层次结构可以很好地处理记录,如以下示例所示:
代码语言:javascript复制//创建一个密封接口Expr
sealed interface Expr {}
//创建一个记录类ConstExpr,扩展了Expr接口
record ConstExpr(int i) implements Expr {}
//创建了一个记录类PlusExpr,扩展了Expr接口
record PlusExpr(Expr e1, Expr e2) implements Expr {}
//创建了一个记录类MinusExpr,扩展了Expr接口
record MinusExpr(Expr e1, Expr e2) implements Expr {}
//创建了一个记录类NegExpr,扩展了Expr接口
record NegExpr(Expr e) implements Expr {}
def threePlusNegOne = new PlusExpr(new ConstExpr(3), new NegExpr(new ConstExpr(1)))
println threePlusNegOne //输出:PlusExpr[e1=ConstExpr[i=3], e2=NegExpr[e=ConstExpr[i=1]]]
3.1 和java的区别
- Java没有为密封类的子类提供默认修饰符,并要求指定final、密封或非密封中的一个。Groovy默认为非密封的,但是如果您愿意,仍然可以使用
non-sealed
或@NonSealed
。我们预计样式检查工具CodeNarc最终会有一个规则来查找非密封的存在,所以想要更严格风格的开发人员可以使用CodeNarc和该规则。 - 目前,Groovy不会检查
permittedSubclasses
中提到的所有类是否在编译时可用,并与基密封类一起编译。这可能会在Groovy的未来版本中改变。
Groovy支持注解方式创建密封类和接口,也支持使用sealed
关键字创建密封类和接口。
@SealedOptions
注解支持一个mode
注解属性,它可以接受以下参数:(AUTO是默认值)
NATIVE
:产生一个类似于Java的类。在JDK17之前的jdk上编译时产生错误。EMULATE
:指示使用@Sealed
注解对类进行密封。这种机制适用于JDK8 的Groovy编译器,但不能被Java编译器识别。AUTO
:为JDK17 生成一个本机记录,并以其他方式模拟该记录。
使用sealed
关键字还是@Sealed
注解与mode
无关。示例如下:
import groovy.transform.SealedMode
import groovy.transform.RecordOptions
@SealedOptions(mode = SealedMode.NATIVE)
sealed interface Expr {}
PS:
@SealedOptions
注解最低版本Groovy SDK 4.0.0。更低版本中没有该注解
4. 小结
本篇学习了记录类record 和密封 sealed的使用。该功能从4.0.0版本开始孵化。现在最新Groovy版本为4.0.6版本。
所以,之后版本可能还是会有一些变化的。但是整体的功能框架已经确定了。改动应该不会过多。
下一篇开始介绍Groovy的闭包吧。如果觉得本篇内容介绍的还算清晰,希望能够给我点个赞。谢谢。
本篇内容参考与Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_record_classes_incubating 。