End

Kotlin 朱涛-2 面向对象 类 继承 构造 嵌套类 数据类

本文地址


目录

02 | 面向对象

class Person(val name: String, var age: Int)

Kotlin 定义的类,以及类中的方法、属性,默认都是 public final

抽象类

Kotlin 中的抽象类同样也不能直接用来创建对象。

abstract class Person(val name: String) {  // 抽象类
    protected abstract fun walk()          // 抽象方法,可见性为 protected
}

fun main() {
    val person = object : Person("bqt") {
        public override fun walk() = println("$name walk") // 可见性提升为 public
    }
    person.walk()                                          // bqt walk
    println("javaClass: ${person.javaClass.simpleName}")   // javaClass: (空字符串)
    println("javaClass: ${person.javaClass.name}")         // javaClass: KotlinTestKt$main$person$1
    println("class: ${person::class.simpleName}")          // class: null
}

接口

Kotlin 中的接口,除了具有 Java 中接口的特性外,同时还拥有了部分抽象类的特性。

interface Behavior {     // 通过关键字 interface 定义接口
    val canWalk: Boolean // 接口中可以有属性(本质上是一个普通的接口方法)
    fun run()
    fun walk() = println("call walk") // 也可以有默认实现的方法(本质上是静态内部类中的一个静态方法)
}

class Person(val name: String) : Behavior {    // 实现接口法和继承类的语法一致
    override val canWalk: Boolean get() = true // 重写接口的属性
    override fun run() = println("$name run")  // 重写接口的方法
}

fun main() {
    val behavior = object : Behavior {
        override fun run() = println("call run")
        override val canWalk: Boolean get() = true
    }
    behavior.walk()
    behavior.run()

    val person = Person("bqt")
    person.walk()
    person.run()
}

Kotlin 接口中的这些特性,不是基于 Java 1.8 实现的,而是基于编译器做的一些转换来实现的。

继承

  • Kotlin 中使用 : 表示继承或实现,使用关键字 override 表示重写
  • 和 Java 一样,只能继承自一个类,可以同时实现多个接口。但是继承和实现的先后顺序不做要求
  • 在 override 前加上 final 关键字,可以关闭 override 的遗传性 --- 子类不可以重写
abstract class AppCompatActivity {
    protected abstract fun onCreate()
}

interface OnClickListener

class MainActivity : OnClickListener, AppCompatActivity() { // 这里要括号
    override fun onCreate() {}
}

class MainActivity2 : AppCompatActivity {                   // 这里不要括号
    constructor() {}

    override fun onCreate() {} // 省略了 protected,override 函数的可见性是继承自父类的
}

Kotlin 的设计思想

Kotlin 定义的类,以及类中的方法、属性,默认都是 public final

  • 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
}

目的:防止出现 Java 中继承被过度使用的问题。

构造函数

主构造函数 和 次构造函数

Kotlin 中构造函数分为主构造函数(Primary constructor)和次构造函数(Secondary constructor)。

  • 主构造函数是在类头声明的,主构造函数中不包含任何代码
  • 次构造函数是在类中使用 constructor 关键字创建的,可以有一个或多个
  • 如果既没有主构造函数,也没有次构造函数,则在编译期会自动添加一个无参构造函数
  • init 代码块用于在构造函数之后执行额外的初始化逻辑,相当于是构造函数的扩展
  • 如果有主构造函数,次构造函数中必须用 this 直接或间接调用主构造函数
  • 如果没有主构造函数,次构造函数必须显示调用 super
  • 如果没有主构造函数,但有次构造函数,此时默认的无参构造函数就没有了
class Person constructor(var name: String, var age: Int) {}  // 声明主构造函数
class Person(var name: String = "bqt") {} // constructor 可以省略,可以指定默认值

添加关键字 constructor 的好处:点击 constructor 可以定位到调用此构造函数的地方

主构造函数中参数的 val/var

Kotlin 主构造函数中的参数可以使用 var/val 修饰,也可以不加修饰。

  • 使用 var:可以在类中使用,相当于在该类中定义了一个 var 的成员变量
  • 使用 val:可以在类中使用,相当于在该类中定义了一个 val 的成员变量
  • 什么都不加:不可以在该类中使用,这个参数的作用仅仅是传递给父类的构造方法

注意:次构造函数 或 普通函数 中的参数,不可以使用 var/val 修饰

案例一

Kotlin 的 init 代码块和 Java 一样,都在实例化时执行,并且执行顺序都在构造器之前

class Person(private val name: String = "bqt") { // 主构造函数
    private var age = 0
    private var tag = ""

    constructor(name: String, age: Int) : this(name) {
        println("constructor")
        this.age = age // 次构造函数,通过 this 直接调用主构造函数
    }

    constructor(name: String, age: Int, tag: String) : this(name, age) {
        this.tag = tag // 次构造函数,通过 this 间接调用主构造函数
    }

    init {
        println("init")
        tag = tag.ifEmpty { "xx" } // init 代码块,用于在构造函数【之前】执行初始化逻辑
    }
}
class User {
    {System.out.println("init - 1");} // 1
    public User() {
        System.out.println("User");
    } // 3
    {System.out.println("init - 2");} // 2
}

案例二

open class Person(val name: String = "bqt")

class Person0 : Person()         // 前面的小括号可以省略
class Person1() : Person()       // 父类有主构造函数时,后面的小括号不可以省略
class Person2 : Person("P2")     // 没有任何构造函数时,会自动添加一个无参构造函数

class P3(name: String) : Person(name) { // 子类需要显示调用父类的构造函数
    constructor() : this("P3")          // 有主构造函数时,次构造函数必须显示通过 this 调用主构造函数
}

class Person4 : Person {         // 只要有构造函数,默认的无参构造函数就没有了
    // 此时会提示:Secondary constructor should be converted to a primary one
    constructor(name: String) : super(name) // 没有主构造函数时,次构造函数必须通过 super 调用父类的构造函数
    constructor(age: Int) : this("$age")    // 当然,也可以通过 this 间接调用父类的构造函数
}

属性

Kotlin 编译器会自动给类中的 public 属性生成 getter 和 setter 方法(属性默认就是 public 的)

  • val 修饰的变量,对应 Java 中的 final 变量,只有 getter 没有 setter
  • var 修饰的变量,既有 getter 也有 setter

自定义 set

class Person(val name: String) {
    var age: Int = 0
        set(i) {
            field = i + 100 // 这里的 field 代表的就是属性 age
        }

    var sex = 0
        private set // 可以给 set 方法加上可见性修饰符。但是 get 的可见性修饰符必须和属性的保持一致
}

自定义 get

如果希望给 Person 类增加一个功能,根据年龄判断是不是成年人

  • 按照 Java 的思维,我们会增加一个新的方法 isAdult()
  • 按照 Kotlin 的思维,我们可以借助 Kotlin 属性的自定义 getter,将 isAdult 定义成类的属性。
class Person(val name: String, var age: Int) {
    fun isAdult() = age >= 18      // Java 思维:增加一个新的方法
    val isAdult get() = age >= 18  // Kotlin 思维:增加一个新的属性,通过自定义 get 方法改变属性的返回值
}

Kotlin 的设计思想:

  • 语法层面来看,isAdult 本来就是属于人身上的一种属性,而非一个行为,所以定义成一个属性更为合适。
  • 实现层面来看,isAdult 仍然还是个方法。
    • Kotlin 编译器能够分析出,isAdult 这个属性,实际上是根据 age 来做逻辑判断的
    • 所以 Kotlin 编译器可以在 JVM 层面,将其优化为一个方法

通过以上两点,我们就成功在语法层面有了一个 isAdult 属性,但在实现层面仍然是个方法。

以上两种方式,反编译后的 Java 代码完全一样。

小案例

class A {
    val abc = 1
    fun getAbc() = 2 // Platform declaration clash: The following declarations have the same JVM signature
}
fun main() {
    val a = A()
    println(a.time)
    println(a.time) // 每次获取的值都不一样
}

class A {
    val time get() = System.currentTimeMillis().also { Thread.sleep(10) }
}
fun main() {
    val person = Person()
    person.age = 1
    println(person.age) // 102
    println(person.age) // 102
}

class Person {
    var age: Int = 0
        set(i) {
            field = i + 100
        }
        get() = field + 1
}

嵌套类

和 Java 类似,Kotlin 中也有(非静态)内部类、静态内部类的概念

默认是静态内部类

和 Java 相反,Kotlin 中的普通嵌套类,本质上是静态内部类,而非普通内部类

class A {
    val name: String = ""
    fun foo() = 1

    class B {         // 对应 Java 中的【静态内部类】
        val a = name  // 报错,无法在静态内部类 B 中访问外部类 A 中的属性
        val b = foo() // 报错,无法在静态内部类 B 中访问外部类 A 中的方法
    }
}

普通内部类 inner

如果想在 Kotlin 中定义一个普通的内部类,需要在嵌套类前加 inner 关键字

class A {
    val name: String = ""
    fun foo() = 1

    inner class C {   // 对应 Java 当中的【普通内部类】
        val a = name  // 通过
        val b = foo() // 通过
    }
}

Kotlin 的设计思想

  • Java 中的(非静态)内部类会持有外部类的引用,导致非常容易出现内存泄漏问题
  • 大部分 Java 开发者之所以犯这样的错误,往往只是因为忘记了加 static 关键字
  • Kotlin 这样的设计,就将开发者默认犯错的风险完全抹掉了

数据类 data

数据类就是用于存放数据的类。

Kotlin 中引入数据类的目的,是为了解决广泛存在、代码冗余的 Java Bean 问题。

定义:只需要在普通的类前面加上一个关键字 data 即可。

注意,数据类是 final 的,不能被继承(不能使用 open 修饰):Modifier 'open' is incompatible with 'data'

编译器会自动为数据类生成一些有用的方法:

  • copy()
  • toString()
  • equals()
  • hashCode()
data class Person(val name: String, val age: Int)  // 数据类中最少要有一个属性
val person = Person("bqt", 18)

println(person.copy(age = 6)) // 创建一份拷贝的同时,修改某个属性
println(person.toString())    // Person(name=bqt, age=18),和不调用 toString() 的结果一致
val (name, age) = person      // 数据类的解构声明:通过数据类创建一连串的变量
println("$name, $age")        // bqt, 18

println(person == person.copy(name = "bqt"))  // true,结构相等
println(person === person.copy(name = "bqt")) // false,引用不相等

枚举类 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()   // 高版本中会推荐使用 data object
    object WOMAN : Human()
}

使用枚举或者密封类的时候,一定要慎重使用 else 分支,否则,当枚举类扩展后,可能引发不易察觉的问题。

可定义一组有限数量的子类

密封类更多的是和 data 一起使用,用来定义一组有限数量的子类针对同一子类,可以创建不同的对象

不一定非得是 data class,也可以是普通的 class,也可以和 object 等共同使用。
唯一的要求是:不管是 object 值,还是 class,都需要继承自密封类

fun main() {
    dealResult(Result.Success("hello"))
    dealResult(Result.Loading)
    dealResult(UNKNOWN)
    println(Result.Success("hello") === Result.Success("hello")) // false,引用不相等
}

fun dealResult(result: Result<String>) = when (result) {  // 使用 when 表达式
    is Result.Success -> println(result.data) // 判断方式:is Result.Success
    is Result.Error -> println(result.e)
    Result.Loading -> println("Loading...")   // 判断方式:不使用 is
    UNKNOWN -> println("UNKNOWN")
}

sealed class Result<out R> {                               // 用于封装网络请求所有可能的结果
    data class Success<out T>(val data: T) : Result<T>()   // 使用 data class
    data class Error(val e: Exception) : Result<Nothing>()
    data object Loading : Result<Nothing>()                // 使用 data object
}

data object UNKNOWN : Result<Nothing>()                    // 也可以定义在 Result 外面

使用密封类的优势

  • 由于密封类只有有限的几种情况,所以使用 when 表达式时不需要 else 分支 --- 当然也支持 else 分支
  • 如果哪天扩充了密封类的子类数量,那么所有密封类的使用处都会智能检测到,并且给出报错
  • 扩充子类型以后,IDE 可以帮我们快速补充分支类型

注意:前提是是使用了 when 表达式,并且没有使用 else 分支!

类型转换 is as

Kotlin 里使用 is 关键字进行「类型判断」,并且因为编译器能够进行类型推断,可以省略强转的写法。

可以使用 as 关键字强转类型,但如果强转成一个错误的类型,会抛出一个异常。

可以使用 as? 关键字强转类型,如果强转成一个错误的类型,会返回一个 null。

val name: Any = "bqt"
if (name is String) {
    println(name.length)          // 省略强转
}
println((name as String).length)  // 3
println((name as? Int)?.toLong()) // null

小结

  • Kotlin 的类,默认是 public 的,默认是对继承封闭的,类中的成员和方法,默认也是无法被重写的
  • Kotlin 接口可以有成员属性,方法可以有默认实现
  • Kotlin 的嵌套类默认是静态的,这种设计可以防止我们无意中出现内存泄漏问题
  • Kotlin 的密封类,作为枚举和对象的结合体,支持 when 表达式完备性

2018-05-10

posted @ 2018-05-10 17:11  白乾涛  阅读(2111)  评论(0编辑  收藏  举报