kotlin学习总结——类和对象继承接口和抽象类

Posted AC_Jobim

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin学习总结——类和对象继承接口和抽象类相关的知识,希望对你有一定的参考价值。

一、类和对象

1.1 类的声明

Kotlin 中使用关键字 class 声明类

class Invoice { /*……*/ }

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号

class Empty

注意:Kotlin 中的类声明默认就是 finalpublic , 所以在 Kotlin 中不能直接继承一个类,因为默认类是 final 的,此外也不需要像 Java 中一样显式使用 public 修饰符。

//在 Kotlin 中不能直接继承一个类,如果需要继承一个类则需要在基类上加 `open` 关键字修饰。
open class Student(
    private val name: String,
    private val nickName: String,
    private val age: Int
)//Student类被继承需要加open关键字,此外Kotlin中构造器初始化也省去了很多模版代码

class SeniorStudent(
    private val name: String,
    private val nickName: String,
    private val age: Int
) : Student(name, nickName, age)//在Kotlin中继承不再使用extends关键字而是使用:来替代

1.2 类的构造函数

1.2.1 主构造函数

  • 每个类都默认会有一个不带参数的主构造函数,也可以显式的指名参数

    //默认会有一个不带参数的主构造函数
    class People
    
  • 在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

    class Person constructor(firstName: String) { /*……*/ }
    
  • 如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。

    class Person(firstName: String) { /*……*/ }
    
  • 如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:

    class Customer public @Inject constructor(name: String) { /*……*/ }
    
  • 主构函数的参数可以在初始化块中使用,也可以在类体内声明的属性初始化器中使用

1.2.2 init初始化块

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中

class Bird(color: String = "green") {
    init {
        println("color: $color")//init块1
    }
}    

在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起:(初始化块和属性初始化器谁在上面谁先执行)

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}
fun main() {
    InitOrderDemo("hello")
}

执行结果:
First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5

1.2.3 次构造函数

1、类也可以声明前缀有 constructor次构造函数

class Person {
    var children: MutableList<Person> = mutableListOf()
    //声明次构造函数
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

2、如果类有一个主构造函数,每个次构造函数需要直接委托或间接委托给主构造函数。委托到同一个类的另一个构造函数用 this关键字即可:

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

3、初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块:

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor")
    }
}

4、如果一个抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数,可见性是public。如果不希望类有一个公有构造函数,需要声明一个带有非默认可见性的主构造函数;

//3.主动声明一个公有构造函数,“覆盖 ”没有声明任何主次构造函数默认生成的不带参数的主构造函数
class MyPrivateTest private constructor () {
}

1.2.4 注意点

1、当只写了主构造函数,没有次构造函数,就会覆盖了默认的空造函数,此时创建对象只能通过你写的主构造函数来调用

class MyTest(var name: String){
}
//调用
 var test1: MyTest = MyTest() // 这么调用默认的空的构造函数会报错
 val test2: MyTest = MyTest("name") //ok

或者如果按照下面的写法就可以调用了

class MyTest(){
}
//调用
 var test1: MyTest = MyTest() // ok

2、当只写了次构造函数,不写主构造函数,创建对象必须通过次构造函数调用,若不写空的参数构造函数,则调用空的构造函数会报错

open class MyTestTwo{
    constructor(){ // 1
        Log.e("MyTestTwo--", "MyTestTwo construct 1")
    }
    constructor(age: Int){ //2
        Log.e("MyTestTwo--", "MyTestTwo construct 2")
    }
    constructor(age: Int, name:String):this(age){ //3
        Log.e("MyTestTwo--", "MyTestTwo construct 3")
    }

//    var test1: MyTestTwo = MyTestTwo() // ok
//    val test2: MyTestTwo = MyTestTwo(2) //ok
//    val test3: MyTestTwo = MyTestTwo(2,"wf") //ok
}
//如不写1的空构造函数, 则test1的初始化就会报错

3、当既写了主构造,又写了次构造函数,可以直接调用主构造函数或者调用次构造函数创建对象。次构造函数最终都是调用主构造函数来创建对象的。

open class MyTestOne(var name: String){
    init {
        Log.e("MyTestOne--", "MyTestOne init")
    }
    constructor(age: Int):this("test"){
        Log.e("MyTestOne--", "MyTestOne construct 1")
    }
    constructor(age: Int, name:String):this(age){
        Log.e("MyTestOne--", "MyTestOne construct 2 $name")
    }

//    val test1: MyTestOne = MyTestOne("name") //ok
//    val test2: MyTestOne = MyTestOne(1)  //ok
//    val test3: MyTestOne = MyTestOne(1,"wf") //ok
}

4、初始化顺序

  1. 主构造函数声明的属性
  2. 初始化块和属性初始化器谁在上面谁先执行
  3. 次构造函数里的属性赋值和函数调用

在这里插入图片描述

1.3 类的实例化

在 Kotlin 中构造对象不再需要 new 关键字了,而是直接调用类的构造器方法就可以创建一个对象了

例:

var test = Test()// 省略了new关键字
var test1 = Test(1,2)

1.4 Getter()与Setter()

getter()对应Java中的get()函数setter()对应Java中的set()函数。不过注意的是,不存在Getter()与Setter()的,这只是Kotlin中的叫法而已,真是的写法,还是用get()、set()

  • 针对定义的每一个属性,Kotlin 会产生一个field、一个 getter,以及一个 setter(如果需要的话)。
  • field 用来存储属性数据。不能直接定义 field,Kotlin 会封装 field,保护它里面的数据, 只暴露给 getter 和 setter 使用
  • 属性的 getter 方法决定如何读取属性值。每个属性都有 getter 方法
  • setter 方法决定你如何给属性赋值,所以,只有可变属性才会有 setter 方法。换句话说,就是 以 var 关键字定义的属性才有 setter 方法。
  • Kotlin 会自动提供默认的 getter 和 setter 方法,但在需要控制如何读写属性数据时,也可以自定义。这种自定义行为叫作覆盖 getter 和 setter 方法。

代码实例:

class Test{
    
    /*
     * getter 和 setter是可选的
     */
     
    // 当用var修饰时,必须为属性赋默认值(特指基本数据类型,因为自定义的类型可以使用后期初始化属性,见后续) 即使在用getter()的情况下,不过这样写出来,不管我们怎么去改变其值,其值都为`123`
    var test1 : String = ""
        get() = "123"
        set(value){field = value}
    
    // 用val修饰时,用getter()函数时,属性可以不赋默认值。但是不能有setter()函数。
    val test2 : String  
        get() = "123"       // 等价于:val test2 : String = "123"
}

自定义getter()setter()

注意:使用val修饰的属性,是不能有setter()的。而使用var修饰的属性可以同时拥有自定义的getter()setter()

例1:用val修饰的属性自定义情况

class Mime{
    // size属性
    private val size = 0    
    
    // 即isEmpty这个属性,是判断该类的size属性是否等于0
    val isEmpty : Boolean
        get() = this.size == 0

    // 另一个例子
    val num = 2
        get() = if (field > 5) 10 else 0
}

// 测试
fun main(args: Array<String>) {
    val mime = Mime()
    println("isEmpty = ${mime.isEmpty}")
    println("num = ${mime.num}")
}

输出结果为:

isEmpty = true
num = 0

例2:用var修饰的属性自定义情况

class Mime{

    var str1 = "test"
        get() = field   // 这句可以省略,kotlin默认实现的方式
        set(value){
            field = if (value.isNotEmpty()) value else "null"
        }

    var str2 = ""
        get() = "随意怎么修改都不会改变"
        set(value){
            field = if (value.isNotEmpty()) value else "null"
        }
}

// 测试
fun main(args: Array<String>) {
    val mime = Mime()
    
    println("str = ${mime.str1}")
    mime.str1 = ""
    println("str = ${mime.str1}")
    mime.str1 = "kotlin"
    println("str = ${mime.str1}")

    println("str = ${mime.str2}")
    mime.str2 = ""
    println("str = ${mime.str2}")
    mime.str2 = "kotlin"
    println("str = ${mime.str2}")
} 

输出结果为:

str = test
str = null
str = kotlin
str = 随意怎么修改都不会改变
str = 随意怎么修改都不会改变
str = 随意怎么修改都不会改变

1.5 可见性修饰符

在 Kotlin 中默认修饰符与 Java 则不一样,在 Kotlin 默认是 public 而 Java 则默认是 default (包级可见性)。此外 Kotlin 中还存在独有的 internal 访问可见修饰符。

修饰符表示含义与 Java 比较
publicKotlin 默认修饰符,全局可见与 Java 中显式指定的 public 效果一致
protected受保护修饰符,类和子类可见与 Java 一致,除了类和子类可见,其包内也可见
private私有修饰符,只有本类可见,类外文件内可见只能类内可见
internal模块内可见无该修饰符

1.6 编译时常数

Java中,我们可以称其为常量。在kotlin中,我们称其为编译时常数。我们可以用const关键字去声明它。其通常和val修饰符连用

关键字:const

满足const的三个条件:

  1. 位于顶层或者是 object 声明 或 companion object 的一个成员
  2. 初始化为String类型或基本类型的值
  3. 没有自定义 getter

声明常量的三种正确方式

  1. 在顶层声明
  2. object修饰的类中声明,在kotlin中称为对象声明,它相当于Java中一种形式的单例类
  3. 在伴生对象中声明

举例说明:

// 1. 顶层声明
const val NUM_A : String = "顶层声明"

// 2. 在object修饰的类中
object TestConst{
    const val NUM_B = "object修饰的类中"
}

// 3. 伴生对象中
class TestClass{
    companion object {
        const val NUM_C = "伴生对象中声明"
    }
}

fun main(args: Array<String>) {
    println("NUM_A => $NUM_A")
    println("NUM_B => ${TestConst.NUM_B}")
    println("NUM_C => ${TestClass.NUM_C}")
}

输出结果为:

NUM_A => 顶层声明
NUM_B => object修饰的类中
NUM_C => 伴生对象中声明

1.7 延迟初始化和惰性初始化

1.7.1 延迟初始化

一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。

为处理这种情况,你可以用 lateinit 修饰符标记该属性。这样 Kotlin 编译器会允许你延后初始化属性。

class Player {
    lateinit var alignment: String

    fun determineFate() {
        alignment = "Good"
    }

    fun proclaimFate() {
        if (::alignment.isInitialized) println(alignment)
    }
} 

使用 lateinit 关键字就相当于你和自己做了个约定:我会在用它之前负责初始化的。如果在变量还没有初始化的情况下就直接使用它,程序一定会崩溃,并且抛出一个 UninitializedPropertyAccessException 异常。

可以通过 ::alignment.isInitialized 来判断 alignment 是否已经初始化。

1.7.2 惰性初始化

惰性初始化即:惰性初始化是指可以暂时不初始化某个变量,当程序在第一次使用到这个变量(属性)的时候再初始化。

  • 使用lazy{}高阶函数,不能用于类型推断。且该函数在变量的数据类型后面,用by链接。
  • 必须是只读变量,即用val声明的变量。

实例讲解:同样是android中常见的例子

// 声明一个延迟初始化的字符串数组变量
private val mTitles : Array<String> by lazy {
    arrayOf(
            ctx.getString(R.string.tab_title_android),
            ctx.getString(R.string.tab_title_ios),
            ctx.getString(R.string.tab_title_h5)
    )
}

// 声明一个延迟初始化的字符串
private val mStr : String by lazy{
    "我是延迟初始化字符串变量"
}

二、继承、接口与抽象类

2.1 继承

1、超类Any
在Kotlin中,说有的类都是继承于Any类,这是这个没有父类型的类。即当我们定义各类时,它默认是继承与Any这个超类的

class Example // 从 Any 隐式继承

Any 有三个方法:equals()hashCode()toString()。因此,为所有 Kotlin 类都定义了这些方法。

2、继承类的定义

默认情况下,Kotlin 类是final的(它们不能被继承)。 要使一个类可继承,请用 open 关键字标记它。

open class Base // 该类开放继承

继承关键字是:,如需声明一个显式的超类型,请在类头中把超类型放到:(冒号)之后

open class Base(p: Int)

class Derived(p: Int) : Base(p)

3、函数重写

  • 重写函数
    父类的函数也要用open关键字修饰,子类才能覆盖它

    //open关键字修饰的类才可以被继承
    open class Product(val name : String) {
        //函数没有用open修饰符修饰不能被重写
        fun description() = "Product: $name"
        //父类的函数也要open关键字修饰,子类才能覆盖它
        open fun load() = "Nothing"
    
        open fun foo(){}
    }
    
    class LuxuryProduct : Product(""){
    
        override fun load(): String {
            return "LuxuryProduct loading..."
        }
    
        //override fun description() = "LuxuryProduct special function"//重写报错
    }
    
  • 重写属性

    在基类中声明的属性,然后在其基类的实现类中重写该属性,该属性必须以override关键字修饰,并且其属性具有和基类中属性一样的类型。

    注意:当基类中属性的变量修饰符为val时,其实现类重写属性可以用var去修饰。反之则不能

    open class Demo{
        open val valStr = "我是用val修饰的属性"
        
        open var varStr = "我是用valr修饰的属性"
    }
    
    class DemoTest : Demo(){
    
        /*
         * 这里用val、或者var重写都是可以的。
         * 不过当用val修饰的时候不能有setter()函数,编辑器直接会报红的
         */
    
         override var valStr: String = ""
        
    //     override val varStr: String = ""//报错
    
    }
    

4、继承类的构造函数

如果派生类有一个主构造函数,其基类必须用派生类主构造函数的参数就地初始化。

当实现类无主构造函数时,则每个辅助构造函数必须使用super关键字初始化基类型,或者委托给另一个构造函数

class MyView : View {
    constructor(ctx: Context) : super(ctx)
    
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

2.2 接口

  • Kotlin接口不能有构造函数,Kotlin在定义接口时,接口名后是不能加主构造函数的
  • Kotlin的接口中的成员始终是open的
  • 接口中的属性

    你可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。

//定义接口
interface Person {
    //接口中的方法和属性默认都是open的
    //定义抽象的属性
    var name: String?
    var age: Int
    fun eat()
    fun run()

}
class Student(): Person{
    override var name: String? = null

    override var age: Int = 5

    override fun eat() {
    }

    override fun run() {
    }
}

2.3 抽象类

类和其中的某些成员可以声明为abstract。抽象成员在本类中可以不用实现。

abstract  class  抽象类名[(主构造函数)]  [:  继承父类和实现接口]{......}
  • Kotlin中的类默认不可继承,但抽象类就是为继承设计的,因此即使不用open关键字修饰,抽象类也是可以被继承的。
  • 使用abstract关键字修饰的抽象函数也可以不加open。
  • 抽象类中的非抽象方法如果不用open修饰的话,不能被子类覆盖
abstract class Person(){
    //抽象方法
    abstract fun eat()
    //非抽象方法,不能被子类重写
    fun run(){

    }
    //使用open修饰的非抽象方法
    open fun sing(){

    }
}
class Student(): Person(){
    override fun eat() {

    }
    override fun sing() {
        super.sing()
    }
}

2.4 类型转换和类型检测