Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >35. Groovy 语法 类型知识详解-第二篇 类型推断

35. Groovy 语法 类型知识详解-第二篇 类型推断

作者头像
zinyan.com
发布于 2023-02-23 09:51:31
发布于 2023-02-23 09:51:31
68800
代码可运行
举报
文章被收录于专栏:zinyanzinyan
运行总次数:0
代码可运行

1. 介绍

接着上篇介绍的类型相关的知识内容,继续了解Groovy中关于类型Typing的相关知识内容。

上一篇内容分享了关于静态类型检测的部分知识要点。34. Groovy 语法 类型知识详解-第一篇

本章继续。

2 类型推断

类型推断的原则:当代码被@typecheck注释时,编译器执行类型推断。它不仅仅依赖于静态类型,而且还使用各种技术来推断变量的类型、返回类型、字面量等等,这样即使激活了类型检查器,代码也尽可能保持干净。

下面通过简单的示例来了解类型推断:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
def message = 'Welcome to Groovy!'       //变量使用def关键字声明       
println message.toUpperCase()                   
println message.upper() // compile time error

toUpperCase调用能够工作的原因是消息类型被推断为String

2.1.1 类型推断中的变量与字段

值得注意的是,尽管编译器对局部变量执行类型推断,但它不会对字段执行任何类型的类型推断,总是返回到字段的声明类型。为了说明这一点,让我们来看看这个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class SomeClass {
    def someUntypedField                                                                
    String someTypedField                                                               

    void someMethod() {
        someUntypedField = '123'                                                        
        someUntypedField = someUntypedField.toUpperCase()  // compile-time error        
    }

    void someSafeMethod() {
        someTypedField = '123'                                                          
        someTypedField = someTypedField.toUpperCase()                                   
    }

    void someMethodUsingLocalVariable() {
        def localVariable = '123'                                                       
        someUntypedField = localVariable.toUpperCase()                                  
    }
}

为什么会有这样的差异? 原因是线程安全。

在编译时,我们不能保证字段的类型。任何线程都可以在任何时间访问任何字段,并且在方法中为字段分配某种类型的变量和之后使用的时间之间,另一个线程可能已经更改了字段的内容。

对于局部变量则不是这样:我们知道它们是否“转义”,因此我们可以确保变量的类型随着时间的推移是常量(或非常量)。

请注意,即使字段是final的,JVM也不会保证它,因此无论字段是否是final的,类型检查器的行为都不会有所不同。

这是Groovy建议使用类型化字段的原因之一。虽然由于类型推断,对于局部变量使用def是完全可以的,但对于字段就不是这样了,因为字段也属于类的公共API,因此类型很重要。

2.1.2 集合文字类型推断

Groovy为各种类型文字提供了一种语法。Groovy中有三种原生集合:

  • lists:通过 [] 符号
  • maps:通过 [:] 符号
  • ranges:区间通过from..to (包括), from..<to (右边不包括),from<..to (左边不包括) 和from<..<to (全部都不包括)

集合的推断类型取决于集合的元素,如下表所示:

示例

类型

def list = []

java.util.List

def list = ['foo','bar']

java.util.List<String>

def list = ["${foo}","${bar}"]

java.util.List<GString>

def map = [:]

java.util.LinkedHashMap

def map1 = [someKey: 'someValue'] def map2 = ['someKey': 'someValue']

java.util.LinkedHashMap<String,String>

def map = ["${someKey}": 'someValue']

java.util.LinkedHashMap<GString,String>

def intRange = (0..10)

groovy.lang.IntRange

def charRange = ('a'..'z')

groovy.lang.Range<String> 使用边界的类型来推断范围的组件类型

正如我们所看到的,除了IntRange之外,推断类型使用泛型类型来描述集合的内容。如果集合包含不同类型的元素,类型检查器仍然执行组件的类型推断,但使用最小上界的概念。

2.1.3 最小上界-LUB

在Groovy中,两种类型AB的最小上界定义为:

  • 超类,对应于AB的公共超类
  • 接口,对应于AB实现的接口
  • 如果AB是基本类型,且A不等于B,则AB的最小上界是它们包装器类型的最小上界

如果AB只有一个公共接口,并且它们的公共超类是Object,那么两者的LUB(最小上界)就是公共接口。

最小上界表示AB都能赋值的最小类型。例如,如果AB都是String,那么两者的LUB(最小上界)也是String

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Top {}
class Bottom1 extends Top {}
class Bottom2 extends Top {}

assert leastUpperBound(String, String) == String                    
assert leastUpperBound(ArrayList, LinkedList) == AbstractList       
assert leastUpperBound(ArrayList, List) == List                     
assert leastUpperBound(List, List) == List                          
assert leastUpperBound(Bottom1, Bottom2) == Top                     
assert leastUpperBound(List, Serializable) == Object  

在这些示例中,LUB总是可以表示为JVM支持的普通类型。但是Groovy在内部将LUB表示为一种更复杂的类型。

例如,不能使用它来定义变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
interface Foo {}
class Top {}
class Bottom extends Top implements Serializable, Foo {}
class SerializableFooImpl implements Serializable, Foo {}

BottomSerializableFooImpl的最小上界是多少?

它们没有共同的超类(除了Object),但是它们共享两个接口(SerializableFoo),所以它们的最小上界是一个表示两个接口(SerializableFoo)并集的类型。这种类型不能在源代码中定义,但Groovy知道它。

在集合类型推断(以及一般的泛型类型推断)上下文中,这变得很方便,因为组件的类型被推断为最小上界。我们可以在下面的例子中说明为什么这很重要:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
interface Greeter { void greet() }                  
interface Salute { void salute() }                  

class A implements Greeter, Salute {                
    void greet() { println "Hello, I'm A!" }
    void salute() { println "Bye from A!" }
}
class B implements Greeter, Salute {                
    void greet() { println "Hello, I'm B!" }
    void salute() { println "Bye from B!" }
    void exit() { println 'No way!' }               
}
def list = [new A(), new B()]                       
list.each {
    it.greet()                                      
    it.salute()                                     
    it.exit()                                       
}

错误信息如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[Static type checking] - Cannot find matching method Greeter or Salute#exit()

这表明exit方法既没有在Greiter上定义,也没有在Salute上定义,这两个接口定义在AB的最小上界中。

2.1.4 实例推导

在正常的、非类型检查的Groovy中,我们可以这样写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Greeter {
    String greeting() { 'Hello' }
}

void doSomething(def o) {
    if (o instanceof Greeter) {     
        println o.greeting()        
    }
}

doSomething(new Greeter()) //输出:Hello

方法调用可以工作是因为动态分派(方法是在运行时选择的)。Java中的等效代码需要在调用greeting方法之前将o转换为Greeter,因为方法是在编译时选择的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (o instanceof Greeter) {
    System.out.println(((Greeter)o).greeting());
}

然而,在Groovy中,即使在doSomething方法上添加了@TypeChecked(从而激活了类型检查),强制转换也不是必需的。编译器嵌入instanceof推理,使强制转换成为可选的。

2.1.5 流类型-Flow typing

流类型是类型检查模式中Groovy的一个重要概念,也是类型推断的扩展。其思想是,编译器能够推断代码流中的变量类型,而不仅仅是在初始化时:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTyping() {
    def o = 'foo'                       
    o = o.toUpperCase()                 
    o = 9d                              
    o = Math.sqrt(o)                    
}

因此,类型检查器知道变量的具体类型随着时间的推移而不同。特别是,如果将最后的赋值替换为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
o = 9d
o = o.toUpperCase()

类型检查器现在将在编译时失败,因为当toUpperCase被调用时,它知道o是一个double类型,因此这是一个类型错误。

重要的是要理解,使用def声明变量并不是触发类型推断的事实。流类型适用于任何类型的任何变量。用显式类型声明变量只限制你可以赋值给变量的内容:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list = list*.toUpperCase()          
    list = 'foo'                        
}

还可以注意到,即使变量声明时没有泛型信息,类型检查器也知道组件类型是什么。因此,这样的代码将无法编译:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List list = ['a','b','c']           
    list.add(1)                         
}

解决这个问题需要在声明中添加显式泛型类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@groovy.transform.TypeChecked
void flowTypingWithExplicitType() {
    List<? extends Serializable> list = []                      
    list.addAll(['a','b','c'])                                  
    list.add(1)                                                 
}

引入流类型是为了减少经典Groovy和静态Groovy之间的语义差异。特别地,考虑这段代码在Java中的行为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Integer compute(String str) {
    return str.length();
}
public String compute(Object o) {
    return "Nope";
}
// ...
Object string = "Some string";          
Object result = compute(string);        
System.out.println(result);

在Java中,这段代码将输出Nope,因为方法选择是在编译时根据声明的类型完成的。因此,即使o在运行时是一个字符串,它仍然是被调用的对象版本,因为o已经声明为对象。简而言之,在Java中,声明的类型是最重要的,无论是变量类型、参数类型还是返回类型。

在Groovy中,我们可以这样写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int compute(String string) { string.length() }
String compute(Object o) { "Nope" }
Object o = 'string'
def result = compute(o)
println result

但这一次,它将返回6,因为所选择的方法是在运行时根据实际的参数类型选择的。所以在运行时,o是一个字符串,所以使用了字符串变量。注意,此行为与类型检查无关,它是Groovy的一般工作方式:动态分派

在类型检查的Groovy中,我们希望确保类型检查器在编译时选择与运行时相同的方法。

由于语言的语义,这在一般情况下是不可能的,但我们可以使用流类型使事情变得更好。

使用流类型,在调用compute方法时,o被推断为String,因此选择接受String并返回int的版本。这意味着我们可以推断方法的返回类型是int,而不是String。这对于后续调用和类型安全非常重要。

因此,在类型检查的Groovy中,流类型是一个非常重要的概念,这也意味着,如果应用了

@TypeChecked,则根据参数的推断类型选择方法,而不是根据声明的类型。这并不能确保100%的类型安全,因为类型检查器可能会选择错误的方法,但它确保了最接近动态Groovy的语义。

2.1.6 高级类型推断

流类型和最小上界推理的组合用于执行高级类型推断,并确保在多种情况下的类型安全。特别是,程序控制结构可能会改变变量的推断类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Top {
   void methodFromTop() {}
}
class Bottom extends Top {
   void methodFromBottom() {}
}
def o
if (someCondition) {
    o = new Top()                               
} else {
    o = new Bottom()                            
}
o.methodFromTop()                               
o.methodFromBottom()  // compilation error

上述的代码执行后,会报编译错误。

当类型检查器访问if/else控制结构时,它检查if/else分支中赋值的所有变量,并计算所有赋值的最小上界。这个类型是if/else块之后的推断变量的类型,所以在这个例子中,oif分支中被分配了一个Top,在else分支中被分配了一个Bottom。其中的LUB是一个Top,所以在条件分支之后,编译器推断o是一个Top。因此,允许调用methodFromTop,但不允许调用methodFromBottom

对于闭包(closures),特别是闭包共享变量,也存在同样的推理。闭包共享变量是定义在闭包外部,但在闭包内部使用的变量,如下例所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
def text = 'Hello, zinyan.com!'                          
def closure = {
    println text                                    
}

Groovy允许开发人员使用这些变量,而不要求它们是final变量。这意味着闭包共享变量可以在闭包内部重新赋值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String result
doSomething { String it ->
    result = "Result: $it"
}
result = result?.toUpperCase()

问题是闭包是一个独立的代码块,可以在任何时候执行(也可以不执行)。特别是,例如,doSomething可能是异步的。这意味着闭包的主体不属于主控制流。

因此,对于每个闭包共享变量,类型检查器也会计算该变量的所有赋值的LUB,并将该LUB用作闭包作用域之外的推断类型,如下例所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Top {
   void methodFromTop() {}
}
class Bottom extends Top {
   void methodFromBottom() {}
}
def o = new Top()                               
Thread.start {
    o = new Bottom()                            
}
o.methodFromTop()                               
o.methodFromBottom()  // compilation error

在这里,很明显,当methodFromBottom被调用时,不能保证在编译时或运行时o的类型将有效地是Bottom。这是有可能的,但我们不能确定,因为它是异步的。所以类型检查器只允许调用最小的上界,也就是这里的Top

所以上面的代码中,当我们调用methodFromBottom后就会出现编译错误了。

3. 小结

本篇内容主要介绍了各种类型推断,以及相关推断的过程和Groovy处理逻辑。相关知识可以参考Groovy 官方文档:

Groovy Language Documentation (groovy-lang.org)

如果觉得本篇内容总结的还可以,希望能够点个赞鼓励一下。谢谢。

下一篇,是关于类型的最后一篇内容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 zinyan 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
36. Groovy 语法 类型知识详解-最终篇
本篇是Typing相关知识的最后一篇。介绍关于类型的闭包和类型推断关系,以及最终的类型静态编译相关知识点。
zinyan.com
2023/02/23
1K0
36. Groovy 语法 类型知识详解-最终篇
34. Groovy 语法 类型知识详解-第一篇
本篇内容开始介绍Groovy中的各种类型知识。将会分多篇文章详细介绍和学习Groovy中的有关于类型的相关知识点。
zinyan.com
2023/02/23
8590
34. Groovy 语法 类型知识详解-第一篇
23. Groovy 面向对象编程-Traits特性学习-第四篇 高级功能
例如方法继承与Java8的区别。与Mixins的差异。以及静态方法属性和字段等相关知识点,
zinyan.com
2023/02/23
5280
23. Groovy 面向对象编程-Traits特性学习-第四篇 高级功能
21. Groovy 面向对象编程-Traits特性学习-第二篇
本篇内容为Groovy学习第二十一篇,上篇学习了Traits的一些使用。本篇继续深入学习Traits相关知识。
zinyan.com
2023/02/23
5140
21. Groovy 面向对象编程-Traits特性学习-第二篇
27. Groovy 闭包知识-第二篇 委托和授权学习
这一篇开始学习闭包的授权策略等知识点。例如和lambda的区别,闭包的委托delegate,所有owner等作用。
zinyan.com
2023/02/23
5250
27. Groovy 闭包知识-第二篇 委托和授权学习
38. Groovy 类型检查扩展,第二篇 使用扩展
在上一篇介绍了基本的Groovy的类型检查扩展,以及该扩展的意义和部分的API说明。
zinyan.com
2023/02/23
7770
38. Groovy 类型检查扩展,第二篇 使用扩展
Groovy 快速入门
Groovy是一门基于JVM的动态语言,很多语法和Java类似。大部分Java代码也同时是合法的Groovy代码。本文是快速入门,所以针对语法并不会做非常详细的介绍。如果需要详细语法,请直接查看Groovy官方文档。另外为了省事,本文中的大部分代码例子直接引用了Groovy文档。
乐百川
2022/05/05
1.5K0
【Groovy】Groovy 动态语言特性 ( Groovy 中函数实参自动类型推断 | 函数动态参数注意事项 )
定义两个不同的类 Student 和 Worker , 在类中都定义 hello 方法 ;
韩曙亮
2023/03/30
9740
【Groovy】Groovy 动态语言特性 ( Groovy 中函数实参自动类型推断 | 函数动态参数注意事项 )
17. Groovy 面向对象编程-类成员学习-第二篇
本篇文章为Groovy语言学习第十七篇,在上一篇针对类成员信息的学习了解了构造函数的多种模式,方法的创建方式,
zinyan.com
2022/12/08
4890
JDK8-JDK17中的新特性(var类型推断、模式匹配、Record、密封类)
新的语法结构,勾勒出了 Java 语法进化的一个趋势,将开发者从复杂、繁琐的低层次抽象中逐渐解放出来,以更高层次、更优雅的抽象,既降低代码量,又避免意外编程错误的出现,进而提高代码质量和开发效率。
鱼找水需要时间
2023/06/01
3.4K0
JDK8-JDK17中的新特性(var类型推断、模式匹配、Record、密封类)
Groovy新手教程
简单地说,Groovy 是下一代的java语言,跟java一样,它也执行在 JVM 中。
全栈程序员站长
2022/07/12
2.2K0
使用Groovy实现Domain-Specific Languages 二
In Groovy number types are considered equal to any other types. As such, it is possible to enhance numbers by adding properties or methods to them. This can be very handy when dealing with measurable quantities for example. Details about how existing classes can be enhanced in Groovy are found in the extension modules section or the categories section.
2022/07/10
5770
37. Groovy 类型检查扩展,第一篇 编写类型检查扩展
本篇Groovy学习笔记第37篇。开始介绍Groovy中的扩展类型检查相关知识。学会如何定义我们的类型检查器。
zinyan.com
2023/02/23
9930
37. Groovy 类型检查扩展,第一篇 编写类型检查扩展
从 Java 和 JavaScript 来学习 Haskell 和 Groovy(类型系统)
接上文 《从 Java 和 JavaScript 来学习 Haskell 和 Groovy(引子)》。
四火
2022/07/19
7450
Groovy关键字def
本文将介绍Groovy中def关键字。它为这种动态 JVM 语言提供了可选的类型化功能。
FunTester
2023/08/04
5430
Groovy关键字def
2--Gradle入门 - Groovy简介、基本语法
Gradle 需要 Groovy 语言的支持,所以本章节主要来介绍 Groovy 的基本语法。
Devops海洋的渔夫
2023/09/01
1.3K0
2--Gradle入门 - Groovy简介、基本语法
Java 10 实战第 1 篇:局部变量类型推断
现在 Java 9 被遗弃了直接升级到了 Java 10,之前也发过 Java 10 新特性的文章,现在是开始实战 Java 10 的时候了。
Java技术栈
2018/07/31
7820
28. Groovy 闭包知识学习-第三篇 终篇
本篇内容为Groovy学习笔记第28篇,继续学习闭包相关知识。前面介绍了闭包的创建与使用,以及闭包的委托等和授权策略。
zinyan.com
2023/02/23
9771
28. Groovy 闭包知识学习-第三篇 终篇
Groovy秘诀 顶
听说java世界里有个Groovy大神!java需要半天处理的事情,Groovy只需要几分钟,是的,几分钟…剩下来的时间,程序员终于有时间泡妹子了,^_^…….技术宅的兄弟,赶紧来看看吧。
白石
2019/08/23
4.8K0
Java 10 var关键字详解和示例教程【面试+工作】
在本文中,我将通过示例介绍新的Java SE 10特性——“var”类型。你将学习如何在代码中正确使用它,以及在什么情况下不能使用它。
Java帮帮
2018/12/13
1.3K0
Java 10 var关键字详解和示例教程【面试+工作】
推荐阅读
相关推荐
36. Groovy 语法 类型知识详解-最终篇
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验