02 | 面向对象
class Person(val name: String, var age: Int)
Kotlin 定义的类,以及类中的方法、属性,默认都是 public final
的
Kotlin 中的抽象类和 Java 基本一样,同样也不能直接用来创建对象。
abstract class Person(val name: String) { // 抽象类
abstract fun walk() // 抽象方法
Kotlin 中的接口,除了具有 Java 中接口的特性外,同时还拥有了部分抽象类
的特性。
interface Behavior { // 通过关键字 interface 定义接口
fun run() // 接口的声明的需要实现的方法
val canWalk: Boolean // 接口中可以有属性(本质上是一个普通的接口方法)
fun walk() {} // 也可以有默认实现的方法(本质上是静态内部类中的一个静态方法)
class Person(val name: String): Behavior { // 实现接口的语法,和继承类的语法一致
override val canWalk: Boolean // 重写接口的属性
get() = true
override fun run() {} // 重写接口的方法
注意:虽然在 Java 1.8 版本中,接口也引入了类似的特性,但由于 Kotlin 是完全兼容 Java 1.6 版本的,因此 Kotlin 接口中的这些特性,并不是基于 Java 1.8 实现的。
事实上,和其他 Kotlin 的特性一样,Kotlin 中的这些特性,也是基于编译器
在背后做的一些转换来实现的。具体原理后续再解释。
Kotlin 中使用 :
表示继承或实现,使用关键字 override
表示重写
和 Java 一样,只能继承自一个类,可以同时实现多个接口。但是继承和实现的先后顺序不做要求
class MainActivity : OnClickListener, AppCompatActivity() {
override fun onCreate() {}
Kotlin 的设计思想
Kotlin 中的类,默认是不允许被继承的,只有被标记为 open
的类才可以被继承
Kotlin 类内部的方法和属性,默认是不允许被重写的,除非它们也被 open
修饰
Java 的规则是:被 final
修饰的类不可以被继承,被 final
修饰的成员不可以被重写。
open class A { // 只有被标记为 open 的【类】才可以被继承
open var i = 1 // 只有被标记为 open 的【属性】才可以被重写
open fun test() = 1 // 只有被标记为 open 的【方法】才可以被重写
class B : A() {
override var i = 2
override fun test() = 2
Kotlin 这样设计的目的是:防止出现 Java 中继承被过度使用的问题。
主构造函数 和 次构造函数
Kotlin 中构造函数分为主构造函数
(Primary constructor)和次构造函数
(Secondary constructor)。
可以在类头
中声明主构造函数
,主构造函数中不包含任何代码
可以在类中使用 constructor
关键字创建一个或多个次构造函数
如果既没有
主构造函数,也没有
次构造函数,则在编译期会自动添加一个无参构造函数
init 代码块
用于在构造函数之后执行额外的初始化逻辑,相当于是构造函数的扩展
如果有主构造函数,次构造函数中必须用 this
直接或间接调用主构造函数
可以没有主构造函数
没有主构造函数时,次构造函数
必须显示调用 super
没有主构造函数,但同时有次构造函数
时,默认的无参构造函数
就没有了
class Person constructor(var name: String, var age: Int) {} // constructor 可以省略
class Person(var name: String = "bqt", var age: Int = 20) {} // 可以指定默认值
主构造函数中参数的 val/var
Kotlin 主构造函数
中的参数可以使用 var/val
修饰,也可以不加修饰
使用 var:可以在类中使用,相当于在该类中定义了一个 var 的成员变量
使用 val:可以在类中使用,相当于在该类中定义了一个 val 的成员变量
什么都不加:不可以在该类中使用,这个参数的作用仅仅是传递给父类的构造方法
的
次构造函数 或 普通函数 中的参数,不可以使用 var/val
修饰
class Person(private val name: String = "bqt") { // 主构造函数
private var age = 0
private var tag = ""
constructor(name: String, age: Int) : this(name) { this.age = age } // 次构造函数,直接调用主构造函数
constructor(n: String, a: Int, t: String) : this(n, a) { tag = t } // 次构造函数,间接调用主构造函数
init { tag = tag.ifEmpty { "unknown" } } // init 代码块,用于在构造函数【之后】执行额外的初始化逻辑
override fun toString(): String = "$name - $age - $tag"
fun main() {
println(Person()) // bqt - 0 - unknown
println(Person("aaa")) // aaa - 0 - unknown
println(Person("bbb", 20)) // bbb - 20 - unknown
println(Person("ccc", 20, "IT")) // ccc - 20 - IT
open class Person(val name: String = "bqt") {
override fun toString(): String = name
class Person1() : Person() // 没有主构造函数时,后面的小括号可以省略
class Person2 : Person("Person2") // 没有任何构造函数时,会自动添加一个无参构造函数
class Person3(name: String) : Person(name) { // 子类需要显示调用父类的构造函数
constructor() : this("Person3") // 有主构造函数时,次构造函数必须显示通过 this 调用主构造函数
class Person4 : Person { // 只要有构造函数,默认的无参构造函数就没有了
// 此时会提示:Secondary constructor should be converted to a primary one
constructor(n: String) : super(n) // 没有主构造函数时,次构造函数必须通过 super 调用父类的构造函数
constructor(i: Int) : this("$i") // 当然,也可以通过 this 间接调用父类的构造函数
fun main() {
println(Person1())
println(Person2())
println(Person3())
println(Person3("Person3"))
println(Person4("Person4"))
println(Person4(4))
Kotlin 编译器会根据实际情况,自动给类中的属性生成 getter 和 setter 方法
用 val
修饰的变量,对应 Java 中的 final
变量,只有 getter 没有 setter
用 var
修饰的变量,既有 getter 也有 setter
自定义 set
通过自定义属性的 setter 可以改变属性的赋值逻辑。
class Person(val name: String) {
var age: Int = 0
set(intValue) {
field = intValue + 100 // 这里的 field 代表了 age,这行代码就是对属性的赋值操作
var sex = 0
private set // 可以给 set 方法加上可见性修饰符,但是 get 的可见性修饰符必须和属性的保持一致
自定义 get
如果希望给 Person 类增加一个功能,根据年龄判断是不是成年人,按照 Java 的思维,我们会增加一个新的方法:
class Person(val name: String, var age: Int) {
fun isAdult() = age >= 18 // Java 思维:增加一个新的方法
按照 Kotlin 的思维,我们可以借助 Kotlin 属性的自定义 getter,将 isAdult 定义成类的属性。
class Person(val name: String, var age: Int) {
val isAdult // Kotlin 思维:增加一个新的属性
get() = age >= 18 // 通过自定义 get 方法改变属性的返回值
Kotlin 的设计思想
从语法层面
来看,isAdult 本来就是属于人身上的一种属性
,而非一个行为
,所以定义成一个属性更为合适。
从实现层面
来看,isAdult 仍然还是个方法。
Kotlin 编译器能够分析出,isAdult 这个属性,实际上是根据 age 来做逻辑判断的
所以 Kotlin 编译器可以在 JVM 层面,将其优化为一个方法
通过以上两点,我们就成功在语法层面
有了一个 isAdult 属性,但在实现层面
仍然是个方法。
以上两种方式,反编译后的 Java 代码完全一样。
和 Java 类似,Kotlin 中也有(非静态)内部类、静态内部类的概念
和 Java 相反,Kotlin 中的普通嵌套类,本质上是静态内部类,而非普通内部类
如果想在 Kotlin 中定义一个普通的内部类,需要在嵌套类前加 inner
关键字
默认是静态内部类
class A {
val name: String = ""
fun foo() = 1
class B { // 对应 Java 中的【静态内部类】
val a = name // 报错,无法在静态内部类 B 中访问外部类 A 中的属性
val b = foo() // 报错,无法在静态内部类 B 中访问外部类 A 中的方法
inner class C { // 对应 Java 当中的【普通内部类】
val a = name // 通过
val b = foo() // 通过
Kotlin 的设计思想
Java 中的(非静态)内部类会持有外部类的引用,导致非常容易出现内存泄漏
问题
大部分 Java 开发者之所以犯这样的错误,往往只是因为忘记了加 static
关键字
Kotlin 这样的设计,就将开发者默认犯错的风险完全抹掉了
数据类 data
数据类就是用于存放数据的类。Kotlin 中引入数据类的目的,是为了解决广泛存在、代码冗余的 Java Bean 问题。
要定义一个数据类,只需要在普通的类前面加上一个关键字 data
即可。编译器会自动为数据类生成一些有用的方法:
copy()
toString()
equals()
hashCode()
componentN()
copy$default()
data class Person(val name: String, val age: Int) // 数据类中最少要有一个属性
val tom = Person("Tom", 18)
println(tom.copy(age = 6)) // 创建一份拷贝的同时,修改某个属性
println(tom.toString()) // Person(name=Tom, age=18)
val (name, age) = tom // 数据类的解构声明:通过数据类创建一连串的变量
println("$name, $age") // Tom, 18
枚举类 enum
和 Java 类似,Kotlin 中的枚举类也用来表示一组有限数量的值。每一个枚举的值,在内存当中始终都是同一个对象的引用。
enum class Human { MAN, WOMAN }
fun main() {
println(Human.MAN == Human.MAN) // true,结构相等
println(Human.MAN === Human.MAN) // true,引用相等
println(Human.values().toList()) // [MAN, WOMAN]
println("${Human.MAN} - ${Human.MAN.name}") // MAN - MAN
println(Human.valueOf("MAN")) // MAN,注意:valueOf() 是用于解析枚举变量名称
println(Human.valueOf("xxx")) // IllegalArgumentException: No enum constant Human.xxx
在 when 表达式当中使用枚举时,不需要 else 分支,编译器可自动推导逻辑是否完备
fun isMan(data: Human) = when (data) {
Human.MAN -> true
Human.WOMAN -> false
密封类 sealed
密封类是枚举和对象的结合体,是更强大的枚举类。
枚举的局限性:每一个枚举的值,在内存当中始终都是同一个对象引用。而使用密封类,就可以让枚举的值拥有不一样的对象引用。
可定义一组有限数量的值
密封类其实是对枚举的一种补充,枚举类能做的事情,密封类也能做到,比如用来定义一组有限数量的值。
enum class Human { MAN, WOMAN }
sealed class Human {
object MAN : Human()
object WOMAN : Human()
使用枚举或者密封类的时候,一定要慎重使用 else 分支,否则,当枚举类扩展后,可能引发不易察觉的问题。
可定义一组有限数量的子类
密封类,更多的是和 data 一起使用,用来定义一组有限数量的子类
。针对同一子类,可以创建不同的对象,这一点是枚举类无法做到的。
sealed class Result<out R> { // 定义了一个密封类,用于封装网络请求所有可能的结果
data class Success<out T>(val data: T) : Result<T>() // 代表成功的数据类
data class Error(val e: Exception) : Result<Nothing>() // 代表失败的数据类
data class Loading(val time: Long) : Result<Nothing>() // 代表请求中的数据类
首先,我们使用 sealed
定义了一个密封类 Result,这个密封类用于封装网络请求所有可能的结果
然后,在密封类中使用 data
定义了三个数据类,代表网络请求有限的三类结果:成功、失败、请求中
这样,网络请求结束后的 UI 展示逻辑就变得非常简单,就是三个非常清晰的逻辑分支:成功、失败、进行中
fun display(result: Result<String>) = when (result) { // 使用 when 表达式处理网络请求的结果
is Result.Success -> displaySuccessUI(result.data) // 如果是 Success,就展示成功的数据
is Result.Error -> showErrorMsg(result.e) // 如果是 Error,就展示错误提示框
is Result.Loading -> showLoading(result.time) // 如果是 Loading,就展示进度条
fun displaySuccessUI(data: String) = println(data)
fun showErrorMsg(e: Exception) = println(e)
fun showLoading(time: Long) = println(time)
使用密封类的优势
由于密封类只有有限的几种情况,所以使用 when
表达式时不需要 else 分支
如果哪天扩充了密封类的子类数量,那么所有密封类的使用处都会智能检测到,并且给出报错
扩充子类型以后,IDE 可以帮我们快速补充分支类型
注意:前提是是使用了 when
表达式,并且没有使用 else
分支!
Kotlin 的类,默认是 public 的,默认是对继承封闭的,类中的成员和方法,默认也是无法被重写的
Kotlin 接口可以有成员属性,方法可以有默认实现
Kotlin 的嵌套类默认是静态的,这种设计可以防止我们无意中出现内存泄漏问题
Kotlin 的密封类,作为枚举和对象的结合体,支持 when 表达式完备性
2018-05-10
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/9020619.html