当前位置: 首页 > article >正文

Kotlin学习笔记之类与对象

类和继承

与java一样,通过关键字class声明类,但是与java不同的地方有很多:

类声明由类名、类头(指定其类型参数、主构造函数等)和由大括号包围的类体构成。类头和类体都是可选的; 如果一个类没有类体,可以省略花括号。

例如:class Empty

构造函数

在 Kotlin 中的一个类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后,可见性修饰符必须写在constructor前面。

classPersonpublic@Injectconstructor(firstName:String){

}

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。

classPerson(firstName:String){

}

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中:

classCustomer(name:String){

init{

logger.info("Customer initialized with value ${name}")

}

}

注意,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用:

classCustomer(name:String){

valcustomerKey=name.toUpperCase()

}

事实上,声明属性以及从主构造函数初始化属性,Kotlin 有简洁的语法:

classPerson(valfirstName:String,vallastName:String,varage:Int){

// ……

}

与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。

次构造函数

类也可以声明前缀有 constructor次构造函数

classPerson{

constructor(parent:Person){

parent.children.add(this)

}

}

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:

classPerson(valname:String){

constructor(name:String,parent:Person):this(name){

parent.children.add(this)

}

}

如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数:

classDontCreateMeprivateconstructor(){

}

注意:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成 一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。

classCustomer(valcustomerName:String="")

创建类的实例

要创建一个类的实例,我们就像普通函数一样调用构造函数,Kotlin中没有new关键字:

valinvoice=Invoice()

valcustomer=Customer("Joe Smith")

继承

在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类:

classExample// 从 Any 隐式继承

Any不是 java.lang.Object;尤其是,它除了 equals()hashCode()toString()外没有任何成员。 要声明一个显式的超类型,我们把类型放到类头的冒号之后:

openclassBase(p:Int)

classDerived(p:Int):Base(p)

如果该类有一个主构造函数,其基类型可以(并且必须) 用(基类型的)主构造函数参数就地初始化。

如果类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:

classMyView:View{

constructor(ctx:Context):super(ctx)

constructor(ctx:Context,attrs:AttributeSet):super(ctx,attrs)

}

类上的 open 标注与 Java 中 final 相反,它允许其他类从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final, 对应于 Effective Java书中的第 17 条:要么为继承而设计,并提供文档说明,要么就禁止继承

覆盖方法

我们之前提到过,Kotlin 力求清晰显式。与 Java 不同,Kotlin 需要显式标注可覆盖的成员(我们称之为开放)和覆盖后的成员:

openclassBase{

openfunv(){}

funnv(){}

}

classDerived():Base(){

overridefunv(){}

}

Derived.v() 函数上必须加上 override标注。如果没写,编译器将会报错。 如果函数没有标注 open 如 Base.nv(),则子类中不允许定义相同签名的函数, 不论加不加 override。在一个 final 类中(没有用 open 标注的类),开放成员是禁止的。

标记为 override 的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用 final 关键字:

openclassAnotherDerived():Base(){

finaloverridefunv(){}

}

覆盖属性

属性覆盖与方法覆盖类似;在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。每个声明的属性可以由具有初始化器的属性或者具有 getter 方法的属性覆盖。

openclassFoo{

openvalx:Intget(){……}

}

classBar1:Foo(){

overridevalx:Int=……

}

你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。这是允许的,因为一个 val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个 setter 方法。

请注意,你可以在主构造函数中使用 override 关键字作为属性声明的一部分。

interfaceFoo{

valcount:Int

}

classBar1(overridevalcount:Int):Foo

classBar2:Foo{

overridevarcount:Int=0

}

调用超类实现

派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现:

openclassFoo{

openfunf(){println("Foo.f()")}

openvalx:Intget()=1

}

classBar:Foo(){

overridefunf(){

super.f()

println("Bar.f()")

}

overridevalx:Intget()=super.x+1

}

在一个内部类中访问外部类的超类,可以通过由外部类名限定的 super 关键字来实现:super@Outer

classBar:Foo(){

overridefunf(){/* …… */}

overridevalx:Intget()=0

innerclassBaz{

fung(){

super@Bar.f()// 调用 Foo 实现的 f()

println(super@Bar.x)// 使用 Foo 实现的 x 的 getter

}

}

}

覆盖规则

在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super<Base>

open class A {

    open fun f() { print("A") }
    fun a() { print("a") }
}

interface B {
    fun f() { print("B") } // 接口成员默认就是“open”的
    fun b() { print("b") }
}

class C() : A(), B {
    // 编译器要求覆盖 f():
    override fun f() {
        super<A>.f() // 调用 A.f()
        super<B>.f() // 调用 B.f()
  }
}

同时继承 A 和 B 没问题,并且 a() 和 b() 也没问题因为 C 只继承了每个函数的一个实现。 但是 f() 由 C继承了两个实现,所以我们必须在 C 中覆盖 f() 并且提供我们自己的实现来消除歧义。

抽象类

类和其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻

我们可以用一个抽象成员覆盖一个非抽象的开放成员

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

属性和字段

声明属性

Kotlin的类可以有属性。 属性可以用关键字var 声明为可变的,否则使用只读关键字val

classAddress{

varname:String=……

varstreet:String=……

varcity:String=……

varstate:String?=……

varzip:String=……

}

要使用一个属性,只要用名称引用它即可,就像 Java 中的字段:

funcopyAddress(address:Address):Address{

valresult=Address()// Kotlin 中没有“new”关键字

result.name=address.name// 将调用访问器

result.street=address.street

// ……

returnresult

}

声明一个属性的完整语法是

var<propertyName>[:<PropertyType>][=<property_initializer>]

[<getter>]

[<setter>]

其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。

varallByDefault:Int?// 错误:需要显式初始化器,隐含默认 getter 和 setter

varinitialized=1// 类型 Int、默认getter 和 setter

我们可以编写自定义的访问器,非常像普通函数,刚好在属性声明内部。这里有一个自定义 getter 的例子:

valisEmpty:Boolean

get()=this.size==0

自定义的 setter:

varstringRepresentation:String

get()=this.toString()

set(value){

setDataFromString(value)// 解析字符串并赋值给其他属性

}

按照惯例,setter 参数的名称是 value,但是如果你喜欢你可以选择一个不同的名称

自 Kotlin 1.1 起,如果可以从 getter 推断出属性类型,则可以省略它:

valisEmptyget()=this.size==0// 具有类型 Boolean

如果你需要改变一个访问器的可见性或者对其注解,但是不需要改变默认的实现, 你可以定义访问器而不定义其实现:

varsetterVisibility:String="abc"

privateset// 此 setter 是私有的并且有默认实现

varsetterWithAnnotation:Any?=null

@Injectset// 用 Inject 注解此 setter

幕后字段

Kotlin 中类不能有字段。然而,当使用自定义访问器时,有时有一个幕后字段(backing field)有时是必要的。为此 Kotlin 提供一个自动幕后字段,它可通过使用 field 标识符访问。

varcounter=0// 此初始器值直接写入到幕后字段

set(value){

if(value>=0)

field=value

}

field 标识符只能用在属性的访问器内

如果属性至少一个访问器使用默认实现,或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段。

例如,下面的情况下, 就没有幕后字段:

valisEmpty:Boolean

get()=this.size==0

幕后属性

如果你的需求不符合这套“隐式的幕后字段”方案,那么总可以使用 幕后属性(backing property)

privatevar_table:Map<String,Int>?=null

publicvaltable:Map<String,Int>

get(){

if(_table==null){

_table=HashMap()// 类型参数已推断出

}

return_table?:throwAssertionError("Set to null by another thread")

}

从各方面看,这正是与 Java 相同的方式。因为通过默认 getter 和 setter 访问私有属性会被优化,所以不会引入函数调用开销。

编译期常量

已知值的属性可以使用 const 修饰符标记为 编译期常量。 这些属性需要满足以下要求:

  • 位于顶层或者是 object 的一个成员,用 String 或原生类型 值初始化,没有自定义 getter。
这些属性可以用在注解中:

constvalSUBSYSTEM_DEPRECATED:String="This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED)funfoo(){……}

延迟初始化属性

一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。

为处理这种情况,你可以用 lateinit 修饰符标记该属性:

publicclassMyTest{

lateinitvarsubject:TestSubject

@SetUpfunsetup(){

subject=TestSubject()

}

@Testfuntest(){

subject.method()// 直接解引用

}

}

该修饰符只能用于在类体中(不是在主构造函数中)声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是原生类型。

在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。

该修饰符只能用于在类体中(不是在主构造函数中)声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是原生类型。

在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。

接口

Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现,在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们

interfaceMyInterface{

funbar()// 默认的访问器

funfoo(){

// 可选的方法体}

}

interfaceMyInterface{

valprop:Int// 抽象的

valpropertyWithImplementation:String

get()="foo"

funfoo(){

print(prop)

}

}

classChild:MyInterface{

overridevalprop:Int=29

}

函数、属性和类、对象和接口可以在顶层声明,即直接在包内:

// 文件名:example.kt

packagefoo

funbaz(){}

classBar{}

  • 如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见,如果你声明为 private,它只会在声明它的文件内可见,如果你声明为 internal,它会在相同模块内随处可见,protected 不适用于顶层声明。

类和接口

对于类内部声明的成员:

  • private 意味着只在这个类内部(包含其所有成员)可见;
  • protected—— 和 private一样 + 在子类中可见。
  • internal —— 能见到类声明的 本模块内 的任何客户端都可见其 internal 成员;
  • public —— 能见到类声明的任何客户端都可见其 public 成员。

注意 对于Java用户:Kotlin 中外部类不能访问内部类的 private 成员。

如果你覆盖一个 protected 成员并且没有显式指定其可见性,该成员还会是 protected 可见性。

扩展

Kotlin 同 C# 和 Gosu 类似,能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。 这通过叫做 扩展 的特殊声明完成。Kotlin 支持 扩展函数 和 扩展属性

扩展函数

声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList<Int> 添加一个swap 函数:

funMutableList<Int>.swap(index1:Int,index2:Int){

valtmp=this[index1]// “this”对应该列表

this[index1]=this[index2]

this[index2]=tmp

}

这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意 MutableList<Int> 调用该函数了:

vall=mutableListOf(1,2,3)

l.swap(0,2)// “swap()”内部的“this”得到“l”的值

当然,这个函数对任何 MutableList<T> 起作用,我们可以泛化它:

fun<T>MutableList<T>.swap(index1:Int,index2:Int){

valtmp=this[index1]// “this”对应该列表

this[index1]=this[index2]

this[index2]=tmp

}

扩展是静态解析的

扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。

我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如:

openclassC

classD:C()

funC.foo()="c"

funD.foo()="d"

funprintFoo(c:C){

println(c.foo())}printFoo(D())

这个例子会输出 "c",因为调用的扩展函数只取决于参数 c 的声明类型,该类型是 C 类。

如果一个类定义有一个成员函数和一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字并且都适用给定的参数,这种情况总是取成员函数。 例如:

classC{

funfoo(){println("member")}

}

funC.foo(){println("extension")}

如果我们调用 C 类型 c的 c.foo(),它将输出“member”,而不是“extension”。

当然,扩展函数重载同样名字但不同签名成员函数也完全可以:

classC{

funfoo(){println("member")}

}

funC.foo(i:Int){println("extension")}

调用 C().foo(1) 将输出 "extension"。

可空接收者

注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测 this == null,这能让你在没有检测 null 的时候调用 Kotlin 中的toString():检测发生在扩展函数的内部。

funAny?.toString():String{

if(this==null)return"null"

// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()

// 解析为 Any 类的成员函数

returntoString()

}

扩展属性

和函数类似,Kotlin 支持扩展属性:

val<T>List<T>.lastIndex:Int

get()=size-1

注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。

valFoo.bar=1// 错误:扩展属性不能有初始化器

var Outer.index : Int
    get() = 1
    set(value) {
        index = value
    }

伴生对象的扩展

如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数和属性:

classMyClass{

companionobject{}// 将被称为 "Companion"

}

funMyClass.Companion.foo(){

// ……

}

就像伴生对象的其他普通成员,只需用类名作为限定符去调用他们

MyClass.foo()

扩展声明为成员

在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个 隐式接收者 —— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为 分发接收者,扩展方法调用所在的接收者类型的实例称为 扩展接收者

classD{

funbar(){……}

}

classC{

funbaz(){……}

funD.foo(){

bar()// 调用 D.bar

baz()// 调用 C.baz

}

funcaller(d:D){

d.foo()// 调用扩展函数

}

}

对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者优先。要引用分发接收者的成员你可以使用 限定的 this 语法

classC{

funD.foo(){

toString()// 调用 D.toString()

this@C.toString()// 调用 C.toString()

}

声明为成员的扩展可以声明为 open 并在子类中覆盖。这意味着这些函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。

openclassD{}

classD1:D(){}

openclassC{

openfunD.foo(){

println("D.foo in C")

}

openfunD1.foo(){

println("D1.foo in C")

}

funcaller(d:D){

d.foo()// 调用扩展函数

}

}

classC1:C(){

overridefunD.foo(){

println("D.foo in C1")

}

overridefunD1.foo(){

println("D1.foo in C1")

}

}

C().caller(D())// 输出 "D.foo in C"

C1().caller(D())// 输出 "D.foo in C1" —— 分发接收者虚拟解析

C().caller(D1())// 输出 "D.foo in C" —— 扩展接收者静态解析

数据类

我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data

dataclassUser(valname:String,valage:Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

equals()/hashCode() 对

toString() 格式是 "User(name=John, age=42)"

componentN() 函数 按声明顺序对应于所有属性;

copy() 函数(见下文)

为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:

主构造函数需要至少有一个参数

主构造函数的所有参数需要标记为 val 或 var

数据类不能是抽象、开放、密封或者内部的

(在1.1之前)数据类只能实现接口

此外,成员生成遵循关于成员继承的这些规则:

如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有final 实现,那么不会生成这些函数,而会使用现有函数;

如果超类型具有 open 的 componentN() 函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错

不允许为 componentN() 以及 copy() 函数提供显式实现

在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。

dataclassUser(valname:String="",valage:Int=0)

复制

在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:

funcopy(name:String=this.name,age:Int=this.age)=User(name,age)

这让我们可以写:

valjack=User(name="Jack",age=1)

valolderJack=jack.copy(age=2)

数据类和解构声明

为数据类生成的 Component 函数 使它们可在解构声明中使用:

valjane=User("Jane",35)

val(name,age)=jane

println("$name, $age years of age")// 输出 "Jane, 35 years of age"

标准数据类

标准库提供了 Pair 和 Triple。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。

密封类

密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在 Kotlin 1.1 之前, 该规则更加严格:子类必须嵌套在密封类声明的内部)。

sealedclassExpr

dataclassConst(valnumber:Double):Expr()

dataclassSum(vale1:Expr,vale2:Expr):Expr()

objectNotANumber:Expr()

(上文示例使用了 Kotlin 1.1 的一个额外的新功能:数据类扩展包括密封类在内的其他类的可能性。 )

一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。

密封类不允许有非-private 构造函数(其构造函数默认为 private)。

请注意,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。

使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。

funeval(expr:Expr):Double=when(expr){

isConst->expr.number

isSum->eval(expr.e1)+eval(expr.e2)

NotANumber->Double.NaN

// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况

}

泛型

与 Java 类似,Kotlin 中的类也可以有类型参数,一般来说,要创建这样类的实例,我们需要提供类型参数

classBox<T>(t:T){

varvalue=t

}

valbox:Box<Int>=Box<Int>(1)

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

valbox=Box(1)// 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。

声明处型变

假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java

interfaceSource<T>{

TnextT();

}

那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java

voiddemo(Source<String>strs){

Source<Object>objects=strs;// !!!在 Java 中不允许

// ……

}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:

abstractclassSource<outT>{

abstractfunnextT():T

}

fundemo(strs:Source<String>){

valobjects:Source<Any>=strs// 这个没问题,因为 T 是一个 out-参数

// ……

}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C<Base> 可以安全地作为 C<Derived>的超类。

简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者

out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们讲声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。

另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类的一个很好的例子是 Comparable

abstractclassComparable<inT>{

abstractfuncompareTo(other:T):Int

}

fundemo(x:Comparable<Number>){

x.compareTo(1.0)// 1.0 拥有类型 Double,它是 Number 的子类型

// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量

valy:Comparable<Double>=x// OK!

}

我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:

类型投影

使用处型变:类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T! 一个很好的例子是 Array:

class Array<T>(val size: Int) {

fun get(index: Int): T { ///* …… */ }

fun set(index: Int, value: T) { ///* …… */ }

}

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

fun copy(from: Array<Any>, to: Array<Any>) {

assert(from.size == to.size)

for (i in from.indices)

to[i] = from[i]

}

这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:

val ints: Array<Int> = arrayOf(1, 2, 3)

val any = Array<Any>(3) { "" }

copy(ints, any) // 错误:期望 (Array<Any>, Array<Any>)

这里我们遇到同样熟悉的问题:Array <T> 在 T 上是不型变的,因此 Array <Int> 和 Array <Any> 都不是另一个的子类型。为什么? 再次重复,因为 copy 可能做坏事,也就是说,例如它可能尝试一个 String 到 from, 并且如果我们实际上传递一个 Int 的数组,一段时间后将会抛出一个 ClassCastException 异常。

那么,我们唯一要确保的是 copy() 不会做任何坏事。我们想阻止它到 from,我们可以:

fun copy(from: Array<out Any>, to: Array<Any>) { // ……}

这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。

你也可以使用 in 投影一个类型:

fun fill(dest: Array<in String>, value: String) { // ……}

Array<in String> 对应于 Java 的 Array<? super String>,也就是说,你可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星投影非常像 Java 的原始类型,但是安全。

泛型函数

不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:

fun <T> singletonList(item: T): List<T> {

// ……

}

fun <T> T.basicToString() : String {

// 扩展函数 // ……

}

要调用泛型函数,在调用处函数名之后指定类型参数即可:

val l = singletonList<Int>(1)

泛型约束

能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。

上界

最常见的约束类型是与 Java 的 extends 关键字对应的 上界

fun <T : Comparable<T>> sort(list: List<T>) {
    // ……
}

冒号之后指定的类型是上界:只有 Comparable<T> 的子类型可以替代 T。 例如:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型

sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是

Comparable<HashMap<Int, String>> 的子类型

默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>

where T : Comparable,

T : Cloneable {

return list.filter { it > threshold }.map { it.clone()}

}

嵌套类与内部类

类可以嵌套在其他类中:

class Outer {

private val bar: Int = 1

class Nested {

fun foo() = 2

}

}

val demo = Outer.Nested().foo() // == 2

内部类

类可以标记为 inner 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用:

class Outer {

private val bar: Int = 1

inner class Inner {

fun foo() = bar

}

}

val demo = Outer().Inner().foo() // == 1

匿名内部类

使用对象表达式创建匿名内部类实例:

window.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ……
    }
                                                                                                            
    override fun mouseEntered(e: MouseEvent) {
        // ……
    }
})

如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使用带接口类型前缀的lambda表达式创建它:

val listener = ActionListener { println("clicked") }

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

enum class Direction {

NORTH, SOUTH, WEST, EAST

}

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

enum class Direction {

NORTH, SOUTH, WEST, EAST

}

每个枚举常量都是一个对象。枚举常量用逗号分隔。

初始化

因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:

enum class Color(val rgb: Int) {

RED(0xFF0000),

GREEN(0x00FF00),

BLUE(0x0000FF)

}

匿名类

枚举常量也可以声明自己的匿名类:

enum class ProtocolState {

WAITING {

override fun signal() = TALKING

},

TALKING {

override fun signal() = WAITING

};

abstract fun signal(): ProtocolState

}

使用枚举常量

就像在 Java 中一样,Kotlin 中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是 EnumClass):

EnumClass.valueOf(value: String):

EnumClassEnumClass.values(): Array<EnumClass>

如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf() 方法将抛出 IllegalArgumentException 异常。

自 Kotlin 1.1 起,可以使用 enumValues<T>() 和 enumValueOf<T>() 函数以泛型的方式访问枚举类中的常量 :

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // 输出 RED, GREEN, BLUE

每个枚举常量都具有在枚举类声明中获取其名称和位置的属性:

val name: String
val ordinal: Int

对象表达式和对象声明

有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Java 用匿名内部类处理这种情况。 Kotlin 用对象表达式对象声明对这个概念稍微概括了下。

对象表达式

要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:

window.addMouseListener(object : MouseAdapter() {

override fun mouseClicked(e: MouseEvent) {

// ……

}

override fun mouseEntered(e: MouseEvent) {

// ……

}

})

如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

open class A(x: Int) {
    public open val y: Int = x
}

interface B {……}

val ab: A = object : A(1), B {
    override val y = 15
}

任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:

fun foo() {

val adHoc = object {

var x: Int = 0

var y: Int = 0

}

print(adHoc.x + adHoc.y)

}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问。

class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // 没问题
        val x2 = publicFoo().x  // 错误:未能解析的引用“x”
    }
}

就像 Java 匿名内部类一样,对象表达式中的代码可以访问来自包含它的作用域的变量。 (与 Java 不同的是,这不仅限于 final 变量。)

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ……
}

对象声明


http://www.kler.cn/a/582132.html

相关文章:

  • 云创智城 ×YunParking停车源码+YunCharge充电源码+DeepSeek:AI 驱动城市级停车平台升级,构建安全智慧出行新生态
  • 【赵渝强老师】管理MongoDB的运行
  • c语言程序设计---(动态内存分配)考研复试面试问答
  • QEMU构建基于ubuntu的根文件系统
  • vue知识点(2)
  • Docker基础入门
  • 从0到1搭建前端项目
  • ROS实践(四)机器人建图及导航
  • C++ 布尔类型(bool)深度解析
  • Nginx快速安装-Linux-CentOS7
  • Docker介绍和安装
  • Three.js 进阶(灯光阴影关系和设置、平行光、阴影相机)
  • 如何将ipynb文件转换为pdf文件
  • 能否调整爬虫以支持多页商品列表?
  • 【制作PPT的AI工具】
  • http 服务器概念详细介绍
  • C++学习笔记(十六)——函数重载
  • 【每日学点HarmonyOS Next知识】重叠顺序、对话框位置、事件总线、PageMap显示、多表多item类型
  • 51单片机之蓝牙模块的使用
  • C#变量与变量作用域详解