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


    本文地址


    目录

    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

  • 相关阅读:
    列表方块与格式与布局
    框架与样式表的基本概念
    表单
    内容容器,常用标签
    CSS3
    表单验证
    练习题
    document对象
    windows对象
    函数
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/9020619.html
Copyright © 2020-2023  润新知