1. 运行时元编程
运行时元编程,可以将一些决策(诸如解析、注入甚至合成类和接口的方法)推迟到运行时来完成。为了深入了解 Groovy 的 MOP,我们需要理解 Groovy 的对象以及 Groovy 处理方法。在 Groovy 中,我们主要与三类对象打交道:POJO、POGO,还有 Groovy 拦截器。Groovy 的元编程支持所有类型的对象,但是它们采用的方式却各不相同。
- POJO —— 普通的 Java 对象,它的类可以用 Java 或其他任何 JVM 上的语言来编写。
- POGO —— Groovy 对象,它的类使用 Groovy 编写而成,继承自
java.lang.Object
且默认实现了groovy.lang.GroovyObject
接口。 - Groovy 拦截器 —— 实现了 groovy.lang.GroovyInterceptable 接口的 Groovy 对象,并具有方法拦截功能。稍后将在 GroovyInterceptable 一节中详细介绍。
每当调用一个方法时,Groovy 会判断该方法是 POJO 还是 POGO。对于 POJO 对象,Groovy 会从 groovy.lang.MetaClassRegistry
读取它的 MetaClass
,并委托方法调用;对于 POGO 对象,Groovy 将要采取更多的执行步骤,如下图所示:
图 1 Groovy 拦截机制
1.1 GroovyObject 接口
groovy.lang.GroovyObject 是 Groovy 中的关键接口,地位类似于 Java 中的 Object
类。在 groovy.lang.GroovyObjectSupport 类中有一个 GroovyObject
的默认实现,负责将调用传输给 groovy.lang.MetaClass 对象。 GroovyObject
源看起来如下所示:
package groovy.lang;
public interface GroovyObject {
Object invokeMethod(String name, Object args);
Object getProperty(String propertyName);
void setProperty(String propertyName, Object newValue);
MetaClass getMetaClass();
void setMetaClass(MetaClass metaClass);
}
1.1.1 invokeMethod
根据 运行时元编程 的 Schema,当你所调用的方法没有在 Groovy 对象中提供的时候,调用该方法。下面这个例子中,使用了一个重写的 invokeMethod()
方法:
class SomeGroovyClass {
def invokeMethod(String name, Object args) {
return "called invokeMethod $name $args"
}
def test() {
return 'method exists'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.test() == 'method exists'
assert someGroovyClass.someMethod() == 'called invokeMethod someMethod []'
1.1.2 getProperty
与 setProperty
每次对属性的读取都可以通过重写当前对象的 getProperty()
来拦截,下面是一个简单的例子:
class SomeGroovyClass {
def property1 = 'ha'
def field2 = 'ho'
def field4 = 'hu'
def getField1() {
return 'getHa'
}
def getProperty(String name) {
if (name != 'field3')
return metaClass.getProperty(this, name) // 1⃣️
else
return 'field3'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.field1 == 'getHa'
assert someGroovyClass.field2 == 'ho'
assert someGroovyClass.field3 == 'field3'
assert someGroovyClass.field4 == 'hu'
1.1.3 getMetaClass
和 setMetaClass
可以访问一个对象 metaClass
,或者自定义 MetaClass
实现来改变默认的拦截机制。比如,你可以自己编写 MetaClass
接口的实现,并将它赋予对象,从而改变拦截机制。
// getMetaclass
someObject.metaClass
// setMetaClass
someObject.metaClass = new OwnMetaClassImplementation()
你可以在下文的 GroovyInterceptable 主题中看到更多的范例。
1.2 get/setAttribute
该功能与 MetaClass
实现有关。在默认的实现中,可以不用调用 getter 与 setter 而访问字段。下列例子就反映了这种方法。
class SomeGroovyClass {
def field1 = 'ha'
def field2 = 'ho'
def getField1() {
return 'getHa'
}
}
def someGroovyClass = new SomeGroovyClass()
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field1') == 'ha'
assert someGroovyClass.metaClass.getAttribute(someGroovyClass, 'field2') == 'ho'
class POGO {
private String field
String property1
void setProperty1(String property1) {
this.property1 = "setProperty1"
}
}
def pogo = new POGO()
pogo.metaClass.setAttribute(pogo, 'field', 'ha')
pogo.metaClass.setAttribute(pogo, 'property1', 'ho')
assert pogo.field == 'ha'
assert pogo.property1 == 'ho'
1.3 methodMissing
Groovy 支持 methodMissing
这一概念。该方法与 invokeMethod
的不同之处在于:只有当方法分派失败,找不到指定名称或带有指定实参的方法时,才会调用该方法。
class Foo {
def methodMissing(String name, def args) {
return "this is me"
}
}
assert new Foo().someUnknownMethod(42l) == 'this is me'
通常,在使用 methodMissing
时,可能会将结果缓存起来,以备下次调用同样方法时使用。
比如像下面这样在 GORM 类中的动态查找器。它们是根据 methodMissing
来实现的:
class GORM {
def dynamicMethods = [...] // 一些利用正则表达式的动态方法
def methodMissing(String name, args) {
def method = dynamicMethods.find { it.match(name) }
if(method) {
GORM.metaClass."$name" = { Object[] varArgs ->
method.invoke(delegate, name, varArgs)
}
return method.invoke(delegate,name, args)
}
else throw new MissingMethodException(name, delegate, args)
}
}
注意,假如找到一个调用的方法,就会立刻使用 ExpandoMetaClass
动态地注册一个新方法。这样当下次调用同一方法时就会更方便。使用 methodMissing
,并不会产生像调用 invokeMethod
那么大的开销,第二次调用代价也并不昂贵。
1.4 propertyMissing
Groovy 支持 propertyMissing
的概念,用来拦截失败的属性解析尝试。对 getter 方法而言, propertyMissing
接受一个包含属性名的 String
参数:
class Foo {
def propertyMissing(String name) { name }
}
assert new Foo().boo == 'boo'
当 Groovy 运行时无法找到指定属性的 getter 方法时,才会调用 propertyMissing(String)
方法。
对于 setter 方法,可以添加第二个 propertyMissing
定义来接收一个附加值参数。
class Foo {
def storage = [:]
def propertyMissing(String name, value) { storage[name] = value }
def propertyMissing(String name) { storage[name] }
}
def f = new Foo()
f.foo = "bar"
assert f.foo == "bar"
对于 methodMissing
来说,最佳实践应该是在运行时动态注册新属性,从而改善总体的查找性能。
另外,处理静态方法和属性的 methodMissing
和 propertyMissing
方法可以通过 ExpandoMetaClass 来添加。
1.5 GroovyInterceptable
groovy.lang.GroovyInterceptable 接口是一种标记接口,继承自超接口 GroovyObject
,用于通知 Groovy 运行时通过方法分派器机制时应拦截的方法。
package groovy.lang;
public interface GroovyInterceptable extends GroovyObject {
}
当 Groovy 对象实现了 GroovyInterceptable
接口时,它的 invokeMethod()
方法就会在任何方法调用时调用。
下面就列举一个这种类型的方法:
class Interception implements GroovyInterceptable {
def definedMethod() { }
def invokeMethod(String name, Object args) {
'invokedMethod'
}
}
下面这段代码测试显示,无论方法是否存在,调用方法都将返回同样的值。
class InterceptableTest extends GroovyTestCase {
void testCheckInterception() {
def interception = new Interception()
assert interception.definedMethod() == 'invokedMethod'
assert interception.someMethod() == 'invokedMethod'
}
}
我们不能使用默认的 Groovy 方法(比如 println
),因为这些方法已经被注入到了 Groovy 所有的对象中,自然会被拦截。
如果想要拦截所有的方法调用,但又不想实现 GroovyInterceptable
这个接口,那么我们可以在一个对象的 MetaClass
上实现 invokeMethod()
。该方法同时适于 POGO 与 POJO 对象,如下所示:
class InterceptionThroughMetaClassTest extends GroovyTestCase {
void testPOJOMetaClassInterception() {
String invoking = 'ha'
invoking.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert invoking.length() == 'invoked'
assert invoking.someMethod() == 'invoked'
}
void testPOGOMetaClassInterception() {
Entity entity = new Entity('Hello')
entity.metaClass.invokeMethod = { String name, Object args ->
'invoked'
}
assert entity.build(new Object()) == 'invoked'
assert entity.someMethod() == 'invoked'
}
}
参看 MetaClasses 一节内容了解 MetaClass
的更多内容。
1.6 类别(Categories)
如果一个不受控制的类有额外的方法,在某些情况下反而是有用的。为了实现这种功能,Groovy 从 Objective-C 那里借用并实现了一个概念,叫做: 类别 (Categories)。
类别功能是利用 类别类 (category classes)来实现的。类别类的特殊之处在于,需要遵循特定的预定义规则才能定义扩展方法。
系统已经包括了一些类别,可以为类添加相应功能,从而使它们在 Groovy 环境中更为实用。
类别类默认是不能启用的。要想使用定义在类别类中的方法,必须要使用 GDK 所提供的 use
范围方法,并且可用于每一个 Groovy 对象实例内部。
use(TimeCategory) {
println 1.minute.from.now //1⃣️
println 10.hours.ago
def someDate = new Date() //2⃣️
println someDate - 3.months
}
1⃣️ TimeCategory
为 Integer
添加了方法
2⃣️ TimeCategory
为 Date
添加了方法
use
方法将类别类作为第一个形式参数,将一个闭包代码段作为第二个形式参数。在 Category
中,可以访问类别的任何方法。如上述代码所示,甚至 JDK 的 java.lang.Integer
或 java.util.Date
类都可以通过用户定义方法来丰富与增强。
类别不需要直接暴露给用户代码,如下所示:
class JPACategory{
// 下面让我们无需通过 JSR 委员会的支持来增强 JPA EntityManager
static void persistAll(EntityManager em , Object[] entities) { //添加一个接口保存所有
entities?.each { em.persist(it) }
}
}
def transactionContext = {
EntityManager em, Closure c ->
def tx = em.transaction
try {
tx.begin()
use(JPACategory) {
c()
}
tx.commit()
} catch (e) {
tx.rollback()
} finally {
//清除所有资源
}
}
// 用户代码。他们经常会在出现异常时忘记关闭资源,有些甚至会忘记提交,所以不能指望他们。
EntityManager em; //probably injected
transactionContext (em) {
em.persistAll(obj1, obj2, obj3)
// 在这里制定一些逻辑代码,使范例更合理。
em.persistAll(obj2, obj4, obj6)
}
通过查看 groovy.time.TimeCategory
类,我们就会发现,扩展方法都声明为 static
方法。实际上,要想使类别类的方法能成功地添加到 use
代码段内的类中,这是类别类必须满足的条件之一。
public class TimeCategory {
public static Date plus(final Date date, final BaseDuration duration) {
return duration.plus(date);
}
public static Date minus(final Date date, final BaseDuration duration) {
final Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.YEAR, -duration.getYears());
cal.add(Calendar.MONTH, -duration.getMonths());
cal.add(Calendar.DAY_OF_YEAR, -duration.getDays());
cal.add(Calendar.HOUR_OF_DAY, -duration.getHours());
cal.add(Calendar.MINUTE, -duration.getMinutes());
cal.add(Calendar.SECOND, -duration.getSeconds());
cal.add(Calendar.MILLISECOND, -duration.getMillis());
return cal.getTime();
}
// ...
另外一个必备条件是静态方法的第一个实参必须定义方法一旦被启用时,该方法所连接的类型;而另一个实参则是常见的方法用于形参的实参。
由于形参和静态方法的规范,类别方法定义可能会比普通方法定义稍微不太直观。因此,作为替代方案,Groovy 引入了 @Category
标记,利用这一标记,可在编译时将标注的类转化为类别类。
class Distance {
def number
String toString() { "${number}m" }
}
@Category(Number)
class NumberCategory {
Distance getMeters() {
new Distance(number: this)
}
}
use (NumberCategory) {
assert 42.meters.toString() == '42m'
}
使用 @Category
标记的优点在于,在使用实例方法时,可以不需要把目标类别当做第一个形参。目标类别类作为实参提供给标记使用。
关于 @Category
的另外介绍,可参看 编译时元编程
1.7 MetaClasses
(待定)
1.7.1 自定义 metaclass 类
(待定)
授权 metaclass
(待定)
魔法包(Magic package)
(待定)
1.7.2 每个实例的 metaclass
(待定)
1.7.3 ExpandoMetaClass
Groovy 提供了一种叫做 ExpandoMetaClass
的特殊 MetaClass
。其特殊之处在于,它允许可以使用灵活的闭包语法来动态添加或改变方法、构造函数、属性,甚至静态方法。
对于 测试向导 中所展示的模拟或存根情况,使用这些修改会特别有用。
每一个 Groovy 所提供的 java.lang.Class
都带有一个特殊的 metaClass
属性,它将提供一个 ExpandoMetaClass
实例的引用。该实例可用于添加方法或改变已有方法的行为。
默认情况下, ExpandoMetaClass
不支持继承。为了启用继承,必须在应用程序开始运作前(比如在 main 方法或 servlet bootstrap 中)就调用 ExpandoMetaClass#enableGlobally()
。
下面这些内容详细介绍了 ExpandoMetaClass
在不同情况下的应用。
方法
一旦通过调用 metaClass
属性访问了 ExpandoMetaClass
,就可以通过左移( <<
)或等于号( =
)操作符来添加方法。
注意,左移操作符是用于 追加 (append)一个新的方法。如果方法已经存在,则会抛出一个异常。如果需要 替代 (replace)一个方法,则需要使用 =
操作符。
下例展示了操作符是如何应用于 metaClass
的一个不存在的属性上,从而传入 Closure
代码块的一个实例的。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
上例显示,通过访问 metaClass
属性,可将一个新方法添加到一个类上,并可使用 <<
或 =
操作符来指定一个 Closure
代码块。 Closure
形参被解析为方法形参。形参方法可以通过 {→ …}
格式来添加。
属性
ExpandoMetaClass
支持两种方式来添加或重写属性。
首先,只需通过为 metaClass
赋予一个值,就可以声明一个 可变属性 (mutable property):
class Book {
String title
}
Book.metaClass.author = "Stephen King"
def b = new Book()
assert "Stephen King" == b.author
另一个方式是,通过使用添加实例方法的标准机制来添加 getter 和(或)setter 方法:
class Book {
String title
}
Book.metaClass.getAuthor << {-> "Stephen King" }
def b = new Book()
assert "Stephen King" == b.author
在上述源代码实例中,属性由闭包所指定,并且是一个只读属性。添加一个相等的 setter 方法也是可行的,但属性值需要存储起来以备后续使用。这种做法可以参照下面的例子:
class Book {
String title
}
def properties = Collections.synchronizedMap([:])
Book.metaClass.setAuthor = { String value ->
properties[System.identityHashCode(delegate) + "author"] = value
}
Book.metaClass.getAuthor = {->
properties[System.identityHashCode(delegate) + "author"]
}
但这并不是唯一的办法。比如在一个 servlet 容器中,将当前执行请求中的值当作请求属性保存起来(就像 Grails 中的某些情况一样)。
构造函数
构造函数可以通过特殊的 constructor
属性来添加。 <<
或 =
操作符都可以用于指定 Closure
代码段。当代码在运行时执行时, Closure
实参会成为构造函数的实参。
class Book {
String title
}
Book.metaClass.constructor << { String title -> new Book(title:title) }
def book = new Book('Groovy in Action - 2nd Edition')
assert book.title == 'Groovy in Action - 2nd Edition'
但在添加构造函数时要格外注意,因为这极易造成栈溢出。
静态方法
添加静态方法的方法与添加实例方法基本一样,只不过要在方法名前加上 static
修饰符。
class Book {
String title
}
Book.metaClass.static.create << { String title -> new Book(title:title) }
def b = Book.create("The Stand")
借用方法
利用 ExpandoMetaClass
,可以使用 Groovy 方法点标记法从其他类中借用方法。
class Person {
String name
}
class MortgageLender {
def borrowMoney() {
"buy house"
}
}
def lender = new MortgageLender()
Person.metaClass.buyHouse = lender.&borrowMoney
def p = new Person()
assert "buy house" == p.buyHouse()
动态方法名
在 Groovy 中,既然可以使用字符串作为属性名,那么反过来,也可以在运行时动态创建方法与属性名。要想创建具有动态名称的方法,只需使用将字符串引用为属性名的语言特性。
class Person {
String name = "Fred"
}
def methodName = "Bob"
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
def p = new Person()
assert "Fred" == p.name
p.changeNameToBob()
assert "Bob" == p.name
同样的概念可以应用于静态方法与属性。
Grails Web 应用框架可以算是动态方法名的一个应用范例。“动态编解码器”的概念正是通过动态方法名来实现的。
HTMLCodec
类
class HTMLCodec {
static encode = { theTarget ->
HtmlUtils.htmlEscape(theTarget.toString())
}
static decode = { theTarget ->
HtmlUtils.htmlUnescape(theTarget.toString())
}
}
上例实现了一个编解码器。Grails 提供了多种编解码器实现,每种实现都定义在一个类中。在运行时,会在应用类路径上出现多个编解码器类。在应用启动时,框架会将 encodeXXX
和 decodeXXX
方法添加到特定的元类中,这里的 XXX
是指编解码器类名的前面部分(如 encodeHTML
)。下面采用了一些 Groovy 伪码来表示这种机制:
def codecs = classes.findAll { it.name.endsWith('Codec') }
codecs.each { codec ->
Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) }
Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) }
}
def html = '<html><body>hello</body></html>'
assert '<html><body>hello</body></html>' == html.encodeAsHTML()
运行时发现
在运行时阶段执行某个方法时,还有其他什么方法或属性存在?这个问题往往是非常有用的。 ExpandoMetaClass
提供了下列方法(截止目前):
getMetaMethod
hasMetaMethod
getMetaProperty
hasMetaProperty
为什么不能单纯使用反射呢?因为 Groovy 的独特性——它包含两种方法,一种是“真正”的方法,而另一种则是只在运行时才能获取并使用的方法。后者有时(但也并不总是被)称为元方法(MetaMethods)。元方法告诉我们在运行时究竟能够使用何种方法,从而使代码能够适应。
这一点特别适用于重写 invokeMethod
、 getProperty
和/或 setProperty
时。
GroovyObject 方法
ExpandoMetaClass
的另一个特性是能够允许重写 invokeMethod
、 getProperty
和 setProperty
。 groovy.lang.GroovyObject
类中能找到这三个方法。
下面范例展示了如何重写 invokeMethod
:
class Stuff {
def invokeMe() { "foo" }
}
Stuff.metaClass.invokeMethod = { String name, args ->
def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
def result
if(metaMethod) result = metaMethod.invoke(delegate,args)
else {
result = "bar"
}
result
}
def stf = new Stuff()
assert "foo" == stf.invokeMe()
assert "bar" == stf.doStuff()
重写静态方法的逻辑跟之前我们见过的重写实例方法的逻辑基本相同,唯一不同之处在于对 metaClass.static
属性的访问,以及为了获取静态 MetaMethod
实例而对 getStaticMethodName
的调用。
重写静态 invokeMethod
ExpandoMetaClass
甚至可以允许利用一种特殊的 invokeMethod
格式重写静态方法。
class Stuff {
static invokeMe() { "foo" }
}
Stuff.metaClass.'static'.invokeMethod = { String name, args ->
def metaMethod = Stuff.metaClass.getStaticMetaMethod(name, args)
def result
if(metaMethod) result = metaMethod.invoke(delegate,args)
else {
result = "bar"
}
result
}
assert "foo" == Stuff.invokeMe()
assert "bar" == Stuff.doStuff()
扩展接口
可以利用 ExpandoMetaClass
为接口添加方法,但要想这样做, 必须 在应用启动前使用 ExpandoMetaClass.enableGlobally()
方法实施全局启用。
List.metaClass.sizeDoubled = {-> delegate.size() * 2 }
def list = []
list << 1
list << 2
assert 4 == list.sizeDoubled()
1.8 扩展模块
1.8.1 扩展现有类
利用扩展模块,可以为现有类添加新方法,这些类中可以包括 JDK 中那样的预编译类。这些新方法与通过元类或类别定义的方法不同,它们是全局可用的。比如当你编写:
标准扩展方法
def file = new File(...)
def contents = file.getText('utf-8')
File
类中并不存在 getText
方法,但 Groovy 知道它的定义是在一个特殊类中 ResourceGroovyMethods
:
ResourceGroovyMethods.java
public static String getText(File file, String charset) throws IOException {
return IOGroovyMethods.getText(newReader(file, charset));
}
你可能还注意到扩展方法是在“辅助”类(定义了多种扩展方法)中通过一个静态方法来定义的。 getText
的第一个实参对应着接受者,而另一个形参则对应着扩展方法的实参。因此,我们才在 File
类(因为第一个实参是 File
类型)中定义了一个名为 getText
的方法,它只传递了一个实参( String
类型的编码)。
创建扩展模块的过程非常简单:
- 如上例般编写扩展类;
- 编写模块描述符文件。
然后,还必须让 Groovy 能找到该扩展模块,这只需将扩展模块类和描述符放入类路径即可。这意味着有以下两种方法:
- 直接在类路径上提供类和模块描述符。
- 将扩展模块打包为 jar 文件,便于重用。
扩展模块可以为类添加两种方法:
- 实例方法(类实例上调用)
- 静态方法(仅供类自身调用)
1.8.2 实例方法
为现有类添加实例方法,需要创建一个扩展类。比如想在 Integer
上加一个 maxRetries
方法,可以采取下面的方式:
MaxRetriesExtension.groovy
class MaxRetriesExtension { //1⃣️
static void maxRetries(Integer self, Closure code) { //2⃣️
int retries = 0
Throwable e
while (retries<self) {
try {
code.call()
break
} catch (Throwable err) {
e = err
retries++
}
}
if (retries==0 && e) {
throw e
}
}
}
1⃣️ 扩展类
2⃣️ 静态方法的第一个实际参数对应着消息的接受者,也就是扩展实例。
然后, 在已经声明了扩展类之后 ,你可以这样调用它:
int i=0
5.maxRetries {
i++
}
assert i == 1
i=0
try {
5.maxRetries {
throw new RuntimeException("oops")
}
} catch (RuntimeException e) {
assert i == 5
}
1.8.3 静态方法
也可以为类添加静态方法。这种情况下,静态方法需要在自己的文件中定义。
StaticStringExtension.groovy
class StaticStringExtension { // 1⃣️
static String greeting(String self) { // 2⃣️
'Hello, world!'
}
}
1⃣️ 静态扩展类 2⃣️ 静态方法的第一个实参对应着将要扩展并且 还未使用 的类
在这种情况下,可以直接在 String
类中调用它:
assert String.greeting() == 'Hello, world!'
1.8.4 模块描述符
为了使 Groovy 能够加载扩展方法,你必须声明扩展辅助类。必须在 META-INF/services
目录中创建一个名为 org.codehaus.groovy.runtime.ExtensionModule
的文件。
org.codehaus.groovy.runtime.ExtensionModule
moduleName=Test module for specifications
moduleVersion=1.0-test
extensionClasses=support.MaxRetriesExtension
staticExtensionClasses=support.StaticStringExtension
该模块描述符需要 4 个键:
- moduleName:模块名称
- moduleVersion:模块版本。注意,版本号只能用于检查是否将同一个模块加载了两种不同的版本。
- extensionClasses:实例方法的扩展辅助类列表。可以提供几个类,但要用逗号分隔它们。
- staticExtensionClasses:静态方法的扩展辅助类列表。可以提供几个类,也要用逗号分隔它们。
注意,模块并不一定要既能定义静态辅助类,又能定义实例辅助类。你可以在一个模块中添加几个类,也可以在单一模块中扩展不同的类,甚至还可以在单一的扩展类中使用不同的类,但强烈建议将扩展方法按功能集分入不同的类。
1.8.5 扩展模块和类路径
值得注意的是,不能在代码使用已编译扩展模块的时候,你无法使用它。这意味着,要想使用扩展模块,在将要使用它的代码被编译前,它就必须以已编译类的形式出现在类路径上。这其实就是说,与扩展类同一源单位中不能出现测试类(test class),然而,测试源通常在实际中与常规源是分开的,在构建的另一个步骤中执行,所以这根本不会造成任何不良影响。
1.8.6 类型检查的兼容性
与类别不同的是,扩展模块与类型检查是兼容的:如果在类路径上存在这些模块,类检查器就会知道扩展方法,并不会说明调用的时间。它们与静态编译也是兼容的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论