美文网首页
Kotlin:类与对象(三)

Kotlin:类与对象(三)

作者: Coralline_xss | 来源:发表于2021-02-08 14:10 被阅读0次

    本篇内容清单如下:

    • 类:声明方式、实例创建、类成员、构造函数(1 主、n 次)
    • 继承:覆盖方法、覆盖属性、类初始化顺序
    • 属性:幕后字段、幕后属性
    • 接口
    • 可见修饰符
    • 扩展:扩展函数、扩展属性、伴生对象
    • 泛型
    • 类的形式:伴生对象、对象表达式与对象声明、抽象类、数据类、嵌套类、内部类、匿名内部类、枚举类、匿名类、内联类
    • 委托:属性委托(动态绑定 & 静态绑定)
    • 委托属性(没太懂)

    一共 16 点内容,原文档中更细,为重点篇章,初学有很多陌生概念。有些可能也不是太理解,不知使用场景,如 函数式(SAM)接口;有些新知识点用起来会弥补 Java 不能实现的一些缺憾,感觉非常不错,如 扩展

    一、类与继承

    1. 类

    1.1 类声明
    • 使用关键字 class 声明。
    • 类声明 = 类名 + 类头(指定其类型参数、主构造函数等)[可选]+ 花括号包围的类体[可选]
    1.2 构造函数
    • 一个 主构造函数类头的一部分,跟在类名后面。
    • 多个 次构造函数:同一般函数,属于 类体 的一部分。
    主构造函数
    • 使用 constructor 关键字,若主构造函数没有 注解或可见性修饰符 ,则可以省略 constructor
    • 一个类仅一个。
    • 不能够包含任何代码。
    • 初始化代码可放在 init 关键字为前缀的 初始化块 中;
    // 主构造函数语法
    class Person(name: String) { ... }
    
    // 访问主构造的参数
    class Person2(name: String) {
        // 1. 在 类体 内声明的 属性初始化器 中使用 主构造的参数
        val introduction = "My name is $name" 
    
        // 2. 在 init 初始化块 中使用 主构造的参数
        init {
            println("name = $name")
        } 
    }
    
    // 简洁语法:声明属性 && 初始化主构造函数的属性
    class Person3(val name: String, var age: Int) { ... } 
    
    次构造函数
    • 类若有一个主构造函数,则每个 次构造函数,必须 委托给 主构造函数:可直接委托,或间接通过其他次构造函数委托。
    • 委托使用 this 关键字。
    class Person(val name: String) {
        val children: MutableList<Person> = mutableListOf()
    
        constructor(name: String, parent: Person) : this(name) {
            parent.add(this)
        }
    }
    

    Tips:

    • init 初始化块中的代码,实际上会是 主构造函数的一部分所以,若主构造函数被调用,则 init初始化块属性初始化器 都会一起被执行。所以即使没有主构造,次构造函数这种委托仍会隐式发生,仍会执行初始化块。
    • 若一个 非抽象类 没有任何 构造函数,均会有一个生产的 不带参数的主构造函数。
    • 若希望类的构造函数为私有,则使用 private 修饰 主构造函数。
    1.3 创建类的实例
    • Kotlin 没有 new 关键字。
    • 创建一个类的实例,就像 普通函数 一样调用 构造函数。
    val person = Person("Coral")
    
    val demo = Demo()
    
    1.4 类成员

    类 可以包括:

    • 构造函数 与 初始化块
    • 函数
    • 属性
    • 嵌套类与内部类 [1]
    • 对象声明 [2]

    2. 继承

    • Kotlin 中所有类都有一个共同的超类 Any ,等同于 Java Object 。
    • Any 有三个方法:equals()、hashCode() 与 toString() 。
    • 默认情况下,Kotlin 类是 final 的,不能被继承,要使类可继承,使用 open 关键字标记
    • 若需声明一个显式的超类类型,则为 class Derived(p: Int) : Base(p)
    • 若子类有一个 主构造函数,其 父类,必须用 子类的主构造函数的参数 就地初始化。
    • 若子类没有 主构造函数,则子类每个 次构造函数 必须使用 super 关键字 初始化其 父类类型 或 委托给另一个构造函数。 如:class MyView : View { constructor(ctx: Context) : super(ctx) }
    2.1 覆盖方法
    • 必须 类 是 open 可继承的方法 添加 open 修饰才有效,才可被子类覆盖。【类可继承成员才允许开放继承
    • 被覆盖的方法,也必须加上 override 修饰符,否则,编译器会报错。【覆盖方法必须显式添加 override
    • 若方法没有标识 open,则子类不允许定义相同签名的函数,无论有没有 override 。【普通函数不能被继承
    • 若标记为 override 的成员,默认是一直可向下被覆盖的。若想禁止再次覆盖,可使用 final 关键字修饰。【final 可禁止覆盖传递
    open class Shape {
        
        open fun draw() { // 可被覆盖 }  
        open fun color() { }
        
        fun fill()  { // 不可覆盖,子类也不允许有同签名方法 }               
    }
    
    class Circle() : Shape() {
        override fun draw()  { // 覆盖方法 }          
        
        final override fun color() { // 禁止再次覆盖 }
    }
    
    2.2 覆盖属性
    • 同 覆盖方法 类似。子类重写的属性必须要以 override 开头,并且类型要兼容。
    • 每个声明的属性 可由 具有 初始化器 的属性 或 get 方法的属性覆盖。
    • 可以用一个 var 覆盖一个 val 属性,反之不行。因为 var 有 get() 和 set() 方法,而 val 只有 get() 方法。
    • 可以在 主构造函数 中使用 override 关键字 作为 属性声明的一部分。
    open class Shape {
        open val vertexCount: Int = 0
    
        open val borderColor: String = ""
        open val bgColor: String = ""
    }
    
    class Rectangle(override val vertexCount: Int =  4) : Shape() {  // 在主构造覆盖属性
    
        override val borderColor: String = "#000000"
        override var bgColor = "#ffffff"   // 使用 var 覆盖 val 属性
    }
    
    2.3 派生类初始化顺序

    构造派生类实例过程:

    • 第一步:完成其基类的初始化(初始化块、属性构造器)
    • 第二步:初始化派生类中 声明或覆盖的属性。

    Tips:

    • 若在 基类初始化 逻辑中,使用了任何 派生类中声明或覆盖的属性,则可能会导致不正确的行为或 运行时故障(因为此时派生类该属性还未初始化)。
    • 设计一个基类时,应避免在 构造函数、属性初始化器 以及 init 块中使用 open 成员。
    2.4 调用超类实现
    • 派生类中 可以使用 super 关键字 调用其超类的函数与属性访问器的实现。
    • 在一个 内部类 中访问 外部类的超类,可通过由 外部类名限定的 super 关键字来实现:super@Outer,如在 Rectangle 类的 inner class 某个方法中调用:super@Rectangle.draw()
    2.5 覆盖规则
    • 若一个类 从它的直接超类 继承 相同成员的多个实现,则必须覆盖这个成员 并提供自己的实现。
    • 为表示从 哪个超类型继承的实现 以消除歧义,使用 尖括号中超类型名限定的 super,如 super<Base>
        open class R1 {
            open fun draw() {  }
        }
    
        // Tips:接口成员默认是 "open" 的
        interface P1 {
            fun draw() {  }
        }
    
        // Tips:继承父类,必须要带上父类的构造方法,有参或无参构造
        class S1 : R1(), P1 {
            override fun draw() {
                super<P1>.draw()  // 调用 P1.draw()
                super<R1>.draw()  // 调用 R1.draw()
            }
        }
    

    3. 属性

    • 属性可使用关键字 var 声明为可变的,也可以是 val 声明为只读的。
    • 属性 初始化器、getter 和 setter 都是可选的。
    3.1 幕后字段

    举个例子,就很容易知道幕后字段如何使用,并且对比 Java 的使用场景,就知道 幕后字段 使用场景也非常多。

    var counter = 0 
        set(value) {
            this.counter = value
        }
    

    上述代码运行后,会出现 stackOverflow,这是为啥?可转换为 Java 代码,set() 里面赋值的那句,直接等价于重新调用了 set() 方法,如此就出现了 自己调用自己,就出现栈溢出错误了。
    为了解决上述问题,便可以使用 幕后字段

    关于某后字段总结如下:

    • 使用 field 标识符在 访问器中引用,并且只能在 属性的访问器内使用。
    • 并不是所有属性都会有幕后字段,需满足以下条件之一:1)属性至少一个访问器使用默认实现;2)自定义访问器通过 field 引用幕后字段。
    • 自定义属性时,借助其他属性赋值,就没有 幕后字段。
    // 正确写法
    var counter = 0 
        get() = field
        set(value) {
            field = value
        }
    
    // 以下 isEmpty 没有幕后字段,因为操作的不是字段本身
    val isEmpty: Boolean
        get() = this.size == 0
        set(value) {
            this.size = 0
        }
    
    3.2 幕后属性
    • 有时候有这种需求,我们希望一个属性:对外表现为只读,对内表现为可读可写,我们将这个属性成为 幕后属性
        //  _table 为 幕后属性,仅内部操作,table 为对外访问属性
        private var _table: Map<String, Int>? = null
    
        public val table: Map<String, Int>
            get() {
                if (_table == null) {
                    _table = HashMap()  // 类型参数已推断出
                }
                return _table ?: throw AssertionError("Set to null by other thres")
            }
    

    备注:一开始不太理解幕后字段 & 幕后属性这两个概念的,详解与使用场景可查看:https://www.jianshu.com/p/c1a4c04eb33c

    3.3 编译期常量
    • 定义:只读属性 的值在编译期是已知的,可以使用 const 修饰符标记。
      该属性需满足条件:
    • 位于顶层 或 object 声明companion object 的一个成员
    • String 或 原生类型值初始化
    • 没有自定义 getter
    // 对比 Java
    private final String a = "A";
    private final static String b = "B";
    public  final static String c = "C";
    
    // 转换为 Kotlin
    val a = "A"
    companion object {
        val b = "B"
        const val c = "C"   // public 常量才能用 const 标记
    }
    

    3. 接口

    • 关键字 interface 定义接口
    • 一个 类或对象 可实现多个接口 [3]
    • 可在接口中定义属性。在接口中声明的属性,要么是抽象的,要么提供访问器实现。另 该属性不能有 幕后字段,因此访问器不能引用。
    3.1 函数式(SAM)接口

    TODO 暂时不理解定义与使用场景。

    4. 可见性修饰符

    省略。

    5. 扩展

    • 能扩展一个 类的新功能 而无需 继承该类 或 使用像装饰者这样的设计模式。如,可为不能修改的第三方库中的类编写一个新的函数
    • 扩展函数:为类新增函数
    • 扩展属性:为类新增属性
    • 扩展 不能真正修改它们所扩展的类,为 静态分发。如出现同 覆盖方法相同签名的 扩展函数,则具体调用哪个方法,取决于 对象类型是 哪一个。扩展方法,调用时,类型必须同声明一致,没有继承的关系。
    • 同签名的 成员函数 与 扩展函数 调用,则调用的一定个是 成员函数

    6. 类的几种表现形式

    6.1 伴生对象
    • 类内部的对象声明可以用 companion 关键字标记。
    • 可以将其写成 该类内 对象声明 中的一员。
    • [ 在 类内 声明了一个 伴生对象,就可以访问其成员,只是以 类名 作为 限定符。]
    • 伴生对象 看起来像其他语言的静态成员,在运行时仍是 真实对象 的实例成员,并且可以实现接口

    创建一个类实例,但是不用显式声明新的子类,使用 对象表达式对象声明 处理。

    6.2 对象表达式
    • 和 Java 以匿名方式构造 抽象类 实例很像。
    6.3 对象声明
    • 单例模式 中常用
    • Kotlin 对 单例 声明 非常容易,总是在 object 关键字后跟一个名称。
    • 就像变量声明一样,但不是一个表达式,不能用作赋值。
    • 对象声明 的初始化过程是 线程安全 的并且在首次访问时进行。

    上述三者之间的语义差别:

    • 对象表达式 是在使用他们的地方 立即 执行(及初始化的);
    • 对象声明 是在第一次被访问到时 延迟 初始化的;
    • 伴生对象 是在相应的类被加载(解析)时初始化的,与 Java 静态初始化器的语义相匹配。

    代码 DEMO:

        // 1. 对象表达式
        // Demo01:创建一个继承自 某个/某些 类型的匿名类的对象
        abstract class BaseAdapter {
            abstract fun getCount(): Int
        }
    
        class MyListView {
            // 初始化一个 adapter,对象表达式 object : BaseAdapter
            var adapter: BaseAdapter = object : BaseAdapter() {
                override fun getCount(): Int = 3
            }
        }
    
       // 2. 对象表达式   
        class DataProvider
    
        // 对象表达式,object 类名 {}   可以在另一个文件
        object DataManager {
            fun register(provider: DataProvider) {
                //
            }
    
            val providers: Collection<DataManager>
                get() {
                    TODO()
                }
        }
    
        fun testSingleton() {
            DataManager.register(DataProvider())
        }
    
        /**
         * 3. 伴生对象
         * - 类内部的对象声明 可以用 companion 关键字标记。
         */
        class MyCompanion {
            companion object Factory {
                fun create(): MyCompanion = MyCompanion()
                
                val globalCount: Int = 0
            }
        }
    
        // Tips:伴生对象的成员 可通过 只是用类名 作为限定符来调用
        val instance = MyCompanion.create()
        val count = MyCompanion.globalCount
    
    
    6.4 抽象类
    • 类 及 其中某些成员声明为 abstract
    • 并不需要用 open 标注一个 抽象类或函数。
    • 可以用 一个抽象成员 覆盖 一个非抽象的开放成员。
    open class Polygon {
        open fun draw() {}
    }
    
    abstract class Rectangle : Polygon() {
        abstract override fun draw()
    }
    
    6.5 数据类
    • 使用 data 标记。只保存数据的类

    编译器会自动从主构造函数中声明的属性导出/生成以下成员:

    • equals() / hashCode 对
    • toString() 格式为:User(name=John, age = 42)
    • copy() 函数

    为生成合理的代码,数据类满足条件:

    • 主构造函数 至少一个参数;
    • 主构造函数 搜索页参数都需要标记为 valvar
    • 数据类不能是 abstract、open、sealed 或 inner;
    • (1.1 之前) 数据类只能实现接口。
    6.6 密封类
    • 使用 sealed 修饰符。
    • 用来表示 受限的类继承结构:当一个值为 有限几种的类型、而不能有任何其他类型时。某种意义上,是 枚举类 的扩展。
    • 密封类 vs 枚举类:每个枚举常量只存在一个实例,而密封类一个子类可包含状态的多个实例。
    • 子类必须与密封类在相同的文件中声明。
    • 一个密封类自身是 抽象的,不能实例化 并且 可以有 抽象(abstract)成员。
    • 密封类 不允许 有非 private 构造函数(默认为 private)。
    • 扩展 密封类子类的类可以放在任何位置,无需同一个文件中。
    6.7 嵌套类与内部类
    • 嵌套类:类可嵌套在其他类中:类、接口可相互嵌套。
    • 内部类:使用 inner 标记的嵌套类,能访问外部类的成员,因为会带有一个外部类的对象引用。
    • 匿名类:
    • 匿名内部类:使用 对象表达式 创建匿名内部类实例(Java 抽象类)。若为接口,则可以使用 lambda 表达式创建(Java 接口)。
    6.8 枚举类
    • 每个枚举常量都是一个对象,用 逗号 分隔。
    • 每个枚举都是 枚举类 的实例,可以如此初始化:
    enum class Color(val rgb: Int) {
                    RED(0xFF0000),
                    GREEN(0x00FF00),
                    BLUE(0x0000FF),
    }
    
    6.9 内联类

    仅在 Kotlin 1.3 版本之后才可用,目前处于 Alpha 版本。

    • 使用 inline 修饰符声明。

    内联类成员限制:

    • 内联类 不能有 init 代码块
    • 不能含有 幕后字段(因此只能有简单的计算属性)

    7. 泛型

    TODO

    8. 委托

    8.1 委托属性

    有些属性类型,随每次可手动调用,但是能实现一次并放入一个库会更好。例如包括:

    • 延迟属性:其值只在首次访问时计算;
    • 可观察属性:监听器会收到有关此属性变更的通知;
    • 把每个属性储存在一个 map 中,而不是每个存在于
      单独的字段中。

    为了涵盖上述情况及其他,Kotlin 支持 委托属性:

    class Demo {
      var p: String by Delegate()
    }
    

    语法:val/var <属性名>: <类型> by <表达式>by 后面的就是该 委托

    更多详见:https://mp.weixin.qq.com/s/BD1zT80IADDZS4CAxmooPg

    相关文章

      网友评论

          本文标题:Kotlin:类与对象(三)

          本文链接:https://www.haomeiwen.com/subject/avhtxltx.html