Groovy类别
1. 概述
有时我们可能想知道是否可以向编译后的 Java 或 Groovy 类添加一些额外的方便方法,而我们无法修改源代码。事实证明,Groovy 类别可以让我们做到这一点。
Groovy 是一种动态且功能强大的 JVM 语言,具有许多元编程 功能。
在本教程中,我们将探讨 Groovy 中类别的概念。
2. 什么是类别?
类别是一种元编程功能,受 Objective-C 的启发,它允许我们向新的或现有的 Java 或 Groovy 类添加额外的功能。
与extensions 不同,默认情况下不启用类别提供的附加功能。因此,启用类别的关键是使用代码块。
类别实现的额外功能只能在使用代码块内访问。
3. Groovy 中的类别
让我们讨论一些在 Groovy 开发工具包中已经可用的重要类别。
3.1. TimeCategory
TimeCategory 类在groovy.time包中可用,它添加了一些方便的方法来处理Date和Time对象。
此类别添加了将Integer转换为时间符号的功能,如秒、分钟、天和月。
此外,TimeCategory类还提供了诸如plus和minus之类的方法,分别用于轻松地将Duration添加到Date对象和从Date对象中减去Duration**。
让我们检查一下TimeCategory类提供的一些方便的功能。对于这些示例,我们将首先创建一个Date对象,然后使用TimeCategory执行一些操作:
def jan_1_2019 = new Date("01/01/2019")
use (TimeCategory) {
assert jan_1_2019 + 10.seconds == new Date("01/01/2019 00:00:10")
assert jan_1_2019 + 20.minutes == new Date("01/01/2019 00:20:00")
assert jan_1_2019 - 1.day == new Date("12/31/2018")
assert jan_1_2019 - 2.months == new Date("11/01/2018")
}
让我们详细讨论代码。
此处,10.seconds创建值为 10 秒的TimeDuration 对象。并且,加号 (+) 运算符将TimeDuration对象添加到Date对象。
同样,1.day创建值为 1 天的Duration 对象。并且,减号 (-) 运算符从Date对象中减去Duration对象。
此外,TimeCategory类还提供一些方法,例如now、ago和from,它允许创建相对日期。
例如,5.days.from.now将创建一个Date对象,其值比当前日期早 5 天。同样,2.hours.ago设置当前时间之前 2 小时的值。
让我们看看它们的实际效果。此外,我们将使用SimpleDateFormat在比较两个相似的Date对象时忽略时间的界限:
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy")
use (TimeCategory) {
assert sdf.format(5.days.from.now) == sdf.format(new Date() + 5.days)
sdf = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
assert sdf.format(10.minutes.from.now) == sdf.format(new Date() + 10.minutes)
assert sdf.format(2.hours.ago) == sdf.format(new Date() - 2.hours)
}
因此,使用TimeCategory类,我们可以使用我们已知的类编写简单且更易读的代码。
3.2. DOM类别
DOMCategory 类在groovy.xml.dom包中可用。它提供了一些方便的方法来处理 Java 的 DOM 对象。
更具体地说,DOMCategory允许对 DOM 元素进行 GPath 操作,从而可以更轻松地遍历和处理 XML。
首先,让我们编写一个简单的 XML 文本并使用DOMBuilder类解析它:
def blogdemoArticlesText = """
<articles>
<article core-java="true">
<title>An Intro to the Java Debug Interface (JDI)</title>
<desc>A quick and practical overview of Java Debug Interface.</desc>
</article>
<article core-java="false">
<title>A Quick Guide to Working with Web Services in Groovy</title>
<desc>Learn how to work with Web Services in Groovy.</desc>
</article>
</articles>
"""
def blogdemoArticlesDom = DOMBuilder.newInstance().parseText(blogdemoArticlesText)
def root = blogdemoArticlesDom.documentElement
在这里,root对象包含 DOM 的所有子节点。让我们使用DOMCategory类遍历这些节点:
use (DOMCategory) {
assert root.article.size() == 2
def articles = root.article
assert articles[0].title.text() == "An Intro to the Java Debug Interface (JDI)"
assert articles[1].desc.text() == "Learn how to work with Web Services in Groovy."
}
在这里,DOMCategory类允许使用*GPath 提供的**点操作轻松访问节点和元素。此外,它还提供了size和text*等方法来访问任何节点或元素的信息**。
现在,让我们使用DOMCategory将一个新节点附加到根DOM 对象:
use (DOMCategory) {
def articleNode3 = root.appendNode(new QName("article"), ["core-java": "false"])
articleNode3.appendNode("title", "Metaprogramming in Groovy")
articleNode3.appendNode("desc", "Explore the concept of metaprogramming in Groovy")
assert root.article.size() == 3
assert root.article[2].title.text() == "Metaprogramming in Groovy"
}
同样,DOMCategory类也包含一些方法,如appendNode和setValue来修改 DOM**。
4. 创建一个类别
现在我们已经看到了几个实际的 Groovy 类别,让我们探讨如何创建自定义类别。
4.1. 使用自身对象
类别类必须遵循某些惯例才能实现附加功能。
首先,添加附加功能的方法应该是static。其次,该方法的第一个参数应该是这个新特性适用的对象。
让我们将capitalize功能添加到String类。这将简单地将字符串的第一个字母更改为大写。
首先,我们将使用static方法capitalize和String类型作为第一个参数来编写BlogdemoCategory类:
class BlogdemoCategory {
public static String capitalize(String self) {
String capitalizedStr = self;
if (self.size() > 0) {
capitalizedStr = self.substring(0, 1).toUpperCase() + self.substring(1);
}
return capitalizedStr
}
}
接下来,让我们编写一个快速测试以启用BlogdemoCategory并验证String对象的capitalize功能:
use (BlogdemoCategory) {
assert "norman".capitalize() == "Norman"
}
同样,让我们编写一个功能来提高一个数的另一个数的幂:
public static double toThePower(Number self, Number exponent) {
return Math.pow(self, exponent);
}
最后,让我们测试一下我们的自定义类别:
use (BlogdemoCategory) {
assert 50.toThePower(2) == 2500
assert 2.4.toThePower(4) == 33.1776
}
4.2. @Category注解
我们还可以使用*@groovy.lang.Category*注解将类别声明为实例样式类。使用注释时,我们必须提供我们的类别适用的类名。
可以在方法中使用this关键字访问对象的实例。因此,self对象不需要是第一个参数。
让我们编写一个NumberCategory类,并使用*@Category注释将其声明为类别。此外,我们将向我们的新类别添加一些附加功能,例如cube和divideWithRoundUp*:
@Category(Number)
class NumberCategory {
public Number cube() {
return this*this*this
}
public int divideWithRoundUp(BigDecimal divisor, boolean isRoundUp) {
def mathRound = isRoundUp ? BigDecimal.ROUND_UP : BigDecimal.ROUND_DOWN
return (int)new BigDecimal(this).divide(divisor, 0, mathRound)
}
}
在这里,divideWithRoundUp功能将一个数字除以除数,并根据isRoundUp参数将结果向上/向下舍入为下一个或上一个整数。
让我们测试一下我们的新类别:
use (NumberCategory) {
assert 3.cube() == 27
assert 25.divideWithRoundUp(6, true) == 5
assert 120.23.divideWithRoundUp(6.1, true) == 20
assert 150.9.divideWithRoundUp(12.1, false) == 12
}