Kotlin语法糖--类和对象(中)
在上篇的学习中,我们将Kotlin中类的相关概念进行了学习,这篇我们就对对象属性进行学习
-
属性和字段
-
声明属性
Kotlin中的类可以有属性,属性用关键字var表明为变量,用val表明为常量。要使用一个属性,直接通过名称引用即可
fun main(args: Array<String>) {
val a=A()
a.valueA
a.valueB
}
class A {
val valueA = 1
var valueB = 2
}
-
Getters和Setters
声明一个属性的完整语法为
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
其初始化属性部分、getter、setter部分都是可选的。属性的类型如果可以从初始值或者getter返回值推断出来,这个也可以省略掉,但是getter推断仅限于常量属性。常量属性不会有setter方法
来看下getter、setter的完整写法
class B {
var valueC: Int = 10
get() {
return field
}
set(value) {
field=value+1
}
}
简单说明一下
(1) 除了getter/settter结构外,我们会注意到field标识符,我们后面会具体说一下,这里我就简单的告诉你重新赋值跟重新取值需要它
(2) setter里面的参数value就是你赋值时传入到属性中的值,默认是value,你要是自己愿意也可以改成其他的
(3) 如果你需要改变一个访问器的可见性或者对其添加注解,但不需要改变默认的实现,你可以定义访问器而不定义其实现方式,好比这样
var valueB: Int = 12
private set
因为valueB的setter方法是私有的,所以你无法对它进行赋值了
在Java中通过setter/getter方法来使用相应的属性,像这样
A a=new A();
a.getValueA();
a.setValueB(3);
Kotlin中有注解@JvmField,用来指示Kotlin编译器不为该属性生成getter/setter方法,并将其作为字段公开。
@JvmField var valueC = 3
System.out.println(a.valueC);
-
备用字段与备用属性
其实这个地方我是有点困惑的,之前看了很多篇文章对这个地方的解释,感觉都不很清楚,那我就按照自己的理解来说明吧。当你自定义访问器的时候,Kotlin会自动提供一个备用字段,使用刚才那个field标识符去访问备用变量。field标识符只能用在属性的访问器内。刚才初始化的时候那个valueB的值12就会被写入备用字段中,所以field的值就是这么来的
-
编译期常量
我们可以使用const修饰符来编辑编译期常量。编译期常量需要满足以下要求
(1) 位于顶层或者是object的一个成员
(2) 用属性或者原生类型值进行初始化
(3) 没有自定义getter
Java中的编译期常量为final。有人说我们不是之前有了val来表明常量了吗,这边还要一个干嘛?那我们就来看看作用吧
const val ANDROID: String = "1"
object C {
const val XIAOMI: String = "2"
val HUAWEI: String = "3"
}
class D {
companion object {
const val XIAOMI: String = "3"
val HUAWEI: String = "4"
}
}
看好这个const修饰符使用的位置,来看看引用的地方
ANDROID
C.XIAOMI
C.HUAWEI
D.XIAOMI
D.HUAWEI
在Kotlin中貌似没有什么不同,就这样了吗?来看看字节码部分
字节码
这个应该就显而易见了。以object C为例,不是编译期常量的话,在java中是不能直接通过名称访问的,只能通过gettter去访问;而编译期常量直接是一个公开的属性
之前提到的@JvmField,如果用它来修饰val变量,就和const关键字加不加功能都一样
-
惰性初始化属性
这个在系列最开始的时候已经介绍过了。如果你使用依赖注入来进行初始化的话,我们的属性声明是没有必要一开始就初始化的,这个时候就需要用到惰性初始化了
在变量中我们使用lateinit这个修饰符标记该属性
class E {
lateinit var value: String
}
使用时如果你不先初始化而直接使用,那么就妥妥的报错了
val e = E()
e.value="Hello LateInit"
println(e.value)
-
覆盖方法
与Java不同的是,Kotlin需要显式的标注可覆盖的成员和覆盖后的成员,怎么显式标注?直接写open和override就行
open class SuperClassB {
open fun getValue() : String {
return "1"
}
}
class F : SuperClassB() {
override fun getValue() : String {
return "1"
}
}
你要是说我忘记加了怎么办?放心好了,编译器不会让你通过的
还有一种情况,就是我这个子类又被别人继承了,我不想让子类的方法被别人覆盖,怎么办?也很简单,直接加一个final即可
open class F : SuperClassB() {
final override fun getValue() : String {
return "1"
}
}
class G : F()
-
覆盖属性
同覆盖方法一样,属性也是可以被覆盖的,也是要有open跟override。每个声明的属性可以由具有初始化器的属性或者具有getter方法的属性覆盖
class I
open class SuperClassC {
open var i:I? = null
open var a: String = "123"
open val b: String = "456"
open val c: String = "789"
}
class H : SuperClassC() {
override var a: String
get() = super.a
set(value) {}
override val b: String
get() = super.b
override var c: String
get() = super.c
set(value) {} // 不是必须
override var i: I?
get() = super.i
set(value) {}
}
你可以用一个var属性来覆盖一个val属性,但反之不可,因为属性本质是声明了一个getter方法,而将其覆盖为var只是相当于额外声明一个setter方法而已
你还可以将属性覆盖放到主构造函数上去,成为属性声明的一部分
class K(override var i: I?, override var a: String, override val b: String, override val c: String) : SuperClassC()
-
覆盖规则
Kotlin中,如果一个类继承的方法或者属性有多个实现,那么它必须覆盖这个成员并提供其自己的实现,为了表示它采用从哪个超类或者接口的实现,我们使用super关键字并且在其后添加相应的超类或者接口的类型,如super<Base>
open class M {
open fun getValue() : String {
return "1"
}
}
interface N {
fun getValue() : String {
return "2"
}
}
class J : M(), N {
override fun getValue(): String {
super<M>.getValue()
super<N>.getValue()
return "J"
}
}
同时继承M、N没有问题,问题在于getValue方法有2个实现,所以你必须制定super的类型来消除歧义,或者像文章这里2个getValue同时使用
-
委托属性
这个篇幅比较大,我们放到后面详细阐述
-
接口
Kotlin的接口与Java差不多,它既包含抽象方法的声明,也包含方法的实现。与抽象类不同的是,接口无法保存状态。接口可以有属性,但是必须声明为抽象的,或者提供访问器实现,并且不能有备用属性
interface F {
fun getValue()
fun getValue1() {
println("getValue1")
}
val a: Int
val b
get() = "2"
val c: Int
}
来看看接口的实现
class G : F {
override val c: Int
get() = 100
override fun getValue() {
getValue1()
}
override var a: Int = 0
get() = field
set(value) {
field = b.toInt()*10
}
}
这里同继承是一样的,接口也可以使用var属性覆盖val属性,但是要提供初始化值,接口内部实现好的方法也是可以直接使用的
-
解决覆盖冲突
这个在之前的覆盖规则中已经提及,此处不再赘述
-
可见性修饰符
类、对象、接口、构造函数、函数、属性以及他们的getter(通常与他的属性有相同的可见性)、setter都有可见性修饰符。Kotlin中的可见性修饰符有4种:public、internal、protected、private。如果没有显式的指定可见性修饰符的话,默认是public
public:任何地方都可以使用
private:只能在当前文件内使用
protected:在当前文件以及其子类中可见
internal:当前模块内的任何地方可以使用
还有几个地方要注意:
(1) 外部类不能直接访问内部类的private成员
(2) 如果你覆盖了一个protected成员并且没有显式指定其可见性,那么该成员依然还是protected
-
扩展
我们一般在Java代码中会写很多很多工具类,比如StringUtils、NumberFormatUtils这种。如果这些工具类被打成jar的话,我们可能还需要通过继承的方式来扩展相应工具类的功能。但是Kotlin中不需要这样麻烦,让我们来看看吧
-
扩展函数
class I {
fun printlnI() {
println("printlnI")
}
}
本来这个I类只有一个printlnI函数,但是我想让他打印点别的东西,怎么做呢?
fun I.printlnI1() {
println("printlnI1")
}
直接在顶层添加扩展,这样就给I新增了一个printlnI1函数
val i=I()
i.printlnI1()
需要注意的是,扩展并没有真正的修改它所扩展的类,而仅仅让这个类的实例对象通过.调用新的函数。扩展函数的调用是由发起函数调用的表达式的类型决定的,而不是运行时动态获得的表达式类型决定的。我们来验证一下这个结论
open class J
class K: J()
fun J.printlnValue() {
println("J")
}
fun K.printlnValue() {
println("K")
}
fun printValue(j: J) {
j.printlnValue()
}
运行一下
printValue(K())
看看结果
结果
因为扩展函数的调用只取决于声明参数的类型,这里声明的类型为J
如果一个类的成员函数与扩展函数函数签名相同,那么只会调用成员函数
class I {
fun printlnI() {
println("printlnI")
}
}
fun I.printlnI() {
println("printlnI__")
}
运行一下
println(I().printlnI())
看看结果
运行结果
当然函数签名不同的话,还是会执行新定义的函数的
-
扩展属性
函数都可以扩展了那属性当然不甘示弱了
val <T> List<T>.lastValue : T
get() = this[this.size-1]
这里是对List进行扩展,通过LastValue,返回当前集合的最后一个值。
需要注意的是,同扩展函数一样,扩展属性也不是真正的将这个属性添加到类中,因此也没办法让扩展属性拥有备用字段,它只能通过明确提供的getter或者setter方法才能定义
-
伴生对象扩展
class D {
companion object {
const val XIAOMI: String = "3"
val HUAWEI: String = "4"
}
}
fun D.Companion.printlnValue() {
println(D.XIAOMI)
}
val D.Companion.ZHONGXING
get() = "5"
使用倒是没有差别
println(D.printlnValue())
println(D.ZHONGXING)
-
分发接收者与扩展接收者
看看如下情况
class L {
fun a() {
println("L")
}
}
class M {
fun a() {
println("M")
}
fun L.printlnValue() {
a()
}
fun addL(l: L) {
l.printlnValue()
}
}
L、M中都有a函数,那么我在执行addL函数之后,会打印谁呢?
M().addL(L())
来看看结果
运行结果
我们一般称扩展声明所在类的实例为分发接收者,比如这里的M;另外一个扩展方法调用的所在接收者类型的实例成为扩展接收者,比如这里的L。当这种接收者成员名字冲突的时候,扩展接收者优先执行。如果你要引用分发接收者,那么就得使用this@label,比如这里应该是this@M.a()
扩展函数与属性也可以被声明为open在子类中被覆盖
open class N {
open fun L.b() {
println("N")
}
open val L.num
get() = 100
fun printlnValue(l: L) {
l.b()
println(l.num)
}
}
class O : N() {
override fun L.b() {
println("O")
}
override val L.num: Int
get() = 200
}
运行一下
O().printlnValue(L())
看看结果
运行结果
得到的是子类中运行的结果