Kotlin:类与对象(三)

本篇内容清单如下:

  • 类:声明方式、实例创建、类成员、构造函数(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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容