17. Groovy 面向对象编程-类成员学习-第二篇

2022-12-08 17:54:58 浏览数 (1)

1. 介绍

本篇文章为Groovy语言学习第十七篇,在上一篇针对类成员信息的学习了解了构造函数的多种模式,方法的创建方式,

以及可变参数,默认参数的相关知识后,本篇继续学习相关类成员信息。

主要内容为方法的多态选择优先级,以及Groovy中方法的异常声明。

2. 方法-Methods

2.1 方法选择算法

动态Groovy支持多调度(也称为多方法)。当调用方法时,实际调用的方法是基于方法参数的运行时类型动态确定的。首先将考虑方法名称和参数数量(包括可变参数的允许值),然后考虑每个参数的类型。示例如下所示:

PS:后面会有文章专门介绍什么是动态Groovy什么是静态Groovy。现在大家可以简单理解为,脚本写法编译的是动态的,其他参照Java语法规则写的就是静态的。

代码语言:javascript复制
//创建方法1,传递两个Object对象。
def method(Object o1, Object o2) {
     '两个Object对象' }
//创建方法2,传递Integer和String对象。
def method(Integer i, String  s) {
     '第一个参数为Integer,第二个参数为String' }
//创建方法3,传递String和Integer对象。
def method(String  s, Integer i) { 
    '第一个参数为String,第二个参数为Integer' }

//将会调用第三个方法
println(method('zinyan',23))   //输出:第一个参数为String,第二个参数为Integer
//将会调用第一个方法
println(method('zinyan','Z同学'))   //输出: 两个Object对象
//将会调用第二个方法
println(method(1024,'Z同学'))   //输出: 第一个参数为Integer,第二个参数为String

在上面的示例中,Groovy会自动根据方法的数据类型,调用符合规则的方法进行运算。这个逻辑也是面向对象中的多态的概念之一了。

还有一种比较特殊的情况,就是编译时不知道数据类型。例如通过后台接口传值等,预先不知道会是String还是Integer还是Object对象。

示例如下:

代码语言:javascript复制
//创建方法1,传递两个Object对象。
def method(Object o1, Object o2) {
     '两个Object对象' }
//创建方法2,传递Integer和String对象。
def method(Integer i, String  s) {
     '第一个参数为Integer,第二个参数为String' }
//创建方法3,传递String和Integer对象。
def method(String  s, Integer i) { 
    '第一个参数为String,第二个参数为Integer' }

List<List<Object>> pairs = [['zin', 1], [2, 'yan'], [3, 4]]  //创建三种 数组对象
pairs.collect{
     a,b -> println(method(a,b)) 
}

输出结果为:

代码语言:javascript复制
第一个参数为String,第二个参数为Integer
第一个参数为Integer,第二个参数为String
两个Object对象

在实际运行中Groovy会将参数,代入到每个方法中,进行匹配一轮。直到匹配度最高的方法,就会触发该方法并执行。

方法选择就是从具有兼容参数类型的有效方法候选中找到最接近的拟合。因此,方法(Object,Object)对于前两次调用也是有效的,但与类型完全匹配的变量相比,它的匹配度不高。为了确定最接近的拟合,运行时有一个实际参数类型与声明参数类型之间的距离的概念,并试图最小化所有参数之间的总距离。

下表中列出了一些影响方法选择计算的一些因素:

  • 直接实现的接口比继承层次结构要优先:
    • interface I1 {} interface I2 extends I1 {} interface I3 {} class Demo implements I3, I2 { } def method(I1 i1) { "这是I1 接口的实现" } def method(I3 i3) { "这是I3接口的实现" } println(method(new Demo())) // 输出为:这是I3接口的实现 在上面的示例中,我们如果添加一个I2的接口实现例如: def method(I2 i2) { "这是I2接口的实现" } def method(I3 i3) { "这是I3接口的实现" } println(method(new Demo())) //就会出现错误异常: Caught: groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method Zinyan#method. Cannot resolve which method to invoke for [class Demo] due to overlapping prototypes between: [interface I2] [interface I3] groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method Zinyan#method. Cannot resolve which method to invoke for [class Demo] due to overlapping prototypes between: [interface I2] [interface I3] at Zinyan.run(Zinyan.groovy:14) 因为对于程序来说,I2和I3都是直接接口实现,两者优先级相同。就会出现groovy.lang.GroovyRuntimeException异常了。
  • Object 数组对象要比Object对象优先级高:
    • //创建一个数组入参的方法 def method(Object[] arg) { 'array' } //创建一个对象入参的方法 def method(Object arg) { 'object' } println "${method([] as Object[])}" //创建一个数组 输出 array 可以看到,即时数组也可以算做Object对象,但是在上面的方法中会执行Object[]的方法。
  • 与可变参数相比,非可变参数优先级高:
    • //创建一个可变传参 def method(String s, Object... vargs) { "这是有可变传参的方法" } def method(String s) { "这是一个固定传参的方法" } println(method('zinyan')) //输出:这是一个固定传参的方法 在有可变传参和固定传参方法时,方法会先选择固定传参方法处理。
  • 如果有两个可变传参的方法,则使用最小数量可变传参的方法的优先级高:
    • def method(String s, Object... vargs) { '一个参数,一个可变传参' } def method(String s, Integer i, Object... vargs) { '两个固定参数,一个可变传参' } println(method('zinyan.com',1024,"Z同学")) //输出:两个固定参数,一个可变传参 上面两个方法都满足的情况下,会选择固定传参中多的。也就是可变传参数量小的方法。
  • 接口优于继承:
    • interface I {} //创建一个接口 I class Base {} //创建一个 超类(父类) Base class Child extends Base implements I {} //创建一个子类,继承Base类,扩展I接口 //创建一个接口,接收Base类 def method(Base b) { 'Base 类实现' } //创建一个接口,接收I接口 def method(I i) { 'I 接口类实现' } //调用该接口传Child对象,上面两个方法都满足入参需求。 println(method(new Child())) //输出: I 接口类实现 可以明显看出,接口的优先级要比父类的优先级高。
  • 对于基础数据类型参数,相同或稍大的参数类型优先级高:
    • def method(Long l) { 'Long 类型' } def method(Short s) { 'Short 类型' } def method(Integer s) { 'Integer 类型' } def method(BigInteger bi) { 'BigInteger 类型' } def method(def bi) { 'Def 动态类型' } println(method(1024)) //输出Integer 类型 而如果没有Integer类型时,就会输出Long类型。示例如下: def method(Long l) { 'Long 类型' } def method(Short s) { 'Short 类型' } def method(BigInteger bi) { 'BigInteger 类型' } def method(def bi) { 'Def 动态类型' } println(method(1024)) //输出Long类型

上面示例中也有介绍如果方法类型优先级相同。Groovy无法判定该调用哪个方法时就会出现groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method异常。

我们如果有示例如下的代码:

代码语言:javascript复制
//方法1
def method(Date d, Object o) {
      'Date对象和Object对象' }
//方法2
def method(Object o, String s) {
      'Object对象和String对象' }

//如果我们想调用方法1,示例如下:
println(method(new Date(),'zinyan.com')) //输出: aught: groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method Zinyan#method.

在上面示例中。并不会调用方法1反而会出现异常。如果方法出现了上面的混乱情况,可以通过以下方式处理:

代码语言:javascript复制
//方法1
def method(Date d, Object o) {
      'Date对象和Object对象' }
//方法2
def method(Object o, String s) {
      'Object对象和String对象' }

//方案1:
println(method(new Date(),'zinyan.com' as Object)) //输出:Date对象和Object对象

//方案2:
println(method(new Date(),(Object)'zinyan.com')) //输出:Date对象和Object对象

//方案3:
println(method(new Date() as Object,(Object)'zinyan.com')) //输出:Object对象和String对象

将几种情况都进行了梳理。结合方案中的输出结果能够理解。

2.2 异常声明-Exceotion

我们方法中出现了try/catch。但是不想在方法中处理,而选择抛出去由使用方法的对象处理。就需要对方法进行异常声明了。

Groovy自动允许将选中的异常视为未选中的异常。这意味着不需要声明方法可能引发的任何已检查异常,如下面的示例所示,如果找不到文件,则会引发FileNotFoundException

代码语言:javascript复制
def badRead() {
     //得到一个文件对象
    new File('zinyan.txt').text
}

shouldFail(FileNotFoundException) {
    badRead()
}

也不需要在try/catch块中围绕对上一个示例中的badRead方法的调用(PS:如果我们愿意,我们也可以自由这样做)

如果希望声明我们的代码可能抛出的任何异常,可以自由地这样做。添加异常不会改变代码与任何其他Groovy代码的使用方式,但可以将其视为代码读者的文档。异常将成为字节码中方法声明的一部分,因此如果我们的代码可能从Java调用,那么包含它们可能会很有用。下面的示例说明了使用显式检查异常声明:

代码语言:javascript复制
def badRead() throws FileNotFoundException {
    new File('doesNotExist.txt').text
}

shouldFail(FileNotFoundException) {
    badRead()
}

ps:在Groovy中如果有异常,我们可以抛弃不用声明。但是我们的脚本如果要配合Java一起混编。那么在方法中添加throws FileNotFoundException会更方便java端的调用。

3. 小结

关于面向对象编程中,方法的相关知识就到这里了。下一篇学习类成员中的字段和属性知识点。

以上内容可以参考Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_method_selection_algorithm

0 人点赞