赞
踩
公众号「稀有猿诉」 原文链接 一文带你吃透Kotlin类与对象
Kotlin是多范式通用编程语言,对面向对象编程(OOP)自然也提供了全方位的支持。通过先前一篇文章,学习了使用Kotlin进行基本面向对象编程的方法,本文将在前文基础之上继续深入的学习面向对象编程的高级特性,以能够写出更加符合OO的代码,并能够从容应对一些复杂的OOP场景。
在构造对象过程中,有三个地方可以对成员进行初始化:1)是在首构造方法(Primary constructor);2)是在声明成员的同时进行初始化,或者是在**初始化代码块(init {…})中;3)是在次要构造方法(Secondary constructor)**中。
要注意它们之间的区别和执行顺序,首构造方法是最先执行的,但它不能运行代码,只能进行赋值;成员声明和初始化代码块(init {…})是首构造方法的一部分,因此要先于次要构造方法。次要构造方法是最后执行,并且次要构造方法一定要委托到首构造方法。成员声明和初始化代码块之间则依赖于书写的顺序,从上到下执行。
虽然编译器有它的规则来保障顺序,但为了可读性和可维护性,我们不应该完全依赖编译器。这里建议的方式是:
扩展阅读Classes和Properties。
通常成员的初始化可以在声明时完成,比如像集合或者一些简单的原始类型对象(Int, Float, String等)。但如果初始化过程比较复杂,或者初始值较难获得,这种情况下,就适合标记为延迟初始化late init,然后在合适的时机对成员进行初始化(比如系统框架层的回调中,或者依赖注入等等)。使用一个未初始化的late init成员时会抛出一个叫做UninitializedPropertyAccessException的异常,可以在使用成员变量前用.isInitialized来判断成员变量是否初始化过:
if (foo::bar.isInitialized) {
println(foo.bar)
}
可以发现,对于Android 开发来说late init绝对非常有用,因为对于系统组件,我们无法在其构造方法中进行成员初始化,通常都是在第一个回调(如onCreate)中进行初始化,而这些变量全都应该用late init来标记。
另外,需要注意的是,成员是否有被初始化与成员是否是非法值(如null)并不是同一回事,初始化是第一次对成员对象赋值,赋的什么值(正常对象or null)虚拟机并不关心,但只要有过赋值后变量就初始化过了。因此,用late init可以帮助减少null检查。
还需要注意的是,延迟初始化late init与属性委托也不是同一回事,late init通常用于内部私有的成员变量,而属性委托通常用于对外开放的公开成员。
扩展阅读Properties。
接口(interfaces)是更高级别的抽象,专注于行为的抽象,用以实现对象间契约式行为交互。这一部分不打算详细讲解interface的使用,而是重点关注函数式接口(function interface)。Kotlin中的接口与Java 8中的接口是一样的,不再全是抽象方法了,可以有默认方法,也就是对接口的方法添加默认的实现,没有默认实现的方法就是抽象方法了(Abstract method)。只有一个抽象方法的接口称之为函数式接口(functional interface),或者单个抽象方法接口(Single Abstract Method interface)。用fun interface来声明,如:
fun interface IntPredict {
fun accept(i: Int): Boolean
}
函数式接口的最大优势在于,实现接口时可以简化到只用一个lambda,如:
val isEnv = IntPredict { it % 2 == 0 }
注意,只有用fun interface声明的含有一个抽象方法的接口才是函数式接口,才能用lambda。对于普通接口,如果它仅含有一个抽象方法,可以转化为函数式接口,比如原接口是酱紫的:
interface Printer {
fun print()
}
那么,可以直接定义一个fun interface Printer就可以了:
fun interface Printer {
fun print()
}
编译器会帮忙做转化。
扩展阅读Functional (SAM) interfaces。
关键字object用以方便创建匿名对象的场景,如匿名对象,单例以及静态内部类。
有些时候我们会实现一些接口,或者继承某个基类,但仅是在本地一次性使用(One shot),这时匿名对象就派上用场了,类似于Java中的匿名内部类。用**object : **后面跟要实现的接口或者要继承的类:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { ... }
override fun mouseEntered(e: MouseEvent) { ... }
})
用object可以非常方便的实现单例模式:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) { ... }
val allDataProviders: List<DataProvider>
get() = { ... }
}
使用时就直接用类名就可以了:DataProviderManager.registerDataProvider(…)。
在Java中有静态的成员和方法,用以实现一些属于类的成员和方法,在Kotlin中就需要用companion object来实现同样的功能。
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
使用时就是用类+方法:MyClass.create()。
扩展阅读Object expressions and declarations。
对于函数式编程,通常要写大量的PoJo用以在函数之间传递数据,这些对象最大的特点就是仅是数据,且不可变(Immutable),通常的实现方式就是把成员变量全用final修饰(只读read only)。在Kotlin中,可以非常方便的定义这要的类型,即data class。
data class User(val name: String, val age: Int)
针对data class,编译器会自动生成equals, hashCode, toString, copy和componentN方法。注意,虽然成员可以标记为var,但不建议这样做,最好还是都标记为只读val,因为data class就是要Immutable。
扩展阅读Data classes。
密封类和接口是指用关键字sealed修饰的类和接口。它的作用是限制类的层次结构,用sealed修饰的类和接口,它们的所有子类必须在编译的时候就已知,一旦编译完成,不允许再被继承。
密封类型特别适用于库的设计,能够保证库的完整性。通常用于修饰库中的一些关键的有明确类型要求的类型,如消息类型,错误类型等等。因为,库会预定义一些消息类型,以及处理消息的接口,假如调用者扩展了某一消息类型,加了很多自定义的东西,这时再用库中的接口来处理的时候,可能会产生未预期的行为,因为库可能不认识这个新的新的消息类型,但因为是子类继承,语法上是合法的。这时密封类型就能派上用场,把消息类型用sealed修饰,就能保证库的完备性,它提供的错误处理接口一定可以正确处理它定义的消息类型。但注意不能滥用,没有必要为库的每一个类和接口都用sealed修饰,其实大部分时候我们是用不到sealed的。
扩展阅读Sealed classes and interfaces。
一个非常有意思的特性是类型别名,并不是定义一个新类型,而是取个别名。一般情况下,是为了方便,比如目标类型名字太长时,或者有大量的泛型参数时,就可以为它定义一个别名,图个省流。
typealias NodeSet = Set<Network.Node>
typealias MyHandler = (Int, String, Any) -> Unit
扩展阅读Type aliases。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。