Kotlin官方参考整理——03.类和对象2
2017-06-29 22:11
253 查看
3.8 数据类
我们经常创建一些只保存数据的类。在Kotlin中,这叫做数据类并标记为data:data class User(val name: String, val age: Int)
数据类必须满足以下要求:
主构造函数需要至少有一个参数;
主构造函数的所有参数需要标记为val或var;
数据类不能是抽象、开放、密封或者内部的;
数据类的特点是,编译器会自动根据其主构造函数中声明的属性生成如下方法:
equals()、hashCode()
toString(),格式是 “User(name=John, age=42)”
copy()
componentN(),用于支持解构声明
3.8.1 复制
在很多情况下,我们需要复制一个对象并改变它的一些属性,而其余部分保持不变。copy()函数就是为此而生成的。对于上文的User类,会生成类似下面这样的copy()://函数参数有默认值 fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
可见,在copy()函数中创建了一个新的User对象,如果copy函数传入了某个属性的值,则新User对象的该属性就使用传入的值,如果copy函数没有传入某个属性的值,则新User对象的该属性就使用函数参数的默认值,即原有User对象的该属性值:
val jack = User("Jack", 1) val olderJack = jack.copy(age = 2) //olderJack和Jack的name属性相同,age属性不同
3.8.2 解构声明
有时把一个对象解构成多个变量会很方便,例如:val (name, age) = person println(name) println(age)
这种语法称为解构声明,一个解构声明一次创建多个变量。解构声明创建的变量和一般的变量没有什么区别,你可以独立地使用它们。
对一个对象使用解构声明的前提是对象所属的类中声明了所需的componentN函数(称为对象可被解构或类可被解构),因为实际上解构声明
val (name, age) = person会被编译器编译成如下代码:
val name = person.component1() val age = person.component2()
在for循环中使用解构
如果集合中的元素可被解构,则可以:
for ((a, b) in collection) { ... }
例如:
for ((key, value) in map) { //使用key、value做些事情 }
通过
in map获得的是map中的元素entrySet,而entrySet是可被解构的(Entryset类中声明了所需的componentN函数)。
从函数中返回两个变量
在Kotlin中一个简洁的实现⽅式是声明一个数据类并返回其实例:
data class Result(val result: Int, val status: Status) fun function(……): Result { //各种计算... return Result(result, status) } //系统会自动为数据类生成componentN函数,因此数据类必然是可被解构的 val (result, status) = function(……)
在lambda表达式中使用解构
代码如下所示。系统在调用lambda表达式时,传入的参数是一个entry。第一行代码保持了entry的原样,而第二行代码对entry进行了解构:
map.mapValues { entry -> "${entry.value}!" } map.mapValues { (key, value) -> "$value!" }
以下划线代表不关心的变量(自kotlin1.1起)
如果在解构声明中你不需要某个变量,不想费心去给它取一个名字,那么可以用下划线来代表它:
val (_ , status) = getResult()
3.9 密封类
密封类的子类是一个有限的集合。通过sealed关键字来声明一个密封类。只能在声明密封类的文件中声明密封类的子类(Kotlin1.1之前要求更为严格,要求必须在密封类内部声明密封类的子类)。package cn.szx.kotlindemo sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() //这是一个对象声明 //因为Expr是密封类,因此expr只能是Const对象、 Sum对象、或者NotANumber,没有其他的可能了。 fun eval(expr: Expr): Double = when (expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN //不再需要else子句,因为我们已经覆盖了所有的情况 }
3.10 泛型
3.10.1 基本使用
泛型类同Java一样,Kotlin中的类也可以有类型参数(即泛型):
class Box<T>(t: T) { var value = t }
创建对象时需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
如果类型参数可以从构造函数的参数或者其他途径推断出来,则允许省略类型参数:
val box = Box(1) //1具有类型Int,所以编译器知道我们说的是Box<Int>
泛型函数
函数也可以有类型参数(即泛型)。类型参数要放在函数名之前:
fun <T> singletonList(item: T): List<T> { ... } fun <T> T.basicToString() : String { //扩展函数 ... }
调用时要在函数名之后指定类型参数:
val l = singletonList<Int>(1)
3.10.2 实现协变与逆变(了解)
在阅读以下内容或官方参考的相关部分之前,请务必先阅读《协变与逆变.md》Joshua Bloch称那些你只能从中读取的对象为生产者,称那些你只能写入的对象为消费者。
声明处型变
假设有这样一个泛型类
Source<T>:
class Source<T> { ... }
为保证安全,
Source<T>是不可型变的(即不支持协变也不支持逆变),
Source<Object>不是
Source<String>的父类,
Source<String>也不是
Source<Object>的父类。
我们可以在泛型声明之前加上out关键字,来告诉系统,
Source<T>只支持读取T,不支持写入T。那么此时,
Source<T>就变成了可协变的:
class Source<out T> { ... } fun demo(strs: Source<String>) { val objects: Source<Object> = strs //合法,因为可协变,因此Source<Object>是Source<String>的父类 }
同理,我们也可以在泛型声明之前加上in关键字,来告诉系统,
Source<T>只支持写入T,不支持读取T。那么此时,
Source<T>就变成了可逆变的:
class Source<in T> { ... } fun demo(objs: Source<Object>) { val strs: Source<String> = objs //合法,因为可逆变,因此Source<String>是Source<Object>的父类 }
使用处型变:类型投影
将类型参数T声明为out非常方便,但是有些类实际上不能限制为只能读取T不能写入T,一个很好的例子是Array:
class Array<T>(val size: Int) { fun get(index: Int): T {...} fun set(index: Int, value: T) {...} }
因为有读、写T的方法,该类在T上既不能是协变的也不能是逆变的,这造成了一些不灵活性。考虑下述函数:
//将from中的元素复制到to中 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 anys = Array<Any>(3) copy(ints, anys) //编译报错,函数的第一个参数声明为Array<Any>,而实际传入的是Array<Int>,但Array<Int>并不是Array<Any>的子类(不可协变)
这是很不方便的。为了让copy的参数from能够接受一个
Array<Int>,可以使用out来声明copy函数的参数from:
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
这样做其实是告诉编译器,在函数中只会从from中读取元素,不会向其中写入元素,那么使参数from协变就是安全的,因此from就可以接受任意的
Array<Any的子类>了,包括
Array<Int>,这就是使用处协变。
同理,因为在函数中只会向参数to中写入元素,而不会从中读取元素,因此我们可以使用关键字in来声明参数to,这样参数to就可以接受任意的
Array<Any的父类>了:
fun copy(from: Array<out Any>, to: Array<in Any>) { ... }
使用处型变也称为类型投影,以from为例,这里的from是一个受限制的数组(只能读、不能写),就像是一个影子,它只具有原始数组的一部分功能(只能读、不能写)。
3.11 嵌套类、内部类、匿名内部类
package cn.szx.kotlindemo class AAA { private val m: Int = 1 //嵌套类,不持有外部类对象的引用,不能访问外部类的成员 class BBB { fun foo() = 2 } //被inner修饰的嵌套类就是内部类,持有外部类对象的引用,可以访问外部类的成员 inner class CCC { fun foo() = m } } //BBB是嵌套类,不持有AAA对象的引用,因此直接通过AAA的类名就能访问 val x = AAA.BBB().foo() //CCC是内部类,持有AAA对象的引用,因此只能先创建AAA的对象(即AAA()),再通过AAA的对象访问 val y = AAA().CCC().foo()
Kotlin中没有static关键字,因此没有静态内部类的概念。但是很容易看出,Kotlin中的嵌套类类似于Java中的静态内部类,而内部类类似于Java中的非静态内部类。
this关键字也可以与标签配合使用,这在多层嵌套的类中很有用:
class A { //隐式标签@A inner class B { //隐式标签@B //为Int类扩展了一个foo()函数,详见“扩展函数”部分 fun Int.foo() { //隐式标签@foo val a = this@A //指A的对象 val b = this@B //指B的对象 val c = this //指foo()的接收者,即Int对象 val c1 = this@foo //指foo()的接收者,即Int对象 //带接收者的匿名函数 val funLit = fun String.() { val d = this //指funLit的接收者 } //lambda表达式 val funLit2 = { s: String -> val d1 = this //这个lambda表达式没有接收者,因此这里的this指foo()的接收者,也就是一个Int对象 } } } }
匿名内部类的写法见“3.12.1 对象表达式”。
3.12 对象表达式和对象声明
对象表达式和对象声明都使用object关键字。3.12.1 对象表达式
有时候我们需要创建一个对某个类做了轻微改动的类的对象,而不想为此去专门声明一个新的类。Java中使用匿名内部类来处理这种情况,而在Kotlin中我们可以使用对象表达式。对象表达式的基本格式(父类是可选的):
object[:父类]{ ... }
比如给一个View设置点击监听,我们可以这样写:
val tv = findViewById(R.id.tv) tv.setOnClickListener(object : OnClickListener { override fun onClick(v: View?) { Log.e("Log", "onClick") } }) //实际上,这里的代码使用lambda表达式可以进一步简化,详见lambda表达式相关章节
需要注意的是,如果父类是类而不是接口,则必须传递适当的构造函数参数给它。多个父类以逗号分隔:
/* * A、B是类,C是接口 */ open class A(x: Int) { open val y: Int = x } open class B { ... } interface C { ... } /* * 使用对象表达式来创建对象 */ val ac1:A = object : A(1), C { } val ac2:C = object : A(1), C { } val bc1:B = object : B(), C { } val bc2:C = object : B(), C { }
就像Java中的匿名内部类那样,对象表达式中的代码可以访问来自包含它的作用域的变量。与Java不同的是,这不仅限于final变量(被final修饰的变量在Java中意为符号常量,而在kotlin中意为不可被覆写):
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++ } }) }
没有父类的对象表达式一般用不到,并且其中有一些坑,详见官方参考:
private val obj = object { val x: String = "x" }
3.12.2 对象声明
对象声明的作用也是创建一个对象,但对象声明不是表达式,不能用在等号的右边。对象声明的格式(父类是可选的):
object 对象名[:父类]{ ... }
例如:
//有父类 object aaa : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { } override fun mouseEntered(e: MouseEvent) { } } //也可以没有父类 object bbb{ val b = 1 fun functionB() = 2 }
通过对象名访问对象的成员:
fun test(){ aaa.mouseClicked(event1) aaa.mouseEntered(event2) print(bbb.b) print(bbb.functionB()) }
在类内使用对象声明创建的对象不同于一般的类成员,它可以直接通过所在类的类名来访问。但是在这样的对象内不能访问其所在类中的成员。
package cn.szx.kotlindemo class MyClass { val a = 1 //通过对象声明创建了名为obj1的对象 object obj1{ val b = 2 //val c = a//编译报错,不能访问MyClass中的成员 } } fun test() { print(MyClass().a) //通过MyClass对象来访问MyClass的成员a print(MyClass.obj1.toString()) //通过MyClass类名来访问对象obj1 print(MyClass.obj1.b) //通过MyClass类名来访问对象obj1 }
对象声明可以位于顶层位置、类内、嵌套类内,不能位于函数内、内部类内。
伴生对象
在类内使用对象声明创建对象时,可以加上companion关键字,即创建伴生对象。伴生对象的特点是,访问伴生对象的成员时可以省略伴生对象的对象名:
package cn.szx.kotlindemo class MyClass { val a = 1 companion object obj1{ val b =2 } } fun test() { print(MyClass().a) print(MyClass.obj1.toString()) print(MyClass.obj1.b) //访问伴生对象的成员 print(MyClass.b) //伴生对象的特性:访问伴生对象的成员时,可以省略伴生对象的对象名 }
创建伴生对象时,伴生对象的对象名也可以省略,这时伴生对象的名字为“Companion”:
class MyClass { val a = 1 companion object{ val b =2 } }
对象声明的初始化时机(即创建对象的时机)
以下来自官方参考,了解即可:
对象声明:当第一次被访问时
伴生对象:当其相应的类被加载时
利用对象声明实现单例模式
参考官方参考
//File1.kt package cn.szx.kotlindemo object DataProviderManager { fun getData() = 1 ... }
//File2.kt package cn.szx.kotlindemo fun test() { val data = DataProviderManager.getData() }
3.13 代理(Delegation)
注意:中文参考中将代理(Delegation)翻译成委托,其实代理和委托描述是相同的关系。例如,用户调用a的doSomething()方法,而a在自己的doSomething()方法中调用b的doSomething()方法。对于用户而言,a是b的代理(类似于一个中介);而对于a自己而言,它把doSomething这项工作委托给了b(因为真正doSomething的是b)。Kotlin中要实现代理非常简单:
interface Base { fun doSomething() } //Impl是Base的实现类 class Impl(val x: Int) : Base { override fun doSomething() { print(x) } } //代理类,代理了b的工作 //注意这段“: Base by b”,意思是:通过b(即by b)来实现Base接口, //系统会自动为Delegation生成doSomething方法,并在doSomething方法中调用b的doSomething class Delegation(b: Base) : Base by b fun main(args: Array<String>) { val b = Impl(10) //调用Delegation的doSomething方法,实际调用的b的doSomething方法 Delegation(b).doSomething() //输出 10 }
3.14 委托属性
3.14.1 示例
Koltin支持委托属性。class Example { //通过Delegate()来实现String //或者说,将自己的操作(读、写)委托给Delegate() var p: String by Delegate() } class Delegate { //当对p进行读操作时会调用此方法 //这里thisRef就是Example对象,而property就是属性p operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } //当对p进行写操作时会调用此方法 operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name} in $thisRef.'") } } //使用 fun test() { val e = Example() println(e.p) //会输出:Example@33a17727, thank you for delegating ‘p’ to me! e.p = "NEW" //会输出:NEW has been assigned to ‘p’ in Example@33a17727. }
3.14.2 委托属性的用途
通过委托属性,可以将属性的声明和实现分离到不同的文件中。有一些常见的属性类型,虽然我们可以在每次需要的时候⼿动实现它们,但是如果能有人把它们实现好并放入一个库中,以后大家要使用这些属性类型的时候可以直接从库中引用,这显然是更好的选择。常见的属性类型:
延迟属性(lazy properties): 其值只在首次访问时计算。
可观察属性(observable properties): 监听器会收到有关此属性变更的通知。
把多个属性储存在一个映射(map)中,而不是每个都存储在单独的字段中。
其实,Kotlin标准库已经为实现上面几种属性类型提供了工厂方法,通过这些工厂方法可以很方便的生成所需的被委托对象(即by之后的对象)。
3.14.3 实现延迟属性
lazy()是接受一个lambda表达式并返回一个Lazy <T>实例的函数,返回的实例可以作为实现延迟属性的被委托对象:第一次读取属性值时会执行lamdba表达式并记录结果,后续再读取属性值则只是返回记录的结果。
val lazyValue: String by lazy { println("computed!") "Hello" } fun main(args: Array<String>) { println(lazyValue) println(lazyValue) } 输出: computed! Hello Hello
默认情况下,对于延迟属性的求值是有同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将LazyThreadSafetyMode.PUBLICATION作为参数传递给lazy()函数。而如果你确定初始化将总是发生在单个线程,那么你可以使用LazyThreadSafetyMode.NONE模式,它不会有任何线程安全的保证和相关的开销。
3.14.4 实现可观察属性
Delegates.observable()接受两个参数:初始值和lambada表达式。每次属性被赋值后就会执行lambda表达式,lambda表达式有三个参数:属性、旧值和新值:class User { //first为初始值,lambda表达式为当属性被赋值后要执行的代码 var name: String by Delegates.observable("first") { prop, old, new -> println("$old -> $new") } } fun main(args: Array<String>) { val user = User() user.name = "second" user.name = "third" } 输出: first -> second second -> third
如果你希望能够截获一个赋值并“否决”它,那么可以使⽤vetoable()取代observable()。在对属性的赋值生效之前会执行传递给vetoable的lambda表达式,你将有机会否决掉这次赋值。
3.14.5 把属性储存在映射(map)中
一个常见的用例是在一个map⾥中存储属性的值,这时候你可以直接使用一个map对象来作为被委托对象。class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map } fun test() { val user = User(mapOf( "name" to "John Doe", "age" to 25 )) println(user.name) //Prints "John Doe" println(user.age) //Prints 25 }
这也适用于var属性,如果把只读的Map换成MutableMap的话:
class MutableUser(val map: MutableMap<String, Any?>) { var name: String by map var age: Int by map }
3.14.6 属性委托的要求&委托属性的实现原理
详见官方参考(了解)另,从Kotlin1.1开始,局部变量也能像属性那样进行委托了。
相关文章推荐
- Kotlin官方参考整理——03.类和对象1
- Kotlin官方参考整理——06.Java互操作
- Kotlin官方参考整理——05.其他
- Kotlin官方参考整理——01.开始
- Kotlin官方参考整理——04.函数和lambda表达式
- Kotlin官方参考整理——02.基础
- 【翻】【英汉对照】【完整官方参考】Windows媒体播放器11 SDK 播放器对象模型(三)
- ASP.Net七大内置对象 (整理的不错,转过来参考)
- Kotlin官方文档翻译,类和对象:类和继承
- 【翻】【英汉对照】【完整官方参考】Windows媒体播放器11 SDK 播放器对象模型(一)
- [整理]JavaScript对象参考大全(31个)
- django 1.8 官方文档翻译: 2-3-2 关联对象参考
- JavaScript RegExp 对象参考手册--整理
- kotlin官方文档中文翻译(三) 类和对象
- ASP.Net七大内置对象 (整理的不错,转过来参考)
- javascript 基础学习整理 二 之 html对象总结,参考W3C
- Kotlin 官方参考文档 中文版_kotlin-reference-chinese.pdf
- Python面向对象知识点回顾(参考廖雪峰的官方网站)
- 03.Oracle官方并发教程之线程对象
- 【翻】【英汉对照】【完整官方参考】Windows媒体播放器11 SDK 播放器对象模型(二)