kotlin面向对象一

类和对象

定义类

类是某一批对象的抽象,对象是一个具体存在的实体

Kotlin 定义类的语法:

[修饰符] class 类名 [constructor 主构造器] {
    零个到多个次构造器定义...
    零个到多个属性...
    零个到多个方法...
}

修饰符可以是 public|internal|private (只能出现其中之一)、 final|open|abstract (也只能出现其中之一),或者完全省略修饰符,open是final 的反义词,用于修饰一个类、方法或属性,表明类可派生子类,方法或属性可被重写,类名大驼峰表示法

class User constructor(firstName: String) {
    //主构造器可以定义多个形参,这些形参可以在属性声明、初始化块中使用
}

可以省略 constructor 关键字

class User(firstName: String) {

}

如果没有为非抽象类定义任何(主或次)构造器,系统会自动提供一个无参数的主构造器,该构造器默认用 public 修饰

定义属性的语法格式:

[修饰符] var|val 属性名: 类型 [= 默认值]
    [<getter>]
    [<setter>]
  1. 修饰符:修饰符可以省略,也可以是 public | protected | internal | private final | open | abstruct,对于属性使用 public | protected | internal 修饰和不使用访问控制符,效果是一样的。如果使用 private 修饰该属性,该属性将作为幕后属性使用
  2. var|val:使用 var 声明读写属性,使用 val 声明只读属性(不能修改)
  3. 属性名:合法的标识符,小驼峰表示法
  4. 默认值:要么在此处指定初始值,要么在构造器或初始化块中指定初始值
  5. getter、setter:Kotlin会为读写属性提供默认的getter、setter方法:为只读属性提供默认的 getter方法

定义方法的语法和定义函数的语法一样

顶层函数的语法与类中方法的语法的主要区别就在于修饰符:顶层函数不能使用 protected|abstract|final 修饰符,但类中的方法可使用 public | protected | internal | private final | abstract | open 这些修饰符

定义构造器的语法格式:

[修饰符] constructor (形参列表) {
    //由零条到多条可执行语句组成的构造器执行体
}
  1. 修饰符: 修饰符可以省略,也可以是 public|protected|internal|private 其中之一
  2. 形参列表:其格式和定义方法的形参列表的格式完全相同

构造器既不能定义返回值类型,也不能使用 Unit 声明构造器没有返回值

class Person {
    //下面定义了两个属性
    var name: String = ""
    var age: Int = 0

    //下面定义了一个 say 方法
    fun say(content: String) {
        Log.d(TAG, "say: $content")
    }
}

对象的产生和使用

//使用 Person 类定义一个 Person 类型的变量
var p: Person
//调用 Person 类的构造器,返回一个 Person 对象,并赋给p变量
p = Person()

...
//在定义 变量的同时为p变量赋值
var p: Person = Person()

访问方法或属性的语法:

对象.属性|方法(参数)
//访问 的口name属性,直接为该属性赋值
p.name = "kk"
//调用 say()方法
p.say("Kotlin")
//直接输出 name 属性 将输出kk
Log.d(TAG, "onCreate: ${p.name}")

对象的 this 引用

this 关键字总是指向调用该方法的对象,this 最大的作用就是让类中的一个方法访问该类的另一个方法或属性,当 this 出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表的只能是当前类的实例,只有这个方法被调用时,它所代表的对象才被确定下来,谁在调用这个方法, this 就代表谁

class Dog {
        //定义一个名为 age 的属性
        var age = 0

        constructor() {
            //在构造器中定义一个名为 age 的变量
            val age = 3
            //使用 this 代表该构造器正在初始化的对象
            //下面的代码将会把该构造器正在初始化的对象的 age 属性设为0
            this.age = 0
        }

        //定义jump()方法
        fun jump() {
            Log.d(TAG, "jump: ")
        }

        //定义run()方法,run()方法需要借助jump()方法
        fun run(): Dog {
            //使用this引用调用run()方法的对象
            this.jump()

            //Kotlin允许对象的一个成员直接调用另一个成员,可以省略this前缀
            //jump()
            Log.d(TAG, "run: ")
            return this
        }
    }

大部分时候,一个方法调用类中的其他方法、属性时无须使用 this 前缀,但如果方法中有一个局部变量和属性同名,但程序又需要在该方法中访问这个被隐藏的属性,则必须使用 this前缀

程序可以像访问普通变量一样来访问 this 引用,甚至可以把 this 当成普通方法的返回值

方法详解

方法与函数的关系

Kotlin 的方法与函数其实是统一的,不仅定义函数和方法的语法相同,而且定义在类中的方法依然可独立出来

// dogRun 变量的类型应该是(Dog)->Dog
var dogRun: (Dog) -> Dog = Dog::run
// dogRun 变量的类型应该是(Dog)->Unit,系统能自动推断出类型
var dogJump = Dog::jump
var dog = Dog()
dogRun(dog)
dogJump(dog)

Dog 的run()方法类型是() ->Dog类型,但将 run()方法独立成函数时,调用 run()方法的调用者( Dog 对象)将作为第一个参数传入,因此 Dog::run 的实际类型是(Dog)->Dog

中缀表示法

Kotlin 的方法还可使用 infix 修饰,这样该方法就可通过中缀表示法调用,就像这些方法是双目运算符,infix 方法只能有一个参数,因为双目运算符的后面只能带一个参数

class Apple(var weight: Double) {

    //定义中缀方法,使用 infix 修饰
    infix fun add(other: Apple): Double {
        return this.weight + other.weight
    }

    //定义中缀方法,使用 infix 修饰
    infix fun drop(other: Apple): Double {
        return this.weight - other.weight
    }
}

//调用
var apple1 = Apple(5.7)
var apple2 = Apple(3.6)
var totalWeight = apple1 add apple2
var droppedWeight = apple1 drop apple2
Log.d(TAG, "onCreate: totalWeight=$totalWeight droppedWeight=$droppedWeight")

componentN 方法与解构

Kotlin 允许将一个对象N个属性“解构“给多个变量,按照component1()、component2()、component3()...的顺序返回各个属性

class Person constructor(private var name: String, private var age: Int) {
    //定义operator修饰的 componentN 方法,用于解构
    operator fun component1(): String {
        return name
    }

    operator fun component2(): Int {
        return age
    }
}

//使用
var person = Person("小明", 34)
//利用 person 对象的 component1()和 component2()方法
var (name, age) = person
Log.d(TAG, "onCreate: name=$name age=$age")

在某些时候,希望解构对象后面几个 componentN()方 法的返回值、忽略前面几个componentN()方法的返回值,可通过下画线(_)来占位

var (_, age) = person
Log.d(TAG, "onCreate: age=$age")

程序可通过 for-in 循环来遍历 Map 结构,这是由于 Map 包含了一个 operator 修饰的 iterator()方法,且该方法返回了 Iterator<Map.Entry<K,V>>对象,Iterator 代表了可迭代的对象,它的泛型参数代表了被迭代的元素的类型Map.Entry<K,V> (Entry 是 Map 的嵌套类),而 Map.Entry则定义了如下两个方法:

public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value

Map 本身是可迭代的,我们原本可按如下方式遍历 Map 集合:

for (entry in map) {
    
}

但是由于 Kotlin 允许对entry执行解构操作,因此上面的遍历可改写为如下形式:

for ((key, value) in map) {

}

数据类和返回多个值的函数

Kotlin 本身并不支持定义返回多个值的函数或方法,但通过上面所介绍的对象解构,我们同样可让 kotlin 函数返回多个值一一本质是让 kotlin 返回一个支持解构的对象

为了简化解构的实现, Kotlin 提供了一种特殊的类:数据类,专门用于封装数据

数据类除使用 data 修饰之外,还要满足如下要求:

  1. 主构造器至少需要有一个参数
  2. 主构造器的所有参数需要用 var| val 声明为属性
  3. 数据类不能用 abstract |open| sealed 修饰,也不能定义成内部类
  4. 在 Kotlin 1.1 之前,数据类只能实现接口,现在数据类也可继承其他类

定义数据类之后,系统自动为数据类生成如下内容:

  1. 生成 equals()/hashCode() 方法
  2. 自动重写 toString()方法,返回形如"User(name=John, age=42)"的字符串
  3. 为每个属性自动生成 operator 修饰的 componentN()方法
  4. 生成 copy()方法,用于完成对象复制
//定义一个数据类
//数据类会自动为每个属性定义对应的 componentN 方法
data class Person constructor(var name: String, var age: Int)

//返回一个支持解构的对象
fun getResult(name: String, age: Int): Person {
    return Person(name, age)
}

//使用
//通过解构获取函数返回的两个值
var (name, age) = getResult("小明", 34)
Log.d(TAG, "onCreate: name=$name age= $age")
//Pair可包含两个任意类型的属性,Triple可包含三个任意类型的属性
fun getResult(name: String, age: Int): Pair<String,Int> {
    return Pair(name, age)
}

//或者只返回第二个值
var (_, age) = getResult("小明", 34)
Log.d(TAG, "onCreate:  age= $age")

Lambda 表达式中解构

如果 Lambda 表达式的参数是支持解构的类型(如Pair、Map.Entry等,它们都具有 operator 修饰的 componentN()方法),那么就可通过将它们放在括号中引入多个新参数来代替单个参数

map.mapValues { entry -> Log.d(TAG, "onCreate: ${entry.value}") }
//使用解构,将 entry 解构成(key, value)
map.mapValues { (key, value) -> Log.d(TAG, "onCreate: $value") }

Lambda 表达式包含两个参数和使用解构的区别:Lambda 表达式的多个参数是不需要使用圆括号的,只要看到在Lambda 表达式的形参列表中出现圆括号,那就是使用解构,如果希望只使用后面几个 componentN() 方法的返回值,则可使用下画线来代替

{ a -> ... }//一个参数
{ a, b -> ... }//两个参数
{ (a, b) -> ... }//一个解构对
{ (a, b), c -> ...}//一个解构对和第三个参数

属性和字段

读写属性和只读属性

使用 val 定义只读属性,使用 var 定义读写属性,系统会为只读属性生成 getter法,会为读写属性生成 getter和setter 方法,在定义 Kotlin 普通属性时,需要显式指定初始值:要么在定义时指定初始值,要么在构造器中指定初始值

class Address {
    var street: String = ""
    var city = ""
    var province = ""
    var postcode: String? = null
}

如果系统可根据属性初始值推断出属性的类型,那么程序就可以不显式指定属性的类型

Kotlin 定义一个属性,就相当于定义一个 Java 类的 private 修饰的field ,以及 public、final 修饰的 getter和setter 方法,Kotlin 程序使用时只能通过点语法,Java 程序使用时只能通过 getter和setter 方法

var addr = Address()
//通过点语法对属性赋值,实际就是调用 setter 方法
addr.street = "高新街"
addr.city = "成都"
//通过点语法访问属性,实际就是调用 getter 方法
Log.d(TAG, "onCreate: street=${addr.street} city=${addr.city}")

自定义 getter和setter

定义getter和setter方法时无须使用 fun 关键字

class Address {
    var street: String = ""
    var city: String = ""
    //不能指定初始值,因为重写了getter和setter方法,并且在这两个方法中都没有
    //显式的使用field关键字,因此不会生成幕后字段,也就没地方保存初始值
    var address: String
        //自定义 getter 方法
        get() {
            Log.d(TAG, "get: address")
            return "$city-$street"
        }
        //使用单表达式定义 getter 方法的方法体
        //get() = "$city-$street"
        
        set(value) {
            Log.d(TAG, "set: address")
            // value 字符串中不包含“-”或包含几个“-”都不行
            if ("-" !in value || value.indexOf("-") != value.lastIndexOf("-")) {
                Log.d(TAG, "输入不合法: value=$value")
            } else {
                var split = value.split("-")
                city = split[0]
                street = split[1]
            }
        }
}

由于address并不需要真正存储状态,它的返回值其实是通过 street 和 city 两个属性计算出来的,因此 Kotlin 也不需要为其生成对应的 field 。当 Kotlin 不需要为该属性生成对应的 field 时,也就不能为该属性指定初始值

如果仅需要改变 getter 和 setter 方法的可见性或者对其添加注解,但不需要改变默认的实现,那么 Kotlin 允许只自定义 getter 和 setter 方法名,而不重新定义其代码实现

var street: String = ""
private set //将 setter 方法改为 private 修饰,但依然使用默认实现
var city: String = ""
@TestAnnotation set //使用 @TestAnnotation 修饰 setter 方法,但依然使用默认实现

幕后字段

在 Kotlin 中定义一个普通属性时, Kotlin 会为该属性生成 field(字段)、 getter 和 setter 方法(只读属性没有 setter 方法)。 Kotlin 为该属性所生成的 field 就被称为幕后字段(backing field)

如果 Kotlin 类的属性有幕后宇段, Kotlin 要求为该属性显式指定初始值一一要么在定义时指定,要么在构造器中指定,如果没有,则不允许为该属性指定初始值

只要满足以下条件,系统就会为属性生成幕后字段:

  1. 该属性使用 Kotlin 自动生成的 getter 和 setter 方法或其中之一
  2. 重写 getter 、setter 方法时,使用 field 关键字显式引用了幕后字段

幕后属性

如果希望自己定义 field ,并为该 field 提供 getter 和 setter 方法,像 Java所使用的方法

幕后属性就是用 private 修饰的属性,Kotlin 不会为幕后属性生成任何getter 和 setter方法,因此程序不能直接访问幕后属性,必须由开发者为幕后属性提供getter 和 setter方法

class Address {
    //定义 private 修饰的属性,该属性是幕后属性
    private var _address: String = address
    var address: String
    //重写 getter 方法,返回幕后属性的值
    get() = _address
    set(value) {
        Log.d(TAG, "set: address")
        // value 字符串中不包含“-”或包含几个“-”都不行
        if ("-" !in value || value.indexOf("-") != value.lastIndexOf("-")) {
            Log.d(TAG, "输入不合法: value=$value")
        } else {
            var split = value.split("-")
            //对幕后属性赋值
            _address = "${split[0]}-${split[1]}"
        }
    }
}

//使用
var addr = Address()
//对 addr.address 赋值,实际上会转为对幕后属性 addr._address 赋值
addr.address = "成都-高新街"
//访问 addr.address,实际上会转为访问幕后属性addr._address
Log.d(TAG, "onCreate: address=${addr.address}")

延迟初始化属性

Kotlin 要求所有属性必须由程序员显式初始化一一要么在定义该属性时赋初始值;要么在构造器中对该属性赋初始值。但在某些时候,这不是必需的。比如,我们可能通过依赖注入为属性设置初始值

Kotlin 提供了 lateinit 修饰符来解决属性的延迟初始化。使用 lateinit 修饰的属性,可以在定义该属性时和在构造器中都不指定初始值

lateinit 修饰符有以下限制:

  1. lateinit 只能修饰在类体中声明的可变属性(使用 val 声明的属性不行,在主构造器中声明的属性也不行)
  2. lateinit 修饰的属性不能有自定义的 getter 或 setter 方法
  3. lateinit 修饰的属性必须是非空类型
  4. lateinit 修饰的属性不能是原生类型

Kotlin 不会为属性执行默认初始化。因此,如果在 lateinit 属性赋初始值之前访问它,程序将会引发“lateinit property name has not been initialized 异常

class Address {
    //延迟初始化属性
    lateinit var street: String
    lateinit var city: String
}

//使用
var addr = Address()
addr.city = "成都"
addr.street = "高新街"
Log.d(TAG, "onCreate: city=${addr.city} street=${addr.street}")

判断属性是否已经初始化

::属性.isInitialized 可用于判断属性变量是否已经初始化,例如:

    //延迟初始化属性
    lateinit var street: String
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (!::street.isInitialized){
            street="高新"
        }
    }

内联属性

Kotlin 1.1 开始, inline 修饰符可修饰没有幕后字段的属性的 getter 或 setter 方法,既可单独修饰属性的 getter 或 setter 方法;也可修饰属性本身,这相当于同时修饰该属性的 getter 好人 setter 方法

对于使用 inline 修饰的 getter 或 setter 方法,就像前面介绍的内联函数一样,程序在调用 getter 或 setter 方法时也会执行内联化

// inline 修饰属性本身,表明读取和设置属性时都会内联化,
// 下面的getter 和 setter 也加了inline是重复的,这里只是为了演示
inline var address: String
    //inline 修饰属性的 getter 方法,表明读取属性时会内联化
    inline get() {
        Log.d(TAG, "get: address")
        return "$city-$street"
    }
    //inline 修饰属性的 setter 方法,表明设置属性时会内联化
    inline set(value) {
        Log.d(TAG, "set: address")
        // value 字符串中不包含“-”或包含几个“-”都不行
        if ("-" !in value || value.indexOf("-") != value.lastIndexOf("-")) {
            Log.d(TAG, "输入不合法: value=$value")
        } else {
            var split = value.split("-")
            city = split[0]
            street = split[1]
        }
    }

隐藏和封装

封装指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问

包和导包

Kotlin 的包与 Java 的包相同,既是逻辑上的一个程序单元,也是一个命名空间,格式:

package packagename

要求:全部小写字母,公司域名倒写.项目名.模块名.组件类型

使用了这条 package 语句,就意味着该源程序中定义的所有类、函数都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合

package com.example.kotlin

//在com.example.kotlin包下定义一个方法,要在import语句后
fun test() {
    Log.d("TAG", "test: ")
}

//在com.example.kotlin包下定义一个类
class MainActivity : AppCompatActivity() {
    /**
     *
     * @param savedInstanceState Bundle?
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    companion object {
        private const val TAG = "MainActivity"
    }
}

上面程序中 test()函数的完整函数名是 com.example.kotlin.test(),MainActivity类的完整类名是 com.example.kotlin.MainActivity,会在com/example/kotlin文件夹生成3个class文件:

  1. MainActivity$Companion.class
  2. MainActivity.class
  3. MainActivityKt.class

如果没有test()函数则不会生成MainActivityKt.class,test()函数作为MainActivityKt类的类方法而存在

Kotlin的import 语法和 Java 类似:

精确导入:

import androidx.appcompat.app.AppCompatActivity

通配符导入:

import java.io.*

Kotlin不仅可以导入类,还可以导入如下内容:

  1. 顶层函数及属性
  2. 在对象声明中声明的函数和属性
  3. 枚举常量

如果需要在同一个源文件中导入不同包中的同名类(比如 java.util.Date 和 java.sql.Date )

import java.util.Date
//导入 java sql.Date ,并指定别名为 SDate
import java.sql.Date as SDate

Kotlin 的默认导入

Kotlin 默认会导入如下包:

  1. kotlin.*
  2. kotlin.annotation. *
  3. kotlin.collections. *
  4. kotlin.comparisons. *
  5. kotlin.io. *
  6. kotlin.ranges.*
  7. kotlin.sequences. *
  8. kotlin.text. *
  9. java.lang.*
  10. kotlin.jvm. *

使用访问控制符

如果 Kotlin 没有显式指定,默认的访问控制修饰符是public

访问控制符 说明
private private 成员只能在该类的内部或文件的内部被访问
internal internal 成员可在该类的内部或文件的内部或者同一模块内被访问
protected protected 成员可以在该类的内部或文件的内部或者其子类被访问
public 可在任意地方被访问

不同作用域中的成员可支持的访问控制符

位于包内的顶层成员

对于包内的顶层成员(包括顶层类、接口、函数、属性),只能使用 private、internal、public 其中之一,不能使用 protected 修饰符

package com.example.kotlin

private fun testPrivate() {}//该函数仅在 MainActivity.kt 内可访问
internal fun testInternal() {}//该函数可在相同模块内被访问
fun testPublic() {}//该函数可在任意地方被访问
public var a: Int = 7 //该属性可在任意地方被访问
    private set// setter 仅在 MainActivity.kt 内可访问
internal var b = 2 // 该属性可在相同模块内被访问
    private set // setter 仅在 MainActivity.kt 内可访问

属性的 getter 方法的访问权限总是与该属性保持一致,直接定义在包内的顶层函数、属性,会转换成 Kotlin 所生成的类(类名为文件名+Kt 后缀的类)中的静态方法和静态属性

位于类、接口之内的成员

对于位于类、接口之内的成员,能使用 private、internal、protected、public 其中之一

如果重写 protected 成员,且没有显式指定访问权限修饰符,那么该成员依然是protected 访问权限

open class Outer {
    private var a = 1
    protected open var b = 2
    internal var c = 3
    var d = 4 //默认是 public 访问权限

    protected class Nested {
        public var e: Int = 5
    }
}

class Subclass : Outer() {
    // a不可访问
    // b、c、d可访问
    // Nested 和 e 可访问
    override var b = 5 //被重写的 b 依然是 protected 访问权限
}

class Other(o: Outer) {
    // o.a, o.b 不可访问
    // o.c 与 Other 类在同一个模块中,可以被访问
    // o.d 可访问
    // Outer.Nested 不可访问, Nested::e 也不可访问
}

如果需要为主构造器指定访问权限修饰符,则要使用 constructor 关键字,并在该关键宇前面添加 private、internal、protected、public 其中之一

局部声明(包括局部变量、局部方法、局部嵌套类)的作用域仅在该方法(或函数)内有效,因此不能使用访问控制符

深入构造器

主构造器和初始化块

Kotlin 类可以定义。01个主构造器和0N个次构造器。如果主构造器没有任何注解或可见性修饰符,则可以省略 constructor 关键字

主构造器作为类头的一部分,可以声明形参,其作用主要有两点:

  1. 初始化块可以使用主构造器定义的形参
  2. 在声明属性时可以使用主构造器定义的形参

Kotlin 主构造器并不是传统意义上的构造器,它更像 Java 的初始化块,Kotlin 通过主构造器的设计,允许为初始化块传入参数,语法格式如下:

init {
    //初始化块中的可执行代码,可以使用主构造器定义的参数
}
class Person(name: String) {
    //下面定义一个初始化块
    init {
        var a = 6
        if (a > 4) {
            Log.d("TAG", ": Person口初始化块:局部变量 a 的值大于 4")
        }
        Log.d("TAG", ": name 参数为:$name")
    }

    //定义第二个初始化块
    init {
        Log.d("TAG", ": Person 的第二个初始化块")
    }
}

//调用
var person = Person("小明")

当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块,主构造器的主要作用就是为初始化块定义参数,也可以说,初始化块就是主构造器的执行体

如果一个类里定义了两个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行

如果没有为 Kotlin 类提供任何构造器(主构造器和次构造器),则系统会为这个类提供一个无参数的主构造器,因此, Kotlin至少包含一个构造器

次构造器和构造器重载

Java 的初始化块其实是假象,其会自动插入到每个构造器的前面执行

Kotlin 的主构造器其实属于初始化块(或者说,初始化块其实是主构造器的执行体),因此 Kotlin 要求所有的次构造器必须委托调用主构造器,可以直接委托或通过别的次构造器间接委托,所谓“委托”,其实就是要先调用主构造器(执行初始化块中的代码),然后才执行次构造器代码

因此,Kotlin 的初始化块和 Java 的初始化块一样,都是多个次构造器中相同的代码

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载

class Person(name: String) {
    lateinit var gender: String
    var name: String
    var age: Int = 0

    //一个初始化块
    init {
        Log.d("TAG", ": name 参数为:$name")
        this.name = name
    }

    //构造器重载
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }

    //构造器重载
    constructor(name: String, age: Int, gender: String) : this(name, age) {
        this.gender = gender
    }
}

主构造器声明属性

Kotlin 允许在主构造器上声明属性,直接在参数之前使用 var 或 val 即可声明属性,当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性

class Person(var name: String, var age: Int, var gender: String) {
}

//调用
var person = Person("小明", 34, "男")

如果主构造器的所有参数都有默认值,程序能以构造参数的默认值来调用该构造器(即不需要为构造参数传入值),此时看上去就像调用无参数的构造器

class Person(var name: String = "小明", var age: Int = 34, var gender: String = "男") {
}

//调用
var person = Person()

类的继承

Kotlin 的继承同样是单继承,每个子类最多只有一个直接父类

继承的语法

语法格式:

修饰符 class SubClass : Superclass {
    //类定义部分
}

如果在定义一个 Kotlin 类时并未显式指定这个类的直接父类,则这个类默认扩展 Any,Any 类不是java.lang.Object类, Any 类只有 equals()、hashCode() 和 toString()这 3 个方法

Kotlin 的类默认就有 final 修饰,因此 Kotlin 的类默认是不能派生子类的,为了让一个类能派生子类 需要使用 open 修饰该类

open class BaseClass {

}
//这里会报错:This type has a constructor, and thus must be initialized here
//报错是因为:子类构造器总要调用父类构造器一次,而这里并没有调用
//解决方法:在BaseClass后面加上(),相当于调用了父类生成的无参数的主构造器
class Subclass : BaseClass {

}

子类的主构造器

如果子类定义了主构造器,为了让主构造器能调用父类构造器,因此 构造器必须在继承父类的同时委托调用父类构造器

open class BaseClass(var name: String) {

}

//子类没有显式声明主构造器
//子类默认有一个主构造器,因此要在声明继承时委托调用父类构造器
class Subclass1 : BaseClass("小明") {

}

//子类显式声明主构造器
//主构造器必须在声明继承时委托调用父类构造器
class Subclass2(name: String) : BaseClass(name) {

}

子类的次构造器

次构造器同样需要委托调用父类构造器

如果子类定义了主构造器,由于子类的次构造器总会委托调用子类的主构造器(直接或间接),而主构造器一定会委托调用父类构造器,因此子类的所有次构造器最终也调用了父类构造器

如果子类没有定义主构造器,则此时次构造器委托调用父类构造器可分为 3 种方式:

  1. 子类构造器显式使用:this(参数) 显式调用本类中重载的构造器,最终还是要调用父类构造器
  2. 子类构造器显式使用:super(参数) 委托调用父类构造器
  3. 子类构造器既没有:super(参数) 调用,也没有:this(参数) 调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
open class BaseClass {
    lateinit var name: String

    constructor() {}
    constructor(name: String) {
        this.name = name
    }
}

class Subclass1 : BaseClass {
    private var age: Int = 0

    //构造器没有显式委托
    //因此该次构造器将会隐式委托调用父类无参数的构造器
    constructor() {}

    //构造器用 super(name) 显式委托父类带 String 参数的构造器
    constructor(name: String) : super(name) {
    }

    //构造器用 this(name) 显式委托本类中带 String 参数的构造器
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行,因此,最先执行的总是 Any 类的构造器

重写父类的方法

大部分时候,子类总是以父类为基础,额外增加新的属性和方法。但有一种情况例外,子类需要重写父类的方法

//鸟类
open class Bird {
    // Kotlin 默认为所有方法添加 final 修饰符,阻止该方法被重写
    // 添加 open 关键字用于阻止 Kotlin 自动添加 final 修饰符
    open fun fly() {
        Log.d("TAG", "fly: ")
    }
}

//鸵鸟类
class Ostrich : Bird() {
    //重写 Bird 类的 fly() 方法
    override fun fly() {
        Log.d("TAG", " can only run")
    }
}

//调用
// Ostrich 对象
var os = Ostrich()
//执行 Ostrich 对象的 fly() 方法,会调用子类的 fly() 方法
os.fly()

Kotlin 类重写父类的方法必须添加 override 修饰符,这种子类包含与父类同名方法的现象被称为方法重写,方法的重写要遵循“两同两小一大”:

  1. 方法名相同
  2. 形参列表相同
  3. 子类方法的返回值类型应比父类方法的返回值类型更小或相等
  4. 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
  5. 是子类方法的访问权限应比父类方法的访问权限更大或相等

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法使用 super 作为调用者来调用父类中被覆盖的方法

如果父类方法具有 private 访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法,如果子类中定义了一个与父类 private 方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,那么这不是重写,只是在子类中重新定义了个新方法

重写父类的属性

  1. 重写的子类属性的类型与父类属性的类型要兼容
  2. 重写的子类属性要提供更大的访问权限,在访问权限方面,子类属性的访问权限应比父类属性的访问权限更大或相等;只读属性可被读写属性重写,但读写属性不能被只读属性重写
open class BaseClass {
    open protected var price: Double = 10.9
    open val name: String = ""
    open var validDays: Int = 0
}

class Subclass : BaseClass() {
    //正确重写了父类属性,类型兼容,访问权限更大
    public override var price: Double = 0.0

    //正确重写了父类属性,读写属性重写只读属性
    override var name = "book"

    //重写错误,只读属性不能重写读写属性
//    open val validDays: Int = 2
}

super 限定

如果需要在子类方法中调用父类中被覆盖的方法或属性,则可使用 super 限定,super 是 Kotlin 提供的一个关键字,用于限定该对象调用它从父类继承得到的属性或方法

//鸟类
open class Bird {
    open var a = 0

    // Kotlin 默认为所有方法添加 final 修饰符,阻止该方法被重写
    // 添加 open 关键字用于阻止 Kotlin 自动添加 final 修饰符
    protected open fun fly() {
        Log.d("TAG", "fly: ")
    }
}

//鸵鸟类
class Ostrich : Bird() {
    override var a = 9

    //重写 Bird 类的 fly() 方法
    public override fun fly() {
        Log.d("TAG", " can only run")
    }

    fun callOverridedMethod() {
        //在子类方法中通过 super 显式调用父类中被覆盖的方法
        super.fly()
        Log.d("TAG", "callOverridedMethod: ${super.a}")
    }
}

强制重写

如果子类从多个直接超类型(接口或类)继承了同名的成员,那么 Kotlin 要求子类必须重写该成员。如果需要在子类中使用 super 来引用超类型中的成员,则可使用尖括号加超类型名限定的 super 进行引用,如 super<Bar>

interface Bar {
    //接口中成员默认是 open 的
    fun test() {

    }
}

class Subclass : BaseClass(), Bar {
    //编译器要求必须重写 test()
    override fun test() {
        super<BaseClass>.test()//调用父类 BaseClass 的 test()
        super<Bar>.test()//调用父接口 Bar 的 test()
    }
}

多态

Kotlin 的变量也有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态

open class BaseClass {
    open var book = 6
    fun base() {
        Log.d("TAG", "base: 父类的普通方法")
    }

    open fun test() {
        Log.d("TAG", "base: 父类的被覆盖的方法")
    }
}

class Subclass : BaseClass() {
    //重写父类的属性
    override var book = 60

    //重写父类的方法
    override fun test() {
        Log.d("TAG", "base: 子类的覆盖父类的方法")
    }

    fun sub() {
        Log.d("TAG", "base: 子类的普通方法")
    }
}

//调用
//编译时类型和运行时类型不一样,多态发生
var ploymophicBc: BaseClass = SubClass()
//输出 60 ,表明访问的依然是子类对象的属性,因此对象的属性同样具有多态性
Log.d(TAG, "onCreate: ${ploymophicBc.book}")
//下面调用将执行从父类继承的 base() 方法
ploymophicBc.base()
//下面调用将执行当前类的 test() 方法
ploymophicBc.test()
//因为 ploymophicBc 的编译时类型是 BaseClass
// BaseClass 类没有提供 sub() 方法,所以下面代码编译时会山现错误
// ploymophicBc.sub()

子类其实是一种特殊的父类,因此 Kotlin 允许把一个子类对象直接赋给一个父类变量,无须任何类型转换,或者被称为向上转型(upcasting ),向上转型由系统自动完成

ploymophicBc 变量的编译时类型是 BaseClass ,而运行时类型是 Subclass,当运行时调用该变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量,调用同一个方法时表现出多种不同子类的行为特征,这就是多态

ploymophicBc.sub(),这行代码会在编译时引发错误。虽然 ploymophicBc 引用的对象实际上确实包含 sub()方法(可以通过反射来执行该方法),但因为它的编译时类型为 BaseClass ,因此编译时无法调用 sub() 方法

使用 is 检查类型

变量只能调用其编译时类型的方法,而不能调用其运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果要让这个变量调用其运行时类型的方法,就需要把它强制转换成运行时类型,强制转换需要借助于强制转型运算符。Kotlin 的类型转换运算符包含 as 和 as? 两个

为了保证类型转换不会出错, Kotlin 提供了类型检查运算符: is 和 !is

is 运算符的前一个操作数通常是一个变量,后一个操作数通常是一个类( 也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的变量是否为后面类的实例对象,或者前面的变量是否为后面类的子类的实例对象。如果是,则返回 true ,否则返回 false

is 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译时程序报错

!is 就是 is 运算符的反义词,只要程序使用 is 或 !is 对变量进行了判断,系统就会自动将变量的类型转换为目标类型

var a: Any = "kotlin"
//直接访问 length 属性,编译器报错
//由于 a 的编译时类型是 Any ,因此编译时它没有 length 属性
//Log.d(TAG, "onCreate: ${a.length}")
//先判断 String if 条件体中 被自动转换为 String 类型
if (a is String) Log.d(TAG, "onCreate: ${a.length}")

// !is也有这种智能转换
if (a !is String) return
Log.d(TAG, "onCreate: ${a.length}")

//when 分支
when (a) {
    is String -> Log.d(TAG, "onCreate: ${a.length}")
    else -> Log.d(TAG, "onCreate: ${a.javaClass}")
}


// &&运算符,能进入 && 之后的表达式,表明 String 类型
if (a is String && a.length > 3) {

}
// || 运算符,能进入 || 之后的表达式,表明 String 类型
if (a !is String || a.length == 0) {

}

使用 as 运算符转型

  1. as :不安全的强制转型运算符,如果转型失败,程序将会引发 ClassCastException 异常
  2. as ?: 安全的强制转型运算符,如果转型失败,程序不会引发异常,而是返回 null

如果试图把一个父类变量转换成子类类型,则该变量实际引用的实例必须是子类实例才行

//转换失败,date值为null
var date = a as? Date
Log.d(TAG, "onCreate: $date")

//运行时会直接报错:ClassCastException
//var number = a as Number
//Log.d(TAG, "onCreate: $number")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容