1.4 泛型、对象表达式和代理
1.4.1 泛型
泛型是类型参数化的一种实现方式。基于泛型技术可使用相同的程序来实现对不同类型的参数进行处理或计算。泛型在使用时需要加注符号:<>,并在符号内设置泛型参数(一般使用大写英文字母来表示),例如:T。Kotlin语言可在定义类时使用泛型,下列示例程序展示了泛型的基本使用方法:
1 class Simple Class<T>(v: T){
2 var value = v //value的类型与v的类型一致,而v的类型可被动态指定
3 }
4
5 fun main(args: Array<String>){
6 val n = Simple Class<Int>(10)
7 val p = Simple Class<Float>(12.23f)
8 println(n.value)
9 println(p.value)
10 }
上述程序中,T为一个泛型。程序运行时,Simple Class类中的value类型会根据T类型的变化而变化。例如,程序第6行将T设置为Int类型,程序第8行所运行的结果为整型;而程序第7行将T设置成Float类型,程序第9行所运行的结果为小数。
Kotlin泛型在声明时可使用关键字out和in。当定义类的泛型声明中使用了关键字out,则带有指定类型的对象可赋值给带有该指定类型的父类型的变量或常量。例如,Simple Class<out T>声明中使用了out,则在后续程序中,Simple Class<TYPE>对象可赋值给Simple Class<TYPE的父类型>变量或常量。下列示例程序说明了out的使用方法:
1 open class Su{
2 var v: String = "cls"
3 }
4 class Super: Su()
5 class Simple Class<out T>(v: T){
6 val value: T = v
7 }
8 fun main(args: Array<String>){
9 val s = Super()
10 val c1 = Simple Class<Super>(s)
11 val c2: Simple Class<Su> = c1
12 val c3: Simple Class<Any> = c2
13 println(c2.value.v)
14 println(c3.value)
15 }
上述程序中,Simple Class类的泛型声明中使用了out,则c1(Simple Class<Super>对象)可赋值给c2(Simple Class<Su>常量),这是由于Su是Super的父类;另外,c2(Simple Class<Su>对象)可赋值给c3(Simple Class<Any>常量),这是因为Any是Kotlin所有类的父类。
类似,当定义类的泛型声明中使用了关键字in,则带有指定类型的对象可直接赋值给带有该指定类型的子类型的变量或常量。例如,在下列程序中,因为泛型声明中使用in,所以Super类型可被当作Su类型来使用:
1 open class Su
2 class Super: Su()
3
4 class Simple Class<in T>{
5 var value="value"
6 fun func(v: T): Unit{
7 value = v.to String()
8 }
9 }
10 fun main(args: Array<String>){
11 var c = Simple Class<Su>()
12 val s = Super()
13 c.func(s)
14 println(c.value)
15 }
1.4.2 基于泛型声明方法和泛型限制
Kotlin还可在方法定义时进行泛型声明,基本形式有以下两种:
fun <T> 方法名(v: T, …): …{ //具有泛型声明的方法
…
}
fun <T> T.方法名(…): …{ //基于泛型的方法声明
…
}
例如,可以基于泛型定义相应的方法,并在不同情况下使用这些方法:
1 fun <T> func(v: T): T{ //本方法可对不同的类型数据进行处理
2 return v
3 }
4
5 fun <T> T.funct(): String{ //本方法可在不同的类中进行工作
6 return "string"
7 }
8
9 fun main(args: Array<String>){
10 println(func(10))
11 println(func(12.34f))
12 println(Int.funct())
13 println(Double.funct())
14 }
上述程序中,func和funct都是基于泛型技术定义的方法,而且,它们可以针对不同的数据类型完成相应的工作。
泛型在使用时可指定相关类型的上界,基本的语法为<T:T的类型上界>。当需要为泛型变量指定多个类型上界时使用关键字where,例如:
fun <T> 方法名称(参数列表):返回值类型
where T:类型1, T:类型2{
…
}
1.4.3 对象表达式
对象表达式用于声明一个匿名对象。所谓匿名对象,是指没有命名的对象。基本的使用场景包含:
方法名(object: 接口名{
接口中方法的定义
…
}
)
方法名(object: 抽象类名(){
抽象类中方法的定义
…
}
)
对象表达式还可用于直接定义简单数据结构,程序结构为:
val 常量名= object {
变量声明列表
}
例如:
1 val t=object{
2 val a = 100
3 var b = 0.4
4 }
上述结构在使用时可以使用t.a或t.b来访问具体的数值。匿名对象可直接被类中的私有方法使用,但匿名对象不允许被赋值给类中的公有方法。例如:
1 class Simple{
2 private fun func()=object{
3 var v: String = "value"
4 }
5 public fun get Value(): String{
6 return func().v
7 }
8 }
1.4.4 对象声明
对象声明用于在程序中创建具有唯一运行实例的对象,对象声明不能在方法内部使用,但可在其他对象声明内部或内部类中使用[2]。基本语法为:
object 对象名称{
属性声明列表
…
方法声明
}
对象声明可基于特定父类,语法结构为:
object 对象名称: 父类名称(参数列表){
属性声明列表
…
方法声明
}
基于对象声明以后的对象可直接在程序中使用,基本形式为对象名.属性、对象名.方法名(参数列表)。
1.4.5 伴随对象
在定义类时可在类的内部定义伴随对象,伴随对象的定义使用关键词companion object,基本结构为:
class 类名称{
companion object 伴随对象名称{
伴随对象的属性声明列表
…
伴随对象的方法声明
}
类中其他程序
…
}
上述结构中的“伴随对象名称”在编程时也可以省略。伴随对象中的方法可直接通过类名进行调用(一般程序中,类中的方法必须通过类的实例进行调用),即类名.属性、类名.方法名(参数列表)。
1.4.6 类代理
设计模式中的“代理模式”[3]在Kotlin中已被直接实现,相关技术的使用主要依赖于使用关键字by。代理模式是可以实现对被受限(或未知)对象访问的一种程序结构,这种结构可有效保护被访问对象的技术特征。代理模式实现的基本结构如图1.1所示,图中,访问组件需要访问服务组件,但服务组件可不对外公布访问接口;为了达到访问的目的,可在访问组件和服务组件间建立一个代理组件,该组件接收访问组件的访问请求,并基于访问请求调用服务组件。
图1.1 代理模式的实现结构
Kotlin提供了代理模式的直接实现方法,下面的示例说明实现的方法。
1 interface Inter{ //可访问接口
2 fun service()
3 }
4
5 class Simple Class1: Inter{ //服务组件1
6 override fun service() {
7 println("simple class one")
8 }
9 }
10
11 class Simple Class2: Inter{ //服务组件2
12 override fun service() {
13 println("simple class two")
14 }
15 }
16
17 class Agent(i: Inter): Inter by i //代理组件
18
19 fun main(args: Array<String>){
20 var c: Inter = Simple Class1()
21 Agent(c).service() //通过代理组件访问服务组件1
22 c = Simple Class2()
23 Agent(c).service() //通过代理组件访问服务组件2
24 }
上述程序中,class Agent(i: Inter): Inter by i说明了:①Agent类是代理组件;②Simple Class1()和Simple Class2()为服务组件;③系统会自动实现与代理模式相关的后续技术工作。
1.4.7 代理属性
在类中,可以使用代理属性。对只读变量而言,语法为val 变量名:变量类型 by 代理类名称();对普通变量而言,语法为var 变量名: 变量类型 by 代理类名称()。例如:
1 import kotlin.reflect.KProperty
2
3 class Simple Class{
4 var v: String by Agent()
5 }
6 class Agent{
7 var agent: String = ""
8 operator fun get Value(this Ref: Any?, property: KProperty<*>): String{
9 return "new value: $agent is for '${property.name} in $this Ref'."
10 }
11 operator fun set Value(this Ref: Any?, property: KProperty<*>, value: String) {
12 agent = value
13 println("newvalue:$agenthasbeenassignedto'${property.name}in$this Ref.'")
14 }
15 }
16
17 fun main(args: Array<String>){
18 val e = Simple Class()
19 e.v ="str"
20 println(e.v)
21 }
上述程序中,Simple Class类中的属性v被Agent类中的agent属性所替换。每当对Simple Class对象的v进行取值或设值操作时,被执行的程序实例实际上是Agent对象。代理属性在实现时必须沿用set Value和get Value的方法名称,而且它们是和原属性值的set()和get()方法相对应的。如果程序中被代理的属性是常量,则代理类中只包含get Value定义。
针对只读属性,代理会提供get Value方法,该方法中使用两个参数:this Ref和property。其中,this Ref为被代理对象的持有者(property owner),property为被代理对象[2]。针对一般属性(如变量),代理会提供set Value和get Value方法,这些方法中会使用3个参数:this Ref、property和value。其中,this Ref为被代理对象的持有者(property owner),property为被代理对象,value是被代理对象的值[2]。
1.4.8 预定义的代理工具
Kotlin标准类库中预定义了很多可以直接使用的代理工具,如lazy和observable。其中,lazy方法是针对只读变量定义的代理,使用时直接基于Lambda表达式返回一个Lazy<T>实例[2]。例如:
1 fun main(args: Array<String>){
2 val v1 by lazy(){
3 println("a string")
4 }
5 val v2: Int by lazy{
6 100 + 100
7 }
8
9 v1
10 println(v2)
11 }
上述程序中使用lazy来代理常量v1和v2;其中,v1未指定常量类型,而v2设定类型为整型。需要特别说明的是,Kotlin目前的版本中,lazy{…}和lazy(){…}无实质区别。
observable位于kotlin.properties.Delegates包中,使用时该方法涉及4个参数:变量初始值、变量名、变量已使用的旧值、变量正使用的新值。在定义observable时,可设置变量的初始值;在observable定义中,还可以使用的参数包含变量名、变量已使用的旧值、变量正使用的新值。例如:
1 import kotlin.properties.Delegates
2
3 fun main(args: Array<String>){
4 var v1:String by Delegates.observable("string0"){ //string0为v1的初始值
5 prop, old, new -> println("${prop.name} value: from $old to $new")
6 } //prop为v1,old为旧值,new为新值
7
8 v1 = "string1"
9 v1 = "string2"
10 }
上述程序运行的结果为:
1 v1 value: from string0 to string1
2 v1 value: from string1 to string2
Kotlin中还可以基于Map类(即所谓“字典”)代理类中的属性,例如:
1 class My Pair(map: Map<String, Any?>) {
2 val left: String by map
3 val right: Int by map
4 }
5 fun main(args: Array<String>){
6 var p = My Pair(map Of(
7 "left" to "k",
8 "right" to 100
9 ))
10 println(p.left)
11 println(p.right)
12 }
上述程序中,My Pair中定义了两个常量:left和right,在该类定义时使用了Map类型的参数map作为代理(程序中使用了关键字by)。在main方法中,变量p被指定为My Pair的实例;p初始化时,具体的值被指定到一个map实例中,而map实际包含了两个成员:{“left”:“K”, “right”:100}。当调用p的属性时,程序实际上会返回map中的值。需要说明的是,当使用Map作为代理时,被代理的对象必须是只读变量;当针对普通变量使用Map代理时,则需要使用Mutable Map类。
1.4.9 本地代理属性
对于方法内部的变量也可以使用代理,例如:
1 class Simple Class{
2 fun service(): String{
3 return "service"
4 }
5 }
6 fun func(calc: ()->Simple Class): String{
7 val value by lazy(calc)
8 return value.service()
9 }
10 fun main(args: Array<String>){
11 println(func({->Simple Class()}))
12 }
上述程序中,方法func中的value常量使用lazy方法产生代理(即所谓本地代理属性);具体而言,该代理是通过函数产生的 Simple Class 实例(因为在 func 中使用的参数是 calc:()->Simple Class),程序第8行再调用实例中的方法工作。
1.4.10 注解
注解(Annotation)是关于程序的元数据(即描述程序的数据)。定义注解时使用关键字annotation,例如,当要定义一个注解Tag时,则声明为annotation class Tag。注解的相关特征可通过下列命令来指定[2]。
● @Target指定注解标注的对象,可以是类、函数、属性、表达式等;
● @Retention指定注解是否存储在编译后的class文件中,以及它在运行时能否基于反省技术可见(默认都为真);
● @Repeatable指定是否允许针对同一对象多次使用相同注解;
● @Must Be Documented指定注解是公共API的一部分,而且应该被包含在API文档中。
注解可使用参数。参数不能包含空值,但可使用的类型[2]包括Java中的主数据类型、字符串、类、枚举、其他注解类,以及这些数据类型的数组。例如:
1 @Target(Annotation Target.CLASS, Annotation Target.PROPERTY, Annotation Target.FUNCTION, Annotation Target.VALUE_PARAMETER, Annotation Target.EXPRESSION)
2 @Retention(Annotation Retention.RUNTIME)
3 @Must Be Documented
4 annotation class Tag(val name: String, val value: String) //声明Tag为一个注解
5
6 @Tag(name = "cls", value = "My Class") //对类使用注解Tag
7 class My Class{
8 @Tag(name="prop", value = "att") //对属性使用注解Tag
9 var att = ""
10 @Tag(name="meth", value = "func") //对方法使用注解Tag
11 fun func(){
12 println("func is working")
13 }
14 }
Kotlin中关于注解的使用位置对象主要包含[2]file、property、field、get、set、receiver、param、setparam、delegate。当注解的使用对象不确定时,系统指定的对象顺序为param、property、field。注解信息的访问可通过反省技术来实现。
当针对类的构建器使用注解时,被注解的构建器必须使用关键字constructor进行说明。另外,还可针对Lambda使用注解。
1.4.11 反省
反省是针对程序或程序的运行实例进行分析和解释的技术。反省实现的基础是引用技术,当对类进行引用时,基本语法为类名::class;当对方法进行引用时,基本语法为::方法名;当对类中的方法进行应用时,基本语法为类名::方法名;当对变量或常量进行引用时,基本语法为::变量名(或常量名);当对类中的属性进行引用时,基本的语法为类名::属性名。例如,当要查看上一节中My Class类的注解内容时,则可基于反省实现。
1 fun main(args: Array<String>){
2 val mc = My Class::class //引用类
3 val mf = My Class::func //引用类中的方法
4 val mp = My Class::att //引用类中的属性
5 for(a in mc.annotations){ //检查类的注解
6 var t: Tag = a as Tag
7 println("Tag: "+t.name+" = "+t.value)
8 }
9 for(a in mp.annotations){ //检查属性的注解
10 var t: Tag = a as Tag
11 println("Tag: "+t.name+" = "+t.value)
12 }
13 for(a in mf.annotations){ //检查方法的注解
14 var t: Tag = a as Tag
15 println("Tag: "+t.name+" = "+t.value)
16 }
17 }
上述程序第2行至第4行分别获得My Class的类、方法和属性的引用;第5行至第8行检查类中的注解,并分别显示注解的相关内容;第9行至第12行检查类中所有属性的注解,并分别显示注解的相关内容;第13行至第17行检查类中所有方法的注解,并分别显示注解的相关内容。
另外,方法引用可被应用于实现动态替换程序中的计算方法。例如,在下列程序中,evaluate中的test可被动态替换,程序第13行使用Lambda表达式实现test方法,程序运行时会将数组中的元素全部打印输出;而程序第15行基于方法引用实现test方法,程序运行时会将数组中的所有偶数进行打印输出。
1 fun evaluate(array: Int Array, test: (n: Int)->Boolean){
2 for (i in array){
3 if (test(i)){
4 print("$i ")
5 }
6 }
7 }
8 fun predicate(i: Int) = i%2 == 0
9
10 fun main(args: Array<String>){
11 val arr: Int Array = int Array Of(1, 2, 3, 4, 5, 6, 7, 8)
12 print("all numbers: ")
13 evaluate(arr, {i: Int -> true})
14 println("\neven numbers:")
15 evaluate(arr, ::predicate)
16 }
对类的构造器也可以使用引用,例如,在下列程序中,程序第6行声明builder时使用了::Simple Class,则builder变成了一个类的引用。
1 class Simple Class(str: String){
2 val att = str
3 }
4
5 fun main(args: Array<String>){
6 val builder = ::Simple Class
7 var c = builder("string")
8 println(c.att)
9 }