公众号「稀有猿诉」 原文链接 一文带你吃透Kotlin类与对象
Kotlin是多范式通用编程语言,对面向对象编程(OOP)自然也提供了全方位的支持。通过先前一篇文章,学习了使用Kotlin进行基本面向对象编程的方法,本文将在前文基础之上继续深入的学习面向对象编程的高级特性,以能够写出更加符合OO的代码,并能够从容应对一些复杂的OOP场景。
注意构造的顺序
在构造对象过程中,有三个地方可以对成员进行初始化:1)是在首构造方法(Primary constructor);2)是在声明成员的同时进行初始化,或者是在初始化代码块(init {...})中;3)是在次要构造方法(Secondary constructor)中。
要注意它们之间的区别和执行顺序,首构造方法是最先执行的,但它不能运行代码,只能进行赋值;成员声明和初始化代码块(init {...})是首构造方法的一部分,因此要先于次要构造方法。次要构造方法是最后执行,并且次要构造方法一定要委托到首构造方法。成员声明和初始化代码块之间则依赖于书写的顺序,从上到下执行。
虽然编译器有它的规则来保障顺序,但为了可读性和可维护性,我们不应该完全依赖编译器。这里建议的方式是:
- 把类的最核心的成员放在首构造方法,如必须要依赖的参数,公开的成员,类型体系中的核心成员等,这些应该直接放在首构造方法中,并按重要的顺序进行声明,这样也能方便进行依赖注入和测试Mock对象替换。
- 私有成员应该在类中声明,并且在声明时进行初始化,如果无法初始化就标记为延迟初始(late init)。
- 初始化代码块,应该做一些复杂的初始化过程,或者成员之间有关联的初始化,或者做一些构造完成之后的操作。比如像在ViewModel中,构造之后,可能执行拉取数据,这就非常适合放在init {...}之中。
- 不建议使用次要构造方法,可以用给首构造方法的参数设置默认值的方式来进行成员参数上的重载。
- 初始化代码块要放在所有成员声明之后,以保障执行顺序。
扩展阅读Classes和Properties。
妙用late init
通常成员的初始化可以在声明时完成,比如像集合或者一些简单的原始类型对象(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的妙用
关键字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。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。